From dbecad8606cb3723e6b9543cbecc6fb8277374a1 Mon Sep 17 00:00:00 2001 From: Frank Harris Date: Wed, 17 Jun 2026 13:10:27 -0500 Subject: [PATCH] fixed missing login and billing pages --- Panel/includes/functions.php | 10 + Panel/includes/sso.php | 204 + Panel/modules/administration/module.php | 21 + Panel/modules/billing/BILLING_FIX_SUMMARY.md | 242 - .../modules/billing/COLUMN_RENAME_SUMMARY.md | 110 - Panel/modules/billing/COUPON_SYSTEM.md | 364 - Panel/modules/billing/FIXES_APPLIED.md | 248 - .../billing/GAME_DOCS_TODO_REFERENCE.md | 137 - Panel/modules/billing/INVOICE_FIRST_FLOW.md | 190 - Panel/modules/billing/INVOICE_SYSTEM.md | 133 - .../billing/LOGGING_CHANGES_SUMMARY.md | 223 - Panel/modules/billing/MIGRATION_SUMMARY.md | 201 - Panel/modules/billing/PANEL_INTEGRATION.md | 300 - .../billing/PAYMENT_IMPLEMENTATION_SUMMARY.md | 282 - .../modules/billing/PAYPAL_DEBUGGING_GUIDE.md | 316 - .../billing/PHASE1_COMPLETE_SUMMARY.md | 149 - .../modules/billing/QUICK_DEBUG_REFERENCE.md | 186 - Panel/modules/billing/QUICK_START.md | 261 - Panel/modules/billing/README.md | 196 - Panel/modules/billing/README_COUPON_UPDATE.md | 287 - Panel/modules/billing/RECENT_FIXES_SUMMARY.md | 326 - Panel/modules/billing/STATUS_REPORT.md | 176 - Panel/modules/billing/TESTING_CHECKLIST.md | 339 - .../billing/_archived/CONFIGURATION.md | 165 - Panel/modules/billing/_archived/FEATURES.md | 383 - .../_archived/IMPLEMENTATION_SUMMARY.md | 181 - .../modules/billing/_archived/README_LOGIN.md | 110 - .../modules/billing/_archived/VISUAL_GUIDE.md | 317 - .../ARCHIVE_README.txt | 16 - .../removed-20251023-142000/MOVED_DOCS.md | 3 - .../removed-20251023-202500/MOVED_FILES.json | 75 - .../_archived/removed-20251023-202500/ai.php | 325 - .../data/FREE-548-1761171178.json | 12 - .../data/FREE-549-1761246925.json | 12 - .../data/INV-20250825-170438-e37518.json | 11 - .../data/INV-20250825-174311-0a7993.json | 11 - .../data/NO-INVOICE.json | 10 - .../SIMULATED-WEBHOOK-20251022-101500.json | 10 - .../billing/add_override_price_column.sql | 25 - .../billing/add_paypal_data_column.sql | 10 - .../add_remote_server_enabled_column.sql | 41 - .../modules/billing/add_service_id_column.sql | 10 - Panel/modules/billing/add_to_cart.php | 468 -- Panel/modules/billing/admin.php | 75 - Panel/modules/billing/admin_config.php | 896 -- Panel/modules/billing/admin_coupons.php | 537 -- Panel/modules/billing/admin_invoices.php | 173 - Panel/modules/billing/admin_orders.php | 241 - Panel/modules/billing/admin_payments.php | 97 - Panel/modules/billing/admin_xml_editor.php | 161 - Panel/modules/billing/adminserverlist.php | 718 -- Panel/modules/billing/ai.php | 326 - Panel/modules/billing/api/capture_order.php | 433 - Panel/modules/billing/api/create_order.php | 104 - Panel/modules/billing/api/log_error.php | 44 - Panel/modules/billing/billing_bootstrap.php | 175 - Panel/modules/billing/bootstrap.php | 143 - Panel/modules/billing/cart.php | 956 --- Panel/modules/billing/check_table.php | 76 - Panel/modules/billing/checkout_free.php | 261 - .../billing/classes/BillingRepository.php | 674 -- .../billing/classes/BillingService.php | 179 - .../billing/classes/GatewayFactory.php | 30 - .../modules/billing/classes/ManualGateway.php | 36 - .../modules/billing/classes/PayPalGateway.php | 194 - .../classes/PaymentGatewayInterface.php | 40 - .../modules/billing/classes/StripeGateway.php | 25 - Panel/modules/billing/cleanupDB.sh | 3 - .../modules/billing/create_coupons_table.sql | 107 - .../modules/billing/create_invoices_table.sql | 34 - Panel/modules/billing/create_servers.php | 1535 ---- Panel/modules/billing/cron-shop.php | 433 - Panel/modules/billing/css/header.css | 423 - Panel/modules/billing/diag_remote.php | 77 - Panel/modules/billing/docs.php | 431 - .../modules/billing/docs/7daystodie/icon.jpg | Bin 40986 -> 0 bytes .../modules/billing/docs/7daystodie/index.php | 413 - .../billing/docs/7daystodie/metadata.json | 7 - .../COMPREHENSIVE_DOCUMENTATION_UPDATE.md | 234 - .../docs/DOCUMENTATION_ENHANCEMENT_SUMMARY.md | 250 - .../docs/DOCUMENTATION_EXPANSION_PLAN.md | 271 - .../modules/billing/docs/GAME_SERVER_LIST.md | 282 - .../modules/billing/docs/GENERATION_README.md | 199 - ...sting Reference (Multiplayer PC Games).pdf | Bin 135853 -> 0 bytes .../billing/docs/IMPLEMENTATION_SUMMARY.md | 198 - Panel/modules/billing/docs/README.md | 160 - Panel/modules/billing/docs/XML-Notes.md | 622 -- .../billing/docs/aliensvspredator/icon.png | Bin 2869 -> 0 bytes .../billing/docs/aliensvspredator/index.php | 411 - .../docs/aliensvspredator/metadata.json | 7 - Panel/modules/billing/docs/amxmodx/index.php | 362 - .../billing/docs/amxmodx/metadata.json | 7 - Panel/modules/billing/docs/aoc/icon.png | Bin 3622 -> 0 bytes Panel/modules/billing/docs/aoc/index.php | 430 - Panel/modules/billing/docs/aoc/metadata.json | 7 - Panel/modules/billing/docs/arkse/icon.jpg | Bin 133659 -> 0 bytes Panel/modules/billing/docs/arkse/index.php | 600 -- .../modules/billing/docs/arkse/metadata.json | 7 - .../billing/docs/arma-reforger/icon.png | Bin 3266 -> 0 bytes .../billing/docs/arma-reforger/index.php | 431 - .../billing/docs/arma-reforger/metadata.json | 7 - Panel/modules/billing/docs/arma2co/icon.png | Bin 3010 -> 0 bytes Panel/modules/billing/docs/arma2co/index.php | 457 -- .../billing/docs/arma2co/metadata.json | 7 - Panel/modules/billing/docs/arma2oa/icon.jpg | Bin 40955 -> 0 bytes Panel/modules/billing/docs/arma2oa/index.php | 435 - .../billing/docs/arma2oa/metadata.json | 7 - Panel/modules/billing/docs/arma3/icon.jpg | Bin 33192 -> 0 bytes Panel/modules/billing/docs/arma3/index.php | 460 -- .../modules/billing/docs/arma3/metadata.json | 7 - .../billing/docs/assettocorsa/icon.png | Bin 3168 -> 0 bytes .../billing/docs/assettocorsa/index.php | 413 - .../billing/docs/assettocorsa/metadata.json | 7 - Panel/modules/billing/docs/atlas/icon.png | Bin 2490 -> 0 bytes Panel/modules/billing/docs/atlas/index.php | 479 -- .../modules/billing/docs/atlas/metadata.json | 7 - Panel/modules/billing/docs/avorion/icon.jpg | Bin 32342 -> 0 bytes Panel/modules/billing/docs/avorion/index.php | 511 -- .../billing/docs/avorion/metadata.json | 7 - Panel/modules/billing/docs/b3/index.php | 362 - Panel/modules/billing/docs/b3/metadata.json | 7 - Panel/modules/billing/docs/bec/icon.jpg | Bin 71176 -> 0 bytes Panel/modules/billing/docs/bec/index.php | 379 - Panel/modules/billing/docs/bec/metadata.json | 7 - Panel/modules/billing/docs/bf2/icon.png | Bin 2772 -> 0 bytes Panel/modules/billing/docs/bf2/index.php | 362 - Panel/modules/billing/docs/bf2/metadata.json | 7 - Panel/modules/billing/docs/bfbc2/icon.png | Bin 2618 -> 0 bytes Panel/modules/billing/docs/bfbc2/index.php | 387 - .../modules/billing/docs/bfbc2/metadata.json | 7 - .../billing/docs/bloodfrontier/icon.png | Bin 2702 -> 0 bytes .../billing/docs/bloodfrontier/index.php | 362 - .../billing/docs/bloodfrontier/metadata.json | 7 - .../modules/billing/docs/brainbread2/icon.jpg | Bin 175402 -> 0 bytes .../billing/docs/brainbread2/index.php | 510 -- .../billing/docs/brainbread2/metadata.json | 7 - .../modules/billing/docs/callofduty/icon.png | Bin 3229 -> 0 bytes .../modules/billing/docs/callofduty/index.php | 412 - .../billing/docs/callofduty/metadata.json | 7 - .../modules/billing/docs/callofduty2/icon.png | Bin 3400 -> 0 bytes .../billing/docs/callofduty2/index.php | 404 - .../billing/docs/callofduty2/metadata.json | 7 - .../billing/docs/callofduty4mw/icon.png | Bin 3245 -> 0 bytes .../billing/docs/callofduty4mw/index.php | 412 - .../billing/docs/callofduty4mw/metadata.json | 7 - .../billing/docs/callofdutymw2/icon.png | Bin 3245 -> 0 bytes .../billing/docs/callofdutymw2/index.php | 375 - .../billing/docs/callofdutymw2/metadata.json | 7 - .../billing/docs/callofdutymw3/icon.png | Bin 3245 -> 0 bytes .../billing/docs/callofdutymw3/index.php | 407 - .../billing/docs/callofdutymw3/metadata.json | 7 - .../billing/docs/callofdutyuo/icon.png | Bin 3245 -> 0 bytes .../billing/docs/callofdutyuo/index.php | 402 - .../billing/docs/callofdutyuo/metadata.json | 7 - .../billing/docs/callofdutywaw/icon.png | Bin 3245 -> 0 bytes .../billing/docs/callofdutywaw/index.php | 402 - .../billing/docs/callofdutywaw/metadata.json | 7 - .../modules/billing/docs/citadelfwf/icon.png | Bin 3121 -> 0 bytes .../modules/billing/docs/citadelfwf/index.php | 415 - .../billing/docs/citadelfwf/metadata.json | 7 - .../billing/docs/cod_blackops/icon.png | Bin 3732 -> 0 bytes .../billing/docs/cod_blackops/index.php | 385 - .../billing/docs/cod_blackops/metadata.json | 7 - .../billing/docs/colonysurvival/icon.jpg | Bin 47610 -> 0 bytes .../billing/docs/colonysurvival/index.php | 466 -- .../billing/docs/colonysurvival/metadata.json | 7 - .../billing/docs/common-issues/icon.png | Bin 70 -> 0 bytes .../billing/docs/common-issues/index.php | 372 - .../billing/docs/common-issues/metadata.json | 6 - .../modules/billing/docs/conanexiles/icon.jpg | Bin 37168 -> 0 bytes .../billing/docs/conanexiles/index.php | 415 - .../billing/docs/conanexiles/metadata.json | 7 - Panel/modules/billing/docs/cs2d/icon.png | Bin 2802 -> 0 bytes Panel/modules/billing/docs/cs2d/index.php | 362 - Panel/modules/billing/docs/cs2d/metadata.json | 7 - Panel/modules/billing/docs/csgo/icon.png | Bin 3168 -> 0 bytes Panel/modules/billing/docs/csgo/index.php | 498 -- Panel/modules/billing/docs/csgo/index_old.php | 67 - Panel/modules/billing/docs/csgo/metadata.json | 7 - Panel/modules/billing/docs/cspromod/icon.png | Bin 3081 -> 0 bytes Panel/modules/billing/docs/cspromod/index.php | 390 - .../billing/docs/cspromod/metadata.json | 7 - Panel/modules/billing/docs/css/icon.jpg | Bin 2126 -> 0 bytes Panel/modules/billing/docs/css/index.php | 430 - Panel/modules/billing/docs/css/metadata.json | 7 - Panel/modules/billing/docs/cstrike/icon.jpg | Bin 28138 -> 0 bytes Panel/modules/billing/docs/cstrike/index.php | 430 - .../billing/docs/cstrike/metadata.json | 7 - Panel/modules/billing/docs/czero/icon.png | Bin 3196 -> 0 bytes Panel/modules/billing/docs/czero/index.php | 438 - .../modules/billing/docs/czero/metadata.json | 7 - Panel/modules/billing/docs/dayz/icon.png | Bin 2483 -> 0 bytes Panel/modules/billing/docs/dayz/index.php | 436 - Panel/modules/billing/docs/dayz/metadata.json | 7 - Panel/modules/billing/docs/dayzmod/icon.jpg | Bin 37224 -> 0 bytes Panel/modules/billing/docs/dayzmod/index.php | 436 - .../billing/docs/dayzmod/metadata.json | 7 - Panel/modules/billing/docs/dmc/icon.jpg | Bin 2269 -> 0 bytes Panel/modules/billing/docs/dmc/index.php | 438 - Panel/modules/billing/docs/dmc/metadata.json | 7 - Panel/modules/billing/docs/dod/icon.png | Bin 2917 -> 0 bytes Panel/modules/billing/docs/dod/index.php | 457 -- Panel/modules/billing/docs/dod/metadata.json | 7 - Panel/modules/billing/docs/dods/icon.jpg | Bin 25387 -> 0 bytes Panel/modules/billing/docs/dods/index.php | 430 - Panel/modules/billing/docs/dods/metadata.json | 7 - Panel/modules/billing/docs/doi/icon.jpg | Bin 1920 -> 0 bytes Panel/modules/billing/docs/doi/index.php | 494 -- Panel/modules/billing/docs/doi/metadata.json | 7 - .../billing/docs/dontstarvetogether/icon.png | Bin 3062 -> 0 bytes .../billing/docs/dontstarvetogether/index.php | 402 - .../docs/dontstarvetogether/metadata.json | 7 - Panel/modules/billing/docs/dystopia/icon.png | Bin 2909 -> 0 bytes Panel/modules/billing/docs/dystopia/index.php | 510 -- .../billing/docs/dystopia/metadata.json | 7 - Panel/modules/billing/docs/eco/icon.jpg | Bin 44396 -> 0 bytes Panel/modules/billing/docs/eco/index.php | 375 - Panel/modules/billing/docs/eco/metadata.json | 7 - .../modules/billing/docs/empyriongs/icon.png | Bin 1813 -> 0 bytes .../modules/billing/docs/empyriongs/index.php | 413 - .../billing/docs/empyriongs/metadata.json | 7 - .../billing/docs/enemyterritory/icon.png | Bin 3474 -> 0 bytes .../billing/docs/enemyterritory/index.php | 399 - .../billing/docs/enemyterritory/metadata.json | 7 - Panel/modules/billing/docs/epochmod/icon.png | Bin 3384 -> 0 bytes Panel/modules/billing/docs/epochmod/index.php | 437 - .../billing/docs/epochmod/metadata.json | 7 - Panel/modules/billing/docs/esmod/icon.png | Bin 1996 -> 0 bytes Panel/modules/billing/docs/esmod/index.php | 438 - .../modules/billing/docs/esmod/metadata.json | 7 - Panel/modules/billing/docs/ets2/icon.png | Bin 1920 -> 0 bytes Panel/modules/billing/docs/ets2/index.php | 413 - Panel/modules/billing/docs/ets2/metadata.json | 7 - Panel/modules/billing/docs/factorio/icon.jpg | Bin 69468 -> 0 bytes Panel/modules/billing/docs/factorio/index.php | 430 - .../billing/docs/factorio/metadata.json | 7 - .../billing/docs/feedthebeast/icon.png | Bin 1774 -> 0 bytes .../billing/docs/feedthebeast/index.php | 362 - .../billing/docs/feedthebeast/metadata.json | 7 - Panel/modules/billing/docs/fgms/icon.png | Bin 2182 -> 0 bytes Panel/modules/billing/docs/fgms/index.php | 362 - Panel/modules/billing/docs/fgms/metadata.json | 7 - Panel/modules/billing/docs/fivem/icon.png | Bin 1495 -> 0 bytes Panel/modules/billing/docs/fivem/index.php | 371 - .../modules/billing/docs/fivem/metadata.json | 7 - Panel/modules/billing/docs/fof/icon.jpg | Bin 52615 -> 0 bytes Panel/modules/billing/docs/fof/index.php | 480 -- Panel/modules/billing/docs/fof/metadata.json | 7 - Panel/modules/billing/docs/freecol/icon.png | Bin 1583 -> 0 bytes Panel/modules/billing/docs/freecol/index.php | 375 - .../billing/docs/freecol/metadata.json | 7 - .../docs/gameserver_catalog_all_sources.yaml | 1052 --- .../docs/gameserver_catalog_lgsm_full.yaml | 432 - .../docs/gameserver_knowledgepack_v2.yaml | 692 -- Panel/modules/billing/docs/garrysmod/icon.jpg | Bin 20463 -> 0 bytes .../modules/billing/docs/garrysmod/index.php | 479 -- .../billing/docs/garrysmod/metadata.json | 7 - Panel/modules/billing/docs/gearbox/icon.png | Bin 3231 -> 0 bytes Panel/modules/billing/docs/gearbox/index.php | 430 - .../billing/docs/gearbox/metadata.json | 7 - .../billing/docs/getting-started/icon.png | Bin 70 -> 0 bytes .../billing/docs/getting-started/index.php | 93 - .../docs/getting-started/metadata.json | 6 - Panel/modules/billing/docs/halo_ce/icon.png | Bin 1536 -> 0 bytes Panel/modules/billing/docs/halo_ce/index.php | 362 - .../billing/docs/halo_ce/metadata.json | 7 - Panel/modules/billing/docs/harsh/icon.jpg | Bin 39125 -> 0 bytes Panel/modules/billing/docs/harsh/index.php | 430 - .../modules/billing/docs/harsh/metadata.json | 7 - .../billing/docs/hidden_source/icon.png | Bin 2151 -> 0 bytes .../billing/docs/hidden_source/index.php | 406 - .../billing/docs/hidden_source/metadata.json | 7 - Panel/modules/billing/docs/hl2d/icon.png | Bin 1692 -> 0 bytes Panel/modules/billing/docs/hl2d/index.php | 472 -- Panel/modules/billing/docs/hl2d/metadata.json | 7 - Panel/modules/billing/docs/hldm/icon.png | Bin 1679 -> 0 bytes Panel/modules/billing/docs/hldm/index.php | 438 - Panel/modules/billing/docs/hldm/metadata.json | 7 - Panel/modules/billing/docs/hltv/icon.png | Bin 1199 -> 0 bytes Panel/modules/billing/docs/hltv/index.php | 430 - Panel/modules/billing/docs/hltv/metadata.json | 7 - Panel/modules/billing/docs/homefront/icon.png | Bin 1779 -> 0 bytes .../modules/billing/docs/homefront/index.php | 434 - .../billing/docs/homefront/metadata.json | 7 - Panel/modules/billing/docs/hurtworld/icon.jpg | Bin 2645 -> 0 bytes .../modules/billing/docs/hurtworld/index.php | 411 - .../billing/docs/hurtworld/metadata.json | 7 - Panel/modules/billing/docs/il2/icon.png | Bin 2184 -> 0 bytes Panel/modules/billing/docs/il2/index.php | 362 - Panel/modules/billing/docs/il2/metadata.json | 7 - Panel/modules/billing/docs/ins/icon.jpg | Bin 34600 -> 0 bytes Panel/modules/billing/docs/ins/index.php | 502 -- Panel/modules/billing/docs/ins/metadata.json | 7 - .../billing/docs/insurgencymic/icon.png | Bin 2107 -> 0 bytes .../billing/docs/insurgencymic/index.php | 454 -- .../billing/docs/insurgencymic/metadata.json | 7 - .../billing/docs/insurgencysandstorm/icon.png | Bin 2107 -> 0 bytes .../docs/insurgencysandstorm/index.php | 532 -- .../docs/insurgencysandstorm/metadata.json | 7 - Panel/modules/billing/docs/ivmp/icon.png | Bin 2225 -> 0 bytes Panel/modules/billing/docs/ivmp/index.php | 362 - Panel/modules/billing/docs/ivmp/metadata.json | 7 - Panel/modules/billing/docs/jcmp/icon.png | Bin 2319 -> 0 bytes Panel/modules/billing/docs/jcmp/index.php | 402 - Panel/modules/billing/docs/jcmp/metadata.json | 7 - .../modules/billing/docs/jediknight2/icon.png | Bin 2343 -> 0 bytes .../billing/docs/jediknight2/index.php | 362 - .../billing/docs/jediknight2/metadata.json | 7 - .../billing/docs/jediknightja/icon.png | Bin 2215 -> 0 bytes .../billing/docs/jediknightja/index.php | 372 - .../billing/docs/jediknightja/metadata.json | 7 - .../billing/docs/killingfloor/icon.jpg | Bin 28352 -> 0 bytes .../billing/docs/killingfloor/index.php | 425 - .../billing/docs/killingfloor/metadata.json | 7 - .../billing/docs/killingfloor2/icon.jpg | Bin 57305 -> 0 bytes .../billing/docs/killingfloor2/index.php | 437 - .../billing/docs/killingfloor2/metadata.json | 7 - Panel/modules/billing/docs/left4dead/icon.jpg | Bin 57336 -> 0 bytes .../modules/billing/docs/left4dead/index.php | 487 -- .../billing/docs/left4dead/metadata.json | 7 - .../modules/billing/docs/left4dead2/icon.jpg | Bin 39230 -> 0 bytes .../modules/billing/docs/left4dead2/index.php | 478 -- .../billing/docs/left4dead2/metadata.json | 7 - .../billing/docs/lifeisfeudal/icon.png | Bin 1854 -> 0 bytes .../billing/docs/lifeisfeudal/index.php | 402 - .../billing/docs/lifeisfeudal/metadata.json | 7 - .../modules/billing/docs/mab_warband/icon.png | Bin 2797 -> 0 bytes .../billing/docs/mab_warband/index.php | 402 - .../billing/docs/mab_warband/metadata.json | 7 - .../billing/docs/mafia2online/icon.png | Bin 2875 -> 0 bytes .../billing/docs/mafia2online/index.php | 371 - .../billing/docs/mafia2online/metadata.json | 7 - .../billing/docs/metamodsource/index.php | 362 - .../billing/docs/metamodsource/metadata.json | 7 - Panel/modules/billing/docs/minecraft/icon.png | Bin 70 -> 0 bytes .../modules/billing/docs/minecraft/index.php | 376 - .../billing/docs/minecraft/index_old.php | 91 - .../billing/docs/minecraft/metadata.json | 7 - .../modules/billing/docs/miscreated/icon.jpg | Bin 5570 -> 0 bytes .../modules/billing/docs/miscreated/index.php | 432 - .../billing/docs/miscreated/metadata.json | 7 - Panel/modules/billing/docs/mohaa/icon.png | Bin 3012 -> 0 bytes Panel/modules/billing/docs/mohaa/index.php | 362 - .../modules/billing/docs/mohaa/metadata.json | 7 - Panel/modules/billing/docs/mohbr/icon.png | Bin 3012 -> 0 bytes Panel/modules/billing/docs/mohbr/index.php | 362 - .../modules/billing/docs/mohbr/metadata.json | 7 - Panel/modules/billing/docs/mohsp/icon.png | Bin 3012 -> 0 bytes Panel/modules/billing/docs/mohsp/index.php | 362 - .../modules/billing/docs/mohsp/metadata.json | 7 - Panel/modules/billing/docs/mohspdemo/icon.png | Bin 3012 -> 0 bytes .../modules/billing/docs/mohspdemo/index.php | 362 - .../billing/docs/mohspdemo/metadata.json | 7 - Panel/modules/billing/docs/mordhau/icon.jpg | Bin 51057 -> 0 bytes Panel/modules/billing/docs/mordhau/index.php | 411 - .../billing/docs/mordhau/metadata.json | 7 - .../billing/docs/multitheftauto/icon.png | Bin 2504 -> 0 bytes .../billing/docs/multitheftauto/index.php | 371 - .../billing/docs/multitheftauto/metadata.json | 7 - Panel/modules/billing/docs/mumble/icon.jpg | Bin 47416 -> 0 bytes Panel/modules/billing/docs/mumble/index.php | 362 - .../modules/billing/docs/mumble/metadata.json | 7 - Panel/modules/billing/docs/nexuiz/icon.png | Bin 2176 -> 0 bytes Panel/modules/billing/docs/nexuiz/index.php | 378 - .../modules/billing/docs/nexuiz/metadata.json | 7 - .../modules/billing/docs/nmrih_steam/icon.jpg | Bin 41993 -> 0 bytes .../billing/docs/nmrih_steam/index.php | 438 - .../billing/docs/nmrih_steam/metadata.json | 7 - Panel/modules/billing/docs/ns2/icon.png | Bin 2413 -> 0 bytes Panel/modules/billing/docs/ns2/index.php | 430 - Panel/modules/billing/docs/ns2/metadata.json | 7 - .../modules/billing/docs/nucleardawn/icon.png | Bin 2694 -> 0 bytes .../billing/docs/nucleardawn/index.php | 438 - .../billing/docs/nucleardawn/metadata.json | 7 - Panel/modules/billing/docs/ootow/icon.jpg | Bin 43686 -> 0 bytes Panel/modules/billing/docs/ootow/index.php | 413 - .../modules/billing/docs/ootow/metadata.json | 7 - Panel/modules/billing/docs/openttd/icon.png | Bin 3277 -> 0 bytes Panel/modules/billing/docs/openttd/index.php | 380 - .../billing/docs/openttd/metadata.json | 7 - Panel/modules/billing/docs/oxide/index.php | 362 - .../modules/billing/docs/oxide/metadata.json | 7 - Panel/modules/billing/docs/pixark/icon.png | Bin 2233 -> 0 bytes Panel/modules/billing/docs/pixark/index.php | 439 - .../modules/billing/docs/pixark/metadata.json | 7 - Panel/modules/billing/docs/pvkii/icon.png | Bin 2575 -> 0 bytes Panel/modules/billing/docs/pvkii/index.php | 454 -- .../modules/billing/docs/pvkii/metadata.json | 7 - Panel/modules/billing/docs/quake3/icon.png | Bin 3670 -> 0 bytes Panel/modules/billing/docs/quake3/index.php | 401 - .../modules/billing/docs/quake3/metadata.json | 7 - Panel/modules/billing/docs/quake4/icon.png | Bin 3559 -> 0 bytes Panel/modules/billing/docs/quake4/index.php | 401 - .../modules/billing/docs/quake4/metadata.json | 7 - .../billing/docs/redorchestra2/icon.png | Bin 3328 -> 0 bytes .../billing/docs/redorchestra2/index.php | 402 - .../billing/docs/redorchestra2/metadata.json | 7 - .../billing/docs/reignofkings/icon.png | Bin 3087 -> 0 bytes .../billing/docs/reignofkings/index.php | 417 - .../billing/docs/reignofkings/metadata.json | 7 - Panel/modules/billing/docs/ricochet/icon.png | Bin 2670 -> 0 bytes Panel/modules/billing/docs/ricochet/index.php | 438 - .../billing/docs/ricochet/metadata.json | 7 - .../billing/docs/risingstorm2/icon.png | Bin 3089 -> 0 bytes .../billing/docs/risingstorm2/index.php | 402 - .../billing/docs/risingstorm2/metadata.json | 7 - Panel/modules/billing/docs/roadkill/icon.png | Bin 2519 -> 0 bytes Panel/modules/billing/docs/roadkill/index.php | 371 - .../billing/docs/roadkill/metadata.json | 7 - Panel/modules/billing/docs/rorserver/icon.png | Bin 2749 -> 0 bytes .../modules/billing/docs/rorserver/index.php | 389 - .../billing/docs/rorserver/metadata.json | 7 - Panel/modules/billing/docs/rust/icon.png | Bin 2242 -> 0 bytes Panel/modules/billing/docs/rust/index.php | 499 -- Panel/modules/billing/docs/rust/index_old.php | 67 - Panel/modules/billing/docs/rust/metadata.json | 7 - .../billing/docs/sanandreasmp/icon.png | Bin 3527 -> 0 bytes .../billing/docs/sanandreasmp/index.php | 362 - .../billing/docs/sanandreasmp/metadata.json | 7 - .../billing/docs/serioussamhdfe/icon.png | Bin 3518 -> 0 bytes .../billing/docs/serioussamhdfe/index.php | 419 - .../billing/docs/serioussamhdfe/metadata.json | 7 - .../billing/docs/serioussamhdse/icon.png | Bin 3518 -> 0 bytes .../billing/docs/serioussamhdse/index.php | 429 - .../billing/docs/serioussamhdse/metadata.json | 7 - Panel/modules/billing/docs/shoutcast/icon.png | Bin 3623 -> 0 bytes .../modules/billing/docs/shoutcast/index.php | 362 - .../billing/docs/shoutcast/metadata.json | 7 - .../billing/docs/shoutcast_bot/icon.png | Bin 3623 -> 0 bytes .../billing/docs/shoutcast_bot/index.php | 362 - .../billing/docs/shoutcast_bot/metadata.json | 7 - Panel/modules/billing/docs/sinusbot/icon.png | Bin 3448 -> 0 bytes Panel/modules/billing/docs/sinusbot/index.php | 411 - .../billing/docs/sinusbot/metadata.json | 7 - Panel/modules/billing/docs/smashball/icon.png | Bin 3182 -> 0 bytes .../modules/billing/docs/smashball/index.php | 390 - .../billing/docs/smashball/metadata.json | 7 - .../modules/billing/docs/smokinguns/icon.png | Bin 3526 -> 0 bytes .../modules/billing/docs/smokinguns/index.php | 374 - .../billing/docs/smokinguns/metadata.json | 7 - Panel/modules/billing/docs/sms/icon.png | Bin 3303 -> 0 bytes Panel/modules/billing/docs/sms/index.php | 406 - Panel/modules/billing/docs/sms/metadata.json | 7 - .../billing/docs/sniperelitev2/icon.png | Bin 3524 -> 0 bytes .../billing/docs/sniperelitev2/index.php | 411 - .../billing/docs/sniperelitev2/metadata.json | 7 - .../billing/docs/soldatserver/icon.png | Bin 3064 -> 0 bytes .../billing/docs/soldatserver/index.php | 362 - .../billing/docs/soldatserver/metadata.json | 7 - .../billing/docs/space_engineers/icon.jpg | Bin 142929 -> 0 bytes .../billing/docs/space_engineers/index.php | 411 - .../docs/space_engineers/metadata.json | 7 - Panel/modules/billing/docs/spigotmc/icon.png | Bin 3540 -> 0 bytes Panel/modules/billing/docs/spigotmc/index.php | 383 - .../billing/docs/spigotmc/metadata.json | 7 - Panel/modules/billing/docs/spunkybot/icon.jpg | Bin 21286 -> 0 bytes .../modules/billing/docs/spunkybot/index.php | 413 - .../billing/docs/spunkybot/metadata.json | 7 - Panel/modules/billing/docs/squad/icon.jpg | Bin 60703 -> 0 bytes Panel/modules/billing/docs/squad/index.php | 438 - .../modules/billing/docs/squad/metadata.json | 7 - Panel/modules/billing/docs/starbound/icon.jpg | Bin 25697 -> 0 bytes .../modules/billing/docs/starbound/index.php | 411 - .../billing/docs/starbound/metadata.json | 7 - .../modules/billing/docs/stationeers/icon.jpg | Bin 44708 -> 0 bytes .../billing/docs/stationeers/index.php | 457 -- .../billing/docs/stationeers/metadata.json | 7 - Panel/modules/billing/docs/synergy/icon.png | Bin 3268 -> 0 bytes Panel/modules/billing/docs/synergy/index.php | 430 - .../billing/docs/synergy/metadata.json | 7 - .../modules/billing/docs/teamspeak2/icon.png | Bin 2145 -> 0 bytes .../modules/billing/docs/teamspeak2/index.php | 362 - .../billing/docs/teamspeak2/metadata.json | 7 - .../modules/billing/docs/teamspeak3/icon.png | Bin 2178 -> 0 bytes .../modules/billing/docs/teamspeak3/index.php | 397 - .../billing/docs/teamspeak3/metadata.json | 7 - Panel/modules/billing/docs/terraria/icon.jpg | Bin 62177 -> 0 bytes Panel/modules/billing/docs/terraria/index.php | 430 - .../billing/docs/terraria/metadata.json | 7 - Panel/modules/billing/docs/tf2/icon.jpg | Bin 55689 -> 0 bytes Panel/modules/billing/docs/tf2/index.php | 541 -- Panel/modules/billing/docs/tf2/index_old.php | 68 - Panel/modules/billing/docs/tf2/metadata.json | 7 - Panel/modules/billing/docs/tfc/icon.png | Bin 1985 -> 0 bytes Panel/modules/billing/docs/tfc/index.php | 430 - Panel/modules/billing/docs/tfc/metadata.json | 7 - Panel/modules/billing/docs/theforest/icon.png | Bin 1752 -> 0 bytes .../modules/billing/docs/theforest/index.php | 478 -- .../billing/docs/theforest/metadata.json | 7 - .../billing/docs/trackmanianations/icon.png | Bin 1883 -> 0 bytes .../billing/docs/trackmanianations/index.php | 367 - .../docs/trackmanianations/metadata.json | 7 - .../billing/docs/trackmanianf/icon.png | Bin 1883 -> 0 bytes .../billing/docs/trackmanianf/index.php | 367 - .../billing/docs/trackmanianf/metadata.json | 7 - Panel/modules/billing/docs/unturned/icon.jpg | Bin 2087 -> 0 bytes Panel/modules/billing/docs/unturned/index.php | 413 - .../billing/docs/unturned/metadata.json | 7 - Panel/modules/billing/docs/urt/icon.jpg | Bin 26474 -> 0 bytes Panel/modules/billing/docs/urt/index.php | 411 - Panel/modules/billing/docs/urt/metadata.json | 7 - Panel/modules/billing/docs/ut2004/icon.png | Bin 2437 -> 0 bytes Panel/modules/billing/docs/ut2004/index.php | 374 - .../modules/billing/docs/ut2004/metadata.json | 7 - Panel/modules/billing/docs/ut3/icon.png | Bin 2437 -> 0 bytes Panel/modules/billing/docs/ut3/index.php | 503 -- Panel/modules/billing/docs/ut3/metadata.json | 7 - Panel/modules/billing/docs/ut99/icon.png | Bin 2437 -> 0 bytes Panel/modules/billing/docs/ut99/index.php | 381 - Panel/modules/billing/docs/ut99/metadata.json | 7 - Panel/modules/billing/docs/valheim/icon.jpg | Bin 50327 -> 0 bytes Panel/modules/billing/docs/valheim/index.php | 428 - .../billing/docs/valheim/metadata.json | 7 - Panel/modules/billing/docs/vbox/icon.png | Bin 3061 -> 0 bytes Panel/modules/billing/docs/vbox/index.php | 362 - Panel/modules/billing/docs/vbox/metadata.json | 7 - Panel/modules/billing/docs/ventrilo/icon.png | Bin 2781 -> 0 bytes Panel/modules/billing/docs/ventrilo/index.php | 362 - .../billing/docs/ventrilo/metadata.json | 7 - .../modules/billing/docs/vicecitymp/icon.png | Bin 3295 -> 0 bytes .../modules/billing/docs/vicecitymp/index.php | 362 - .../billing/docs/vicecitymp/metadata.json | 7 - Panel/modules/billing/docs/warsow/icon.png | Bin 3332 -> 0 bytes Panel/modules/billing/docs/warsow/index.php | 381 - .../modules/billing/docs/warsow/metadata.json | 7 - .../billing/docs/wolfrtcw_1-4/icon.png | Bin 3474 -> 0 bytes .../billing/docs/wolfrtcw_1-4/index.php | 362 - .../billing/docs/wolfrtcw_1-4/metadata.json | 7 - Panel/modules/billing/docs/wreckfest/icon.png | Bin 3425 -> 0 bytes .../modules/billing/docs/wreckfest/index.php | 411 - .../billing/docs/wreckfest/metadata.json | 7 - Panel/modules/billing/docs/wurmu/icon.jpg | Bin 47158 -> 0 bytes Panel/modules/billing/docs/wurmu/index.php | 504 -- .../modules/billing/docs/wurmu/metadata.json | 7 - Panel/modules/billing/docs/xml_notes.php | 147 - Panel/modules/billing/docs/xonotic/icon.png | Bin 3129 -> 0 bytes Panel/modules/billing/docs/xonotic/index.php | 380 - .../billing/docs/xonotic/metadata.json | 7 - Panel/modules/billing/docs/zps/icon.png | Bin 2845 -> 0 bytes Panel/modules/billing/docs/zps/index.php | 438 - Panel/modules/billing/docs/zps/metadata.json | 7 - .../billing/fix_invoices_table_columns.sql | 205 - Panel/modules/billing/forgot_password.php | 294 - Panel/modules/billing/images/banner.png | Bin 284540 -> 0 bytes .../billing/images/bf3_the_russian.jpg | Bin 159892 -> 0 bytes Panel/modules/billing/images/dark.jpg | Bin 659147 -> 0 bytes .../modules/billing/images/featured/7dtd.jpg | Bin 40986 -> 0 bytes .../modules/billing/images/featured/arkse.jpg | Bin 133659 -> 0 bytes .../featured/arma2_operation_arrowhead.jpg | Bin 52834 -> 0 bytes .../billing/images/featured/arma_3.jpg | Bin 33192 -> 0 bytes .../modules/billing/images/featured/cs_go.jpg | Bin 23981 -> 0 bytes .../modules/billing/images/featured/day_z.jpg | Bin 28105 -> 0 bytes .../billing/images/featured/dayz_epochmod.jpg | Bin 6556 -> 0 bytes .../billing/images/featured/dayz_mod.jpg | Bin 37224 -> 0 bytes .../billing/images/featured/eurotruck2.jpg | Bin 41153 -> 0 bytes .../images/featured/fistful_of_frags.jpg | Bin 52615 -> 0 bytes .../billing/images/featured/insurgency.jpg | Bin 34600 -> 0 bytes .../images/featured/insurgency_sandstorm.jpg | Bin 39090 -> 0 bytes .../billing/images/featured/minecraft.jpg | Bin 53010 -> 0 bytes Panel/modules/billing/images/games/7dtd.jpg | Bin 40986 -> 0 bytes Panel/modules/billing/images/games/arkse.jpg | Bin 133659 -> 0 bytes Panel/modules/billing/images/games/arma2.jpg | Bin 40955 -> 0 bytes .../games/arma2_operation_arrowhead.jpg | Bin 52834 -> 0 bytes Panel/modules/billing/images/games/arma_3.jpg | Bin 33192 -> 0 bytes Panel/modules/billing/images/games/asseto.jpg | Bin 50562 -> 0 bytes .../modules/billing/images/games/avorion.jpg | Bin 32342 -> 0 bytes .../billing/images/games/brainbread_2.jpg | Bin 175402 -> 0 bytes .../modules/billing/images/games/chivalry.jpg | Bin 115345 -> 0 bytes .../modules/billing/images/games/citadel.jpg | Bin 59357 -> 0 bytes .../billing/images/games/colonysurvival.jpg | Bin 47610 -> 0 bytes .../billing/images/games/conanexiles.jpg | Bin 37168 -> 0 bytes Panel/modules/billing/images/games/cs_go.jpg | Bin 23981 -> 0 bytes .../modules/billing/images/games/cstrike.jpg | Bin 28138 -> 0 bytes .../billing/images/games/cstrikesource.jpg | Bin 18125 -> 0 bytes .../images/games/day_of_defeat_source.jpg | Bin 25387 -> 0 bytes Panel/modules/billing/images/games/day_z.jpg | Bin 28105 -> 0 bytes .../billing/images/games/dayz_epochmod.jpg | Bin 6556 -> 0 bytes .../modules/billing/images/games/dayz_mod.jpg | Bin 37224 -> 0 bytes .../images/games/deathmatch_classic.jpg | Bin 32228 -> 0 bytes Panel/modules/billing/images/games/dst.jpg | Bin 56997 -> 0 bytes Panel/modules/billing/images/games/eco.jpg | Bin 44396 -> 0 bytes .../billing/images/games/eurotruck2.jpg | Bin 41153 -> 0 bytes .../billing/images/games/fistful_of_frags.jpg | Bin 52615 -> 0 bytes .../billing/images/games/garrys_mod.jpg | Bin 20463 -> 0 bytes .../images/games/half-life2_deathmatch.jpg | Bin 25477 -> 0 bytes Panel/modules/billing/images/games/harsh.jpg | Bin 39125 -> 0 bytes .../billing/images/games/hurt_world.jpg | Bin 35501 -> 0 bytes .../billing/images/games/insurgency.jpg | Bin 34600 -> 0 bytes .../images/games/insurgency_sandstorm.jpg | Bin 39090 -> 0 bytes .../billing/images/games/killing_floor.jpg | Bin 28352 -> 0 bytes .../billing/images/games/killing_floor_2.jpg | Bin 57305 -> 0 bytes .../billing/images/games/left_4_dead.jpg | Bin 57336 -> 0 bytes .../billing/images/games/left_4_dead_2.jpg | Bin 39230 -> 0 bytes .../billing/images/games/minecraft.jpg | Bin 53010 -> 0 bytes .../images/games/miscreated_server.jpg | Bin 35617 -> 0 bytes .../modules/billing/images/games/mordhau.jpg | Bin 51057 -> 0 bytes .../billing/images/games/nomoreroominhell.jpg | Bin 41993 -> 0 bytes Panel/modules/billing/images/games/ootow.jpg | Bin 43686 -> 0 bytes .../billing/images/games/rust_header.jpg | Bin 15212 -> 0 bytes Panel/modules/billing/images/games/scp.jpg | Bin 29895 -> 0 bytes Panel/modules/billing/images/games/squad.jpg | Bin 60703 -> 0 bytes .../billing/images/games/starbound.jpg | Bin 25697 -> 0 bytes .../billing/images/games/stationeers.jpg | Bin 44708 -> 0 bytes .../billing/images/games/team_fortress_2.jpg | Bin 55689 -> 0 bytes .../modules/billing/images/games/terraria.jpg | Bin 62177 -> 0 bytes Panel/modules/billing/images/games/urt.jpg | Bin 26474 -> 0 bytes .../modules/billing/images/games/valheim.jpg | Bin 50327 -> 0 bytes Panel/modules/billing/images/games/wurmu.jpg | Bin 47158 -> 0 bytes Panel/modules/billing/images/logo-sm.png | Bin 20171 -> 0 bytes Panel/modules/billing/images/logo.jpg | Bin 54815 -> 0 bytes Panel/modules/billing/images/logo.png | Bin 302940 -> 0 bytes Panel/modules/billing/includes/README.md | 28 - Panel/modules/billing/includes/admin_auth.php | 51 - .../modules/billing/includes/cart_helper.php | 22 - .../billing/includes/config.example.php | 59 - .../billing/includes/config_loader.php | 392 - Panel/modules/billing/includes/footer.php | 25 - Panel/modules/billing/includes/log.php | 33 - .../billing/includes/login_required.php | 9 - Panel/modules/billing/includes/menu.php | 159 - .../modules/billing/includes/panel_bridge.php | 126 - .../billing/includes/payment_processor.php | 223 - .../billing/includes/session_bridge.php | 32 - Panel/modules/billing/includes/top.php | 6 - Panel/modules/billing/index.php | 98 - Panel/modules/billing/invoices.php | 66 - Panel/modules/billing/logfile.txt | 13 - Panel/modules/billing/login.php | 399 - Panel/modules/billing/logout.php | 30 - .../modules/billing/migration_to_invoices.sql | 176 - Panel/modules/billing/module.php | 425 - Panel/modules/billing/my_account.php | 398 - Panel/modules/billing/my_orders_panel.php | 89 - Panel/modules/billing/my_servers.php | 161 - Panel/modules/billing/navigation.xml | 23 - .../normalize_billing_order_status.sql | 64 - Panel/modules/billing/order.php | 439 - Panel/modules/billing/payment_cancel.php | 51 - Panel/modules/billing/payment_success.php | 360 - Panel/modules/billing/paypal/webhook.php | 752 -- Panel/modules/billing/privacy.php | 49 - Panel/modules/billing/register.php | 68 - Panel/modules/billing/renew_server.php | 280 - Panel/modules/billing/reset_password.php | 304 - Panel/modules/billing/return.php | 114 - Panel/modules/billing/server_status.php | 173 - Panel/modules/billing/serverlist.php | 210 - Panel/modules/billing/site_config.example.php | 14 - Panel/modules/billing/site_config.php | 9 - .../sql/002_billing_checkout_fixes.sql | 98 - .../sql/normalize_billing_order_status.sql | 37 - Panel/modules/billing/test_db_connection.php | 158 - Panel/modules/billing/test_integration.php | 141 - Panel/modules/billing/timestamp.txt | 1 - Panel/modules/billing/tools/check_db_user.php | 31 - .../billing/tools/check_invoices_redirect.php | 16 - .../billing/tools/check_logout_redirect.php | 21 - .../billing/tools/debug_invoices_redirect.php | 40 - .../billing/tools/simulate_webhook.php | 39 - Panel/modules/billing/tos.php | 57 - .../billing/update_metadata_complete.ps1 | 46 - Panel/modules/billing/webhook.php | 312 - Panel/modules/dashboard/dashboard.php | 4 + Panel/modules/reseller/account_details.php | 185 - Panel/modules/reseller/accounts.php | 187 - Panel/modules/reseller/add_to_cart.php | 102 - Panel/modules/reseller/assign_server.php | 592 -- Panel/modules/reseller/bill.php | 146 - Panel/modules/reseller/cart.css | 50 - Panel/modules/reseller/cart.php | 431 - Panel/modules/reseller/cron-shop.php | 137 - Panel/modules/reseller/ipn_errors.log | 0 Panel/modules/reseller/ipnlistener.php | 309 - Panel/modules/reseller/module.php | 102 - Panel/modules/reseller/navigation.xml | 16 - Panel/modules/reseller/pack_image.png | Bin 334419 -> 0 bytes Panel/modules/reseller/paid-ipn.php | 166 - Panel/modules/reseller/paid.php | 18 - Panel/modules/reseller/paypal.class.php | 277 - Panel/modules/reseller/paypal.php | 78 - Panel/modules/reseller/rs_accounts.css | 50 - Panel/modules/reseller/rs_assign_server.css | 30 - Panel/modules/reseller/rs_packs_shop.css | 30 - Panel/modules/reseller/services.php | 395 - Panel/modules/reseller/settings.php | 83 - Panel/modules/reseller/shop.php | 234 - .../functions.php | 219 - .../game_configs/361580_Windows.xml | 24 - .../game_configs/376030_Linux.xml | 59 - .../game_configs/376030_Windows.xml | 59 - .../game_configs/4020_Linux.xml | 104 - .../game_configs/443030_Windows.xml | 101 - .../game_configs/533830_Linux.xml | 24 - .../game_configs/533830_Windows.xml | 24 - .../game_configs/740_Linux.xml | 67 - .../main.php | 332 - .../module.php | 30 - .../monitor_buttons.php | 43 - .../navigation.xml | 5 - .../steam_workshop.css | 17 - .../uninstall.php | 178 - .../workshop_admin.php | 255 - Panel/modules/tickets/downloadAttachment.php | 58 - Panel/modules/tickets/include/Attachments.php | 180 - .../tickets/include/TicketSettings.php | 67 - .../modules/tickets/include/array_column.php | 114 - Panel/modules/tickets/include/functions.php | 209 - Panel/modules/tickets/include/mime.types.php | 7229 ----------------- Panel/modules/tickets/include/ticket.php | 282 - Panel/modules/tickets/js/helpers.js | 21 - Panel/modules/tickets/js/javascript_vars.php | 26 - Panel/modules/tickets/js/rating.js | 52 - Panel/modules/tickets/js/ticket.js | 118 - Panel/modules/tickets/js/ticket_settings.js | 31 - Panel/modules/tickets/module.php | 109 - Panel/modules/tickets/navigation.xml | 9 - Panel/modules/tickets/notificationCount.php | 24 - Panel/modules/tickets/rating.php | 56 - Panel/modules/tickets/submitTicket.php | 154 - Panel/modules/tickets/submitticket.css | 45 - Panel/modules/tickets/supportTickets.php | 64 - Panel/modules/tickets/ticketSettings.php | 85 - Panel/modules/tickets/ticket_settings.css | 21 - Panel/modules/tickets/tickets.css | 22 - Panel/modules/tickets/uploads/.htaccess | 1 - .../uploads/0fba4a38e62886201af21ceb.png | Bin 817045 -> 0 bytes .../uploads/2218ec951fb162d80c6fdf39.png | Bin 675694 -> 0 bytes .../uploads/38cb80de51c3ea2744f6b837.png | Bin 50802 -> 0 bytes .../uploads/3926ed7bd92097bdfae4b348.png | Bin 123849 -> 0 bytes Panel/modules/tickets/uploads/readme.txt | 5 - Panel/modules/tickets/viewTicket.php | 168 - Panel/modules/tickets/viewticket.css | 270 - Panel/modules/website/README.md | 36 +- Panel/modules/website/account.php | 22 + Panel/modules/website/assets/css/site.css | 33 + .../modules/website/config/config.example.php | 1 + Panel/modules/website/includes/bootstrap.php | 303 + Panel/modules/website/includes/footer.php | 17 +- Panel/modules/website/includes/navigation.php | 2 +- Panel/modules/website/login.php | 47 +- Panel/modules/website/logout.php | 14 + Panel/modules/website/order.php | 44 + Panel/modules/website/pages/account.php | 51 + Panel/modules/website/pages/game_servers.php | 2 +- Panel/modules/website/pages/home.php | 6 +- Panel/modules/website/pages/login.php | 27 + Panel/modules/website/pages/order.php | 67 + Panel/modules/website/pages/pricing.php | 2 +- Panel/modules/website/sso.php | 81 + Panel/sso.php | 141 + docs/architecture/API_REFERENCE.md | 26 +- docs/architecture/MODULE_DEPENDENCIES.md | 4 +- docs/features/USER_API.md | 49 +- docs/modules/billing.md | 9 +- docs/modules/website.md | 46 + 755 files changed, 1205 insertions(+), 106715 deletions(-) create mode 100644 Panel/includes/sso.php delete mode 100644 Panel/modules/billing/BILLING_FIX_SUMMARY.md delete mode 100644 Panel/modules/billing/COLUMN_RENAME_SUMMARY.md delete mode 100644 Panel/modules/billing/COUPON_SYSTEM.md delete mode 100644 Panel/modules/billing/FIXES_APPLIED.md delete mode 100644 Panel/modules/billing/GAME_DOCS_TODO_REFERENCE.md delete mode 100644 Panel/modules/billing/INVOICE_FIRST_FLOW.md delete mode 100644 Panel/modules/billing/INVOICE_SYSTEM.md delete mode 100644 Panel/modules/billing/LOGGING_CHANGES_SUMMARY.md delete mode 100644 Panel/modules/billing/MIGRATION_SUMMARY.md delete mode 100644 Panel/modules/billing/PANEL_INTEGRATION.md delete mode 100644 Panel/modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md delete mode 100644 Panel/modules/billing/PAYPAL_DEBUGGING_GUIDE.md delete mode 100644 Panel/modules/billing/PHASE1_COMPLETE_SUMMARY.md delete mode 100644 Panel/modules/billing/QUICK_DEBUG_REFERENCE.md delete mode 100644 Panel/modules/billing/QUICK_START.md delete mode 100644 Panel/modules/billing/README.md delete mode 100644 Panel/modules/billing/README_COUPON_UPDATE.md delete mode 100644 Panel/modules/billing/RECENT_FIXES_SUMMARY.md delete mode 100644 Panel/modules/billing/STATUS_REPORT.md delete mode 100644 Panel/modules/billing/TESTING_CHECKLIST.md delete mode 100644 Panel/modules/billing/_archived/CONFIGURATION.md delete mode 100644 Panel/modules/billing/_archived/FEATURES.md delete mode 100644 Panel/modules/billing/_archived/IMPLEMENTATION_SUMMARY.md delete mode 100644 Panel/modules/billing/_archived/README_LOGIN.md delete mode 100644 Panel/modules/billing/_archived/VISUAL_GUIDE.md delete mode 100644 Panel/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt delete mode 100644 Panel/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/ai.php delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json delete mode 100644 Panel/modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json delete mode 100644 Panel/modules/billing/add_override_price_column.sql delete mode 100644 Panel/modules/billing/add_paypal_data_column.sql delete mode 100644 Panel/modules/billing/add_remote_server_enabled_column.sql delete mode 100644 Panel/modules/billing/add_service_id_column.sql delete mode 100644 Panel/modules/billing/add_to_cart.php delete mode 100644 Panel/modules/billing/admin.php delete mode 100644 Panel/modules/billing/admin_config.php delete mode 100644 Panel/modules/billing/admin_coupons.php delete mode 100644 Panel/modules/billing/admin_invoices.php delete mode 100644 Panel/modules/billing/admin_orders.php delete mode 100644 Panel/modules/billing/admin_payments.php delete mode 100644 Panel/modules/billing/admin_xml_editor.php delete mode 100644 Panel/modules/billing/adminserverlist.php delete mode 100644 Panel/modules/billing/ai.php delete mode 100644 Panel/modules/billing/api/capture_order.php delete mode 100644 Panel/modules/billing/api/create_order.php delete mode 100644 Panel/modules/billing/api/log_error.php delete mode 100644 Panel/modules/billing/billing_bootstrap.php delete mode 100644 Panel/modules/billing/bootstrap.php delete mode 100644 Panel/modules/billing/cart.php delete mode 100644 Panel/modules/billing/check_table.php delete mode 100644 Panel/modules/billing/checkout_free.php delete mode 100644 Panel/modules/billing/classes/BillingRepository.php delete mode 100644 Panel/modules/billing/classes/BillingService.php delete mode 100644 Panel/modules/billing/classes/GatewayFactory.php delete mode 100644 Panel/modules/billing/classes/ManualGateway.php delete mode 100644 Panel/modules/billing/classes/PayPalGateway.php delete mode 100644 Panel/modules/billing/classes/PaymentGatewayInterface.php delete mode 100644 Panel/modules/billing/classes/StripeGateway.php delete mode 100644 Panel/modules/billing/cleanupDB.sh delete mode 100644 Panel/modules/billing/create_coupons_table.sql delete mode 100644 Panel/modules/billing/create_invoices_table.sql delete mode 100644 Panel/modules/billing/create_servers.php delete mode 100644 Panel/modules/billing/cron-shop.php delete mode 100644 Panel/modules/billing/css/header.css delete mode 100644 Panel/modules/billing/diag_remote.php delete mode 100644 Panel/modules/billing/docs.php delete mode 100644 Panel/modules/billing/docs/7daystodie/icon.jpg delete mode 100644 Panel/modules/billing/docs/7daystodie/index.php delete mode 100644 Panel/modules/billing/docs/7daystodie/metadata.json delete mode 100644 Panel/modules/billing/docs/COMPREHENSIVE_DOCUMENTATION_UPDATE.md delete mode 100644 Panel/modules/billing/docs/DOCUMENTATION_ENHANCEMENT_SUMMARY.md delete mode 100644 Panel/modules/billing/docs/DOCUMENTATION_EXPANSION_PLAN.md delete mode 100644 Panel/modules/billing/docs/GAME_SERVER_LIST.md delete mode 100644 Panel/modules/billing/docs/GENERATION_README.md delete mode 100644 Panel/modules/billing/docs/Game Server Hosting Reference (Multiplayer PC Games).pdf delete mode 100644 Panel/modules/billing/docs/IMPLEMENTATION_SUMMARY.md delete mode 100644 Panel/modules/billing/docs/README.md delete mode 100644 Panel/modules/billing/docs/XML-Notes.md delete mode 100644 Panel/modules/billing/docs/aliensvspredator/icon.png delete mode 100644 Panel/modules/billing/docs/aliensvspredator/index.php delete mode 100644 Panel/modules/billing/docs/aliensvspredator/metadata.json delete mode 100644 Panel/modules/billing/docs/amxmodx/index.php delete mode 100644 Panel/modules/billing/docs/amxmodx/metadata.json delete mode 100644 Panel/modules/billing/docs/aoc/icon.png delete mode 100644 Panel/modules/billing/docs/aoc/index.php delete mode 100644 Panel/modules/billing/docs/aoc/metadata.json delete mode 100644 Panel/modules/billing/docs/arkse/icon.jpg delete mode 100644 Panel/modules/billing/docs/arkse/index.php delete mode 100644 Panel/modules/billing/docs/arkse/metadata.json delete mode 100644 Panel/modules/billing/docs/arma-reforger/icon.png delete mode 100644 Panel/modules/billing/docs/arma-reforger/index.php delete mode 100644 Panel/modules/billing/docs/arma-reforger/metadata.json delete mode 100644 Panel/modules/billing/docs/arma2co/icon.png delete mode 100644 Panel/modules/billing/docs/arma2co/index.php delete mode 100644 Panel/modules/billing/docs/arma2co/metadata.json delete mode 100644 Panel/modules/billing/docs/arma2oa/icon.jpg delete mode 100644 Panel/modules/billing/docs/arma2oa/index.php delete mode 100644 Panel/modules/billing/docs/arma2oa/metadata.json delete mode 100644 Panel/modules/billing/docs/arma3/icon.jpg delete mode 100644 Panel/modules/billing/docs/arma3/index.php delete mode 100644 Panel/modules/billing/docs/arma3/metadata.json delete mode 100644 Panel/modules/billing/docs/assettocorsa/icon.png delete mode 100644 Panel/modules/billing/docs/assettocorsa/index.php delete mode 100644 Panel/modules/billing/docs/assettocorsa/metadata.json delete mode 100644 Panel/modules/billing/docs/atlas/icon.png delete mode 100644 Panel/modules/billing/docs/atlas/index.php delete mode 100644 Panel/modules/billing/docs/atlas/metadata.json delete mode 100644 Panel/modules/billing/docs/avorion/icon.jpg delete mode 100644 Panel/modules/billing/docs/avorion/index.php delete mode 100644 Panel/modules/billing/docs/avorion/metadata.json delete mode 100644 Panel/modules/billing/docs/b3/index.php delete mode 100644 Panel/modules/billing/docs/b3/metadata.json delete mode 100644 Panel/modules/billing/docs/bec/icon.jpg delete mode 100644 Panel/modules/billing/docs/bec/index.php delete mode 100644 Panel/modules/billing/docs/bec/metadata.json delete mode 100644 Panel/modules/billing/docs/bf2/icon.png delete mode 100644 Panel/modules/billing/docs/bf2/index.php delete mode 100644 Panel/modules/billing/docs/bf2/metadata.json delete mode 100644 Panel/modules/billing/docs/bfbc2/icon.png delete mode 100644 Panel/modules/billing/docs/bfbc2/index.php delete mode 100644 Panel/modules/billing/docs/bfbc2/metadata.json delete mode 100644 Panel/modules/billing/docs/bloodfrontier/icon.png delete mode 100644 Panel/modules/billing/docs/bloodfrontier/index.php delete mode 100644 Panel/modules/billing/docs/bloodfrontier/metadata.json delete mode 100644 Panel/modules/billing/docs/brainbread2/icon.jpg delete mode 100644 Panel/modules/billing/docs/brainbread2/index.php delete mode 100644 Panel/modules/billing/docs/brainbread2/metadata.json delete mode 100644 Panel/modules/billing/docs/callofduty/icon.png delete mode 100644 Panel/modules/billing/docs/callofduty/index.php delete mode 100644 Panel/modules/billing/docs/callofduty/metadata.json delete mode 100644 Panel/modules/billing/docs/callofduty2/icon.png delete mode 100644 Panel/modules/billing/docs/callofduty2/index.php delete mode 100644 Panel/modules/billing/docs/callofduty2/metadata.json delete mode 100644 Panel/modules/billing/docs/callofduty4mw/icon.png delete mode 100644 Panel/modules/billing/docs/callofduty4mw/index.php delete mode 100644 Panel/modules/billing/docs/callofduty4mw/metadata.json delete mode 100644 Panel/modules/billing/docs/callofdutymw2/icon.png delete mode 100644 Panel/modules/billing/docs/callofdutymw2/index.php delete mode 100644 Panel/modules/billing/docs/callofdutymw2/metadata.json delete mode 100644 Panel/modules/billing/docs/callofdutymw3/icon.png delete mode 100644 Panel/modules/billing/docs/callofdutymw3/index.php delete mode 100644 Panel/modules/billing/docs/callofdutymw3/metadata.json delete mode 100644 Panel/modules/billing/docs/callofdutyuo/icon.png delete mode 100644 Panel/modules/billing/docs/callofdutyuo/index.php delete mode 100644 Panel/modules/billing/docs/callofdutyuo/metadata.json delete mode 100644 Panel/modules/billing/docs/callofdutywaw/icon.png delete mode 100644 Panel/modules/billing/docs/callofdutywaw/index.php delete mode 100644 Panel/modules/billing/docs/callofdutywaw/metadata.json delete mode 100644 Panel/modules/billing/docs/citadelfwf/icon.png delete mode 100644 Panel/modules/billing/docs/citadelfwf/index.php delete mode 100644 Panel/modules/billing/docs/citadelfwf/metadata.json delete mode 100644 Panel/modules/billing/docs/cod_blackops/icon.png delete mode 100644 Panel/modules/billing/docs/cod_blackops/index.php delete mode 100644 Panel/modules/billing/docs/cod_blackops/metadata.json delete mode 100644 Panel/modules/billing/docs/colonysurvival/icon.jpg delete mode 100644 Panel/modules/billing/docs/colonysurvival/index.php delete mode 100644 Panel/modules/billing/docs/colonysurvival/metadata.json delete mode 100644 Panel/modules/billing/docs/common-issues/icon.png delete mode 100644 Panel/modules/billing/docs/common-issues/index.php delete mode 100644 Panel/modules/billing/docs/common-issues/metadata.json delete mode 100644 Panel/modules/billing/docs/conanexiles/icon.jpg delete mode 100644 Panel/modules/billing/docs/conanexiles/index.php delete mode 100644 Panel/modules/billing/docs/conanexiles/metadata.json delete mode 100644 Panel/modules/billing/docs/cs2d/icon.png delete mode 100644 Panel/modules/billing/docs/cs2d/index.php delete mode 100644 Panel/modules/billing/docs/cs2d/metadata.json delete mode 100644 Panel/modules/billing/docs/csgo/icon.png delete mode 100644 Panel/modules/billing/docs/csgo/index.php delete mode 100644 Panel/modules/billing/docs/csgo/index_old.php delete mode 100644 Panel/modules/billing/docs/csgo/metadata.json delete mode 100644 Panel/modules/billing/docs/cspromod/icon.png delete mode 100644 Panel/modules/billing/docs/cspromod/index.php delete mode 100644 Panel/modules/billing/docs/cspromod/metadata.json delete mode 100644 Panel/modules/billing/docs/css/icon.jpg delete mode 100644 Panel/modules/billing/docs/css/index.php delete mode 100644 Panel/modules/billing/docs/css/metadata.json delete mode 100644 Panel/modules/billing/docs/cstrike/icon.jpg delete mode 100644 Panel/modules/billing/docs/cstrike/index.php delete mode 100644 Panel/modules/billing/docs/cstrike/metadata.json delete mode 100644 Panel/modules/billing/docs/czero/icon.png delete mode 100644 Panel/modules/billing/docs/czero/index.php delete mode 100644 Panel/modules/billing/docs/czero/metadata.json delete mode 100644 Panel/modules/billing/docs/dayz/icon.png delete mode 100644 Panel/modules/billing/docs/dayz/index.php delete mode 100644 Panel/modules/billing/docs/dayz/metadata.json delete mode 100644 Panel/modules/billing/docs/dayzmod/icon.jpg delete mode 100644 Panel/modules/billing/docs/dayzmod/index.php delete mode 100644 Panel/modules/billing/docs/dayzmod/metadata.json delete mode 100644 Panel/modules/billing/docs/dmc/icon.jpg delete mode 100644 Panel/modules/billing/docs/dmc/index.php delete mode 100644 Panel/modules/billing/docs/dmc/metadata.json delete mode 100644 Panel/modules/billing/docs/dod/icon.png delete mode 100644 Panel/modules/billing/docs/dod/index.php delete mode 100644 Panel/modules/billing/docs/dod/metadata.json delete mode 100644 Panel/modules/billing/docs/dods/icon.jpg delete mode 100644 Panel/modules/billing/docs/dods/index.php delete mode 100644 Panel/modules/billing/docs/dods/metadata.json delete mode 100644 Panel/modules/billing/docs/doi/icon.jpg delete mode 100644 Panel/modules/billing/docs/doi/index.php delete mode 100644 Panel/modules/billing/docs/doi/metadata.json delete mode 100644 Panel/modules/billing/docs/dontstarvetogether/icon.png delete mode 100644 Panel/modules/billing/docs/dontstarvetogether/index.php delete mode 100644 Panel/modules/billing/docs/dontstarvetogether/metadata.json delete mode 100644 Panel/modules/billing/docs/dystopia/icon.png delete mode 100644 Panel/modules/billing/docs/dystopia/index.php delete mode 100644 Panel/modules/billing/docs/dystopia/metadata.json delete mode 100644 Panel/modules/billing/docs/eco/icon.jpg delete mode 100644 Panel/modules/billing/docs/eco/index.php delete mode 100644 Panel/modules/billing/docs/eco/metadata.json delete mode 100644 Panel/modules/billing/docs/empyriongs/icon.png delete mode 100644 Panel/modules/billing/docs/empyriongs/index.php delete mode 100644 Panel/modules/billing/docs/empyriongs/metadata.json delete mode 100644 Panel/modules/billing/docs/enemyterritory/icon.png delete mode 100644 Panel/modules/billing/docs/enemyterritory/index.php delete mode 100644 Panel/modules/billing/docs/enemyterritory/metadata.json delete mode 100644 Panel/modules/billing/docs/epochmod/icon.png delete mode 100644 Panel/modules/billing/docs/epochmod/index.php delete mode 100644 Panel/modules/billing/docs/epochmod/metadata.json delete mode 100644 Panel/modules/billing/docs/esmod/icon.png delete mode 100644 Panel/modules/billing/docs/esmod/index.php delete mode 100644 Panel/modules/billing/docs/esmod/metadata.json delete mode 100644 Panel/modules/billing/docs/ets2/icon.png delete mode 100644 Panel/modules/billing/docs/ets2/index.php delete mode 100644 Panel/modules/billing/docs/ets2/metadata.json delete mode 100644 Panel/modules/billing/docs/factorio/icon.jpg delete mode 100644 Panel/modules/billing/docs/factorio/index.php delete mode 100644 Panel/modules/billing/docs/factorio/metadata.json delete mode 100644 Panel/modules/billing/docs/feedthebeast/icon.png delete mode 100644 Panel/modules/billing/docs/feedthebeast/index.php delete mode 100644 Panel/modules/billing/docs/feedthebeast/metadata.json delete mode 100644 Panel/modules/billing/docs/fgms/icon.png delete mode 100644 Panel/modules/billing/docs/fgms/index.php delete mode 100644 Panel/modules/billing/docs/fgms/metadata.json delete mode 100644 Panel/modules/billing/docs/fivem/icon.png delete mode 100644 Panel/modules/billing/docs/fivem/index.php delete mode 100644 Panel/modules/billing/docs/fivem/metadata.json delete mode 100644 Panel/modules/billing/docs/fof/icon.jpg delete mode 100644 Panel/modules/billing/docs/fof/index.php delete mode 100644 Panel/modules/billing/docs/fof/metadata.json delete mode 100644 Panel/modules/billing/docs/freecol/icon.png delete mode 100644 Panel/modules/billing/docs/freecol/index.php delete mode 100644 Panel/modules/billing/docs/freecol/metadata.json delete mode 100644 Panel/modules/billing/docs/gameserver_catalog_all_sources.yaml delete mode 100644 Panel/modules/billing/docs/gameserver_catalog_lgsm_full.yaml delete mode 100644 Panel/modules/billing/docs/gameserver_knowledgepack_v2.yaml delete mode 100644 Panel/modules/billing/docs/garrysmod/icon.jpg delete mode 100644 Panel/modules/billing/docs/garrysmod/index.php delete mode 100644 Panel/modules/billing/docs/garrysmod/metadata.json delete mode 100644 Panel/modules/billing/docs/gearbox/icon.png delete mode 100644 Panel/modules/billing/docs/gearbox/index.php delete mode 100644 Panel/modules/billing/docs/gearbox/metadata.json delete mode 100644 Panel/modules/billing/docs/getting-started/icon.png delete mode 100644 Panel/modules/billing/docs/getting-started/index.php delete mode 100644 Panel/modules/billing/docs/getting-started/metadata.json delete mode 100644 Panel/modules/billing/docs/halo_ce/icon.png delete mode 100644 Panel/modules/billing/docs/halo_ce/index.php delete mode 100644 Panel/modules/billing/docs/halo_ce/metadata.json delete mode 100644 Panel/modules/billing/docs/harsh/icon.jpg delete mode 100644 Panel/modules/billing/docs/harsh/index.php delete mode 100644 Panel/modules/billing/docs/harsh/metadata.json delete mode 100644 Panel/modules/billing/docs/hidden_source/icon.png delete mode 100644 Panel/modules/billing/docs/hidden_source/index.php delete mode 100644 Panel/modules/billing/docs/hidden_source/metadata.json delete mode 100644 Panel/modules/billing/docs/hl2d/icon.png delete mode 100644 Panel/modules/billing/docs/hl2d/index.php delete mode 100644 Panel/modules/billing/docs/hl2d/metadata.json delete mode 100644 Panel/modules/billing/docs/hldm/icon.png delete mode 100644 Panel/modules/billing/docs/hldm/index.php delete mode 100644 Panel/modules/billing/docs/hldm/metadata.json delete mode 100644 Panel/modules/billing/docs/hltv/icon.png delete mode 100644 Panel/modules/billing/docs/hltv/index.php delete mode 100644 Panel/modules/billing/docs/hltv/metadata.json delete mode 100644 Panel/modules/billing/docs/homefront/icon.png delete mode 100644 Panel/modules/billing/docs/homefront/index.php delete mode 100644 Panel/modules/billing/docs/homefront/metadata.json delete mode 100644 Panel/modules/billing/docs/hurtworld/icon.jpg delete mode 100644 Panel/modules/billing/docs/hurtworld/index.php delete mode 100644 Panel/modules/billing/docs/hurtworld/metadata.json delete mode 100644 Panel/modules/billing/docs/il2/icon.png delete mode 100644 Panel/modules/billing/docs/il2/index.php delete mode 100644 Panel/modules/billing/docs/il2/metadata.json delete mode 100644 Panel/modules/billing/docs/ins/icon.jpg delete mode 100644 Panel/modules/billing/docs/ins/index.php delete mode 100644 Panel/modules/billing/docs/ins/metadata.json delete mode 100644 Panel/modules/billing/docs/insurgencymic/icon.png delete mode 100644 Panel/modules/billing/docs/insurgencymic/index.php delete mode 100644 Panel/modules/billing/docs/insurgencymic/metadata.json delete mode 100644 Panel/modules/billing/docs/insurgencysandstorm/icon.png delete mode 100644 Panel/modules/billing/docs/insurgencysandstorm/index.php delete mode 100644 Panel/modules/billing/docs/insurgencysandstorm/metadata.json delete mode 100644 Panel/modules/billing/docs/ivmp/icon.png delete mode 100644 Panel/modules/billing/docs/ivmp/index.php delete mode 100644 Panel/modules/billing/docs/ivmp/metadata.json delete mode 100644 Panel/modules/billing/docs/jcmp/icon.png delete mode 100644 Panel/modules/billing/docs/jcmp/index.php delete mode 100644 Panel/modules/billing/docs/jcmp/metadata.json delete mode 100644 Panel/modules/billing/docs/jediknight2/icon.png delete mode 100644 Panel/modules/billing/docs/jediknight2/index.php delete mode 100644 Panel/modules/billing/docs/jediknight2/metadata.json delete mode 100644 Panel/modules/billing/docs/jediknightja/icon.png delete mode 100644 Panel/modules/billing/docs/jediknightja/index.php delete mode 100644 Panel/modules/billing/docs/jediknightja/metadata.json delete mode 100644 Panel/modules/billing/docs/killingfloor/icon.jpg delete mode 100644 Panel/modules/billing/docs/killingfloor/index.php delete mode 100644 Panel/modules/billing/docs/killingfloor/metadata.json delete mode 100644 Panel/modules/billing/docs/killingfloor2/icon.jpg delete mode 100644 Panel/modules/billing/docs/killingfloor2/index.php delete mode 100644 Panel/modules/billing/docs/killingfloor2/metadata.json delete mode 100644 Panel/modules/billing/docs/left4dead/icon.jpg delete mode 100644 Panel/modules/billing/docs/left4dead/index.php delete mode 100644 Panel/modules/billing/docs/left4dead/metadata.json delete mode 100644 Panel/modules/billing/docs/left4dead2/icon.jpg delete mode 100644 Panel/modules/billing/docs/left4dead2/index.php delete mode 100644 Panel/modules/billing/docs/left4dead2/metadata.json delete mode 100644 Panel/modules/billing/docs/lifeisfeudal/icon.png delete mode 100644 Panel/modules/billing/docs/lifeisfeudal/index.php delete mode 100644 Panel/modules/billing/docs/lifeisfeudal/metadata.json delete mode 100644 Panel/modules/billing/docs/mab_warband/icon.png delete mode 100644 Panel/modules/billing/docs/mab_warband/index.php delete mode 100644 Panel/modules/billing/docs/mab_warband/metadata.json delete mode 100644 Panel/modules/billing/docs/mafia2online/icon.png delete mode 100644 Panel/modules/billing/docs/mafia2online/index.php delete mode 100644 Panel/modules/billing/docs/mafia2online/metadata.json delete mode 100644 Panel/modules/billing/docs/metamodsource/index.php delete mode 100644 Panel/modules/billing/docs/metamodsource/metadata.json delete mode 100644 Panel/modules/billing/docs/minecraft/icon.png delete mode 100644 Panel/modules/billing/docs/minecraft/index.php delete mode 100644 Panel/modules/billing/docs/minecraft/index_old.php delete mode 100644 Panel/modules/billing/docs/minecraft/metadata.json delete mode 100644 Panel/modules/billing/docs/miscreated/icon.jpg delete mode 100644 Panel/modules/billing/docs/miscreated/index.php delete mode 100644 Panel/modules/billing/docs/miscreated/metadata.json delete mode 100644 Panel/modules/billing/docs/mohaa/icon.png delete mode 100644 Panel/modules/billing/docs/mohaa/index.php delete mode 100644 Panel/modules/billing/docs/mohaa/metadata.json delete mode 100644 Panel/modules/billing/docs/mohbr/icon.png delete mode 100644 Panel/modules/billing/docs/mohbr/index.php delete mode 100644 Panel/modules/billing/docs/mohbr/metadata.json delete mode 100644 Panel/modules/billing/docs/mohsp/icon.png delete mode 100644 Panel/modules/billing/docs/mohsp/index.php delete mode 100644 Panel/modules/billing/docs/mohsp/metadata.json delete mode 100644 Panel/modules/billing/docs/mohspdemo/icon.png delete mode 100644 Panel/modules/billing/docs/mohspdemo/index.php delete mode 100644 Panel/modules/billing/docs/mohspdemo/metadata.json delete mode 100644 Panel/modules/billing/docs/mordhau/icon.jpg delete mode 100644 Panel/modules/billing/docs/mordhau/index.php delete mode 100644 Panel/modules/billing/docs/mordhau/metadata.json delete mode 100644 Panel/modules/billing/docs/multitheftauto/icon.png delete mode 100644 Panel/modules/billing/docs/multitheftauto/index.php delete mode 100644 Panel/modules/billing/docs/multitheftauto/metadata.json delete mode 100644 Panel/modules/billing/docs/mumble/icon.jpg delete mode 100644 Panel/modules/billing/docs/mumble/index.php delete mode 100644 Panel/modules/billing/docs/mumble/metadata.json delete mode 100644 Panel/modules/billing/docs/nexuiz/icon.png delete mode 100644 Panel/modules/billing/docs/nexuiz/index.php delete mode 100644 Panel/modules/billing/docs/nexuiz/metadata.json delete mode 100644 Panel/modules/billing/docs/nmrih_steam/icon.jpg delete mode 100644 Panel/modules/billing/docs/nmrih_steam/index.php delete mode 100644 Panel/modules/billing/docs/nmrih_steam/metadata.json delete mode 100644 Panel/modules/billing/docs/ns2/icon.png delete mode 100644 Panel/modules/billing/docs/ns2/index.php delete mode 100644 Panel/modules/billing/docs/ns2/metadata.json delete mode 100644 Panel/modules/billing/docs/nucleardawn/icon.png delete mode 100644 Panel/modules/billing/docs/nucleardawn/index.php delete mode 100644 Panel/modules/billing/docs/nucleardawn/metadata.json delete mode 100644 Panel/modules/billing/docs/ootow/icon.jpg delete mode 100644 Panel/modules/billing/docs/ootow/index.php delete mode 100644 Panel/modules/billing/docs/ootow/metadata.json delete mode 100644 Panel/modules/billing/docs/openttd/icon.png delete mode 100644 Panel/modules/billing/docs/openttd/index.php delete mode 100644 Panel/modules/billing/docs/openttd/metadata.json delete mode 100644 Panel/modules/billing/docs/oxide/index.php delete mode 100644 Panel/modules/billing/docs/oxide/metadata.json delete mode 100644 Panel/modules/billing/docs/pixark/icon.png delete mode 100644 Panel/modules/billing/docs/pixark/index.php delete mode 100644 Panel/modules/billing/docs/pixark/metadata.json delete mode 100644 Panel/modules/billing/docs/pvkii/icon.png delete mode 100644 Panel/modules/billing/docs/pvkii/index.php delete mode 100644 Panel/modules/billing/docs/pvkii/metadata.json delete mode 100644 Panel/modules/billing/docs/quake3/icon.png delete mode 100644 Panel/modules/billing/docs/quake3/index.php delete mode 100644 Panel/modules/billing/docs/quake3/metadata.json delete mode 100644 Panel/modules/billing/docs/quake4/icon.png delete mode 100644 Panel/modules/billing/docs/quake4/index.php delete mode 100644 Panel/modules/billing/docs/quake4/metadata.json delete mode 100644 Panel/modules/billing/docs/redorchestra2/icon.png delete mode 100644 Panel/modules/billing/docs/redorchestra2/index.php delete mode 100644 Panel/modules/billing/docs/redorchestra2/metadata.json delete mode 100644 Panel/modules/billing/docs/reignofkings/icon.png delete mode 100644 Panel/modules/billing/docs/reignofkings/index.php delete mode 100644 Panel/modules/billing/docs/reignofkings/metadata.json delete mode 100644 Panel/modules/billing/docs/ricochet/icon.png delete mode 100644 Panel/modules/billing/docs/ricochet/index.php delete mode 100644 Panel/modules/billing/docs/ricochet/metadata.json delete mode 100644 Panel/modules/billing/docs/risingstorm2/icon.png delete mode 100644 Panel/modules/billing/docs/risingstorm2/index.php delete mode 100644 Panel/modules/billing/docs/risingstorm2/metadata.json delete mode 100644 Panel/modules/billing/docs/roadkill/icon.png delete mode 100644 Panel/modules/billing/docs/roadkill/index.php delete mode 100644 Panel/modules/billing/docs/roadkill/metadata.json delete mode 100644 Panel/modules/billing/docs/rorserver/icon.png delete mode 100644 Panel/modules/billing/docs/rorserver/index.php delete mode 100644 Panel/modules/billing/docs/rorserver/metadata.json delete mode 100644 Panel/modules/billing/docs/rust/icon.png delete mode 100644 Panel/modules/billing/docs/rust/index.php delete mode 100644 Panel/modules/billing/docs/rust/index_old.php delete mode 100644 Panel/modules/billing/docs/rust/metadata.json delete mode 100644 Panel/modules/billing/docs/sanandreasmp/icon.png delete mode 100644 Panel/modules/billing/docs/sanandreasmp/index.php delete mode 100644 Panel/modules/billing/docs/sanandreasmp/metadata.json delete mode 100644 Panel/modules/billing/docs/serioussamhdfe/icon.png delete mode 100644 Panel/modules/billing/docs/serioussamhdfe/index.php delete mode 100644 Panel/modules/billing/docs/serioussamhdfe/metadata.json delete mode 100644 Panel/modules/billing/docs/serioussamhdse/icon.png delete mode 100644 Panel/modules/billing/docs/serioussamhdse/index.php delete mode 100644 Panel/modules/billing/docs/serioussamhdse/metadata.json delete mode 100644 Panel/modules/billing/docs/shoutcast/icon.png delete mode 100644 Panel/modules/billing/docs/shoutcast/index.php delete mode 100644 Panel/modules/billing/docs/shoutcast/metadata.json delete mode 100644 Panel/modules/billing/docs/shoutcast_bot/icon.png delete mode 100644 Panel/modules/billing/docs/shoutcast_bot/index.php delete mode 100644 Panel/modules/billing/docs/shoutcast_bot/metadata.json delete mode 100644 Panel/modules/billing/docs/sinusbot/icon.png delete mode 100644 Panel/modules/billing/docs/sinusbot/index.php delete mode 100644 Panel/modules/billing/docs/sinusbot/metadata.json delete mode 100644 Panel/modules/billing/docs/smashball/icon.png delete mode 100644 Panel/modules/billing/docs/smashball/index.php delete mode 100644 Panel/modules/billing/docs/smashball/metadata.json delete mode 100644 Panel/modules/billing/docs/smokinguns/icon.png delete mode 100644 Panel/modules/billing/docs/smokinguns/index.php delete mode 100644 Panel/modules/billing/docs/smokinguns/metadata.json delete mode 100644 Panel/modules/billing/docs/sms/icon.png delete mode 100644 Panel/modules/billing/docs/sms/index.php delete mode 100644 Panel/modules/billing/docs/sms/metadata.json delete mode 100644 Panel/modules/billing/docs/sniperelitev2/icon.png delete mode 100644 Panel/modules/billing/docs/sniperelitev2/index.php delete mode 100644 Panel/modules/billing/docs/sniperelitev2/metadata.json delete mode 100644 Panel/modules/billing/docs/soldatserver/icon.png delete mode 100644 Panel/modules/billing/docs/soldatserver/index.php delete mode 100644 Panel/modules/billing/docs/soldatserver/metadata.json delete mode 100644 Panel/modules/billing/docs/space_engineers/icon.jpg delete mode 100644 Panel/modules/billing/docs/space_engineers/index.php delete mode 100644 Panel/modules/billing/docs/space_engineers/metadata.json delete mode 100644 Panel/modules/billing/docs/spigotmc/icon.png delete mode 100644 Panel/modules/billing/docs/spigotmc/index.php delete mode 100644 Panel/modules/billing/docs/spigotmc/metadata.json delete mode 100644 Panel/modules/billing/docs/spunkybot/icon.jpg delete mode 100644 Panel/modules/billing/docs/spunkybot/index.php delete mode 100644 Panel/modules/billing/docs/spunkybot/metadata.json delete mode 100644 Panel/modules/billing/docs/squad/icon.jpg delete mode 100644 Panel/modules/billing/docs/squad/index.php delete mode 100644 Panel/modules/billing/docs/squad/metadata.json delete mode 100644 Panel/modules/billing/docs/starbound/icon.jpg delete mode 100644 Panel/modules/billing/docs/starbound/index.php delete mode 100644 Panel/modules/billing/docs/starbound/metadata.json delete mode 100644 Panel/modules/billing/docs/stationeers/icon.jpg delete mode 100644 Panel/modules/billing/docs/stationeers/index.php delete mode 100644 Panel/modules/billing/docs/stationeers/metadata.json delete mode 100644 Panel/modules/billing/docs/synergy/icon.png delete mode 100644 Panel/modules/billing/docs/synergy/index.php delete mode 100644 Panel/modules/billing/docs/synergy/metadata.json delete mode 100644 Panel/modules/billing/docs/teamspeak2/icon.png delete mode 100644 Panel/modules/billing/docs/teamspeak2/index.php delete mode 100644 Panel/modules/billing/docs/teamspeak2/metadata.json delete mode 100644 Panel/modules/billing/docs/teamspeak3/icon.png delete mode 100644 Panel/modules/billing/docs/teamspeak3/index.php delete mode 100644 Panel/modules/billing/docs/teamspeak3/metadata.json delete mode 100644 Panel/modules/billing/docs/terraria/icon.jpg delete mode 100644 Panel/modules/billing/docs/terraria/index.php delete mode 100644 Panel/modules/billing/docs/terraria/metadata.json delete mode 100644 Panel/modules/billing/docs/tf2/icon.jpg delete mode 100644 Panel/modules/billing/docs/tf2/index.php delete mode 100644 Panel/modules/billing/docs/tf2/index_old.php delete mode 100644 Panel/modules/billing/docs/tf2/metadata.json delete mode 100644 Panel/modules/billing/docs/tfc/icon.png delete mode 100644 Panel/modules/billing/docs/tfc/index.php delete mode 100644 Panel/modules/billing/docs/tfc/metadata.json delete mode 100644 Panel/modules/billing/docs/theforest/icon.png delete mode 100644 Panel/modules/billing/docs/theforest/index.php delete mode 100644 Panel/modules/billing/docs/theforest/metadata.json delete mode 100644 Panel/modules/billing/docs/trackmanianations/icon.png delete mode 100644 Panel/modules/billing/docs/trackmanianations/index.php delete mode 100644 Panel/modules/billing/docs/trackmanianations/metadata.json delete mode 100644 Panel/modules/billing/docs/trackmanianf/icon.png delete mode 100644 Panel/modules/billing/docs/trackmanianf/index.php delete mode 100644 Panel/modules/billing/docs/trackmanianf/metadata.json delete mode 100644 Panel/modules/billing/docs/unturned/icon.jpg delete mode 100644 Panel/modules/billing/docs/unturned/index.php delete mode 100644 Panel/modules/billing/docs/unturned/metadata.json delete mode 100644 Panel/modules/billing/docs/urt/icon.jpg delete mode 100644 Panel/modules/billing/docs/urt/index.php delete mode 100644 Panel/modules/billing/docs/urt/metadata.json delete mode 100644 Panel/modules/billing/docs/ut2004/icon.png delete mode 100644 Panel/modules/billing/docs/ut2004/index.php delete mode 100644 Panel/modules/billing/docs/ut2004/metadata.json delete mode 100644 Panel/modules/billing/docs/ut3/icon.png delete mode 100644 Panel/modules/billing/docs/ut3/index.php delete mode 100644 Panel/modules/billing/docs/ut3/metadata.json delete mode 100644 Panel/modules/billing/docs/ut99/icon.png delete mode 100644 Panel/modules/billing/docs/ut99/index.php delete mode 100644 Panel/modules/billing/docs/ut99/metadata.json delete mode 100644 Panel/modules/billing/docs/valheim/icon.jpg delete mode 100644 Panel/modules/billing/docs/valheim/index.php delete mode 100644 Panel/modules/billing/docs/valheim/metadata.json delete mode 100644 Panel/modules/billing/docs/vbox/icon.png delete mode 100644 Panel/modules/billing/docs/vbox/index.php delete mode 100644 Panel/modules/billing/docs/vbox/metadata.json delete mode 100644 Panel/modules/billing/docs/ventrilo/icon.png delete mode 100644 Panel/modules/billing/docs/ventrilo/index.php delete mode 100644 Panel/modules/billing/docs/ventrilo/metadata.json delete mode 100644 Panel/modules/billing/docs/vicecitymp/icon.png delete mode 100644 Panel/modules/billing/docs/vicecitymp/index.php delete mode 100644 Panel/modules/billing/docs/vicecitymp/metadata.json delete mode 100644 Panel/modules/billing/docs/warsow/icon.png delete mode 100644 Panel/modules/billing/docs/warsow/index.php delete mode 100644 Panel/modules/billing/docs/warsow/metadata.json delete mode 100644 Panel/modules/billing/docs/wolfrtcw_1-4/icon.png delete mode 100644 Panel/modules/billing/docs/wolfrtcw_1-4/index.php delete mode 100644 Panel/modules/billing/docs/wolfrtcw_1-4/metadata.json delete mode 100644 Panel/modules/billing/docs/wreckfest/icon.png delete mode 100644 Panel/modules/billing/docs/wreckfest/index.php delete mode 100644 Panel/modules/billing/docs/wreckfest/metadata.json delete mode 100644 Panel/modules/billing/docs/wurmu/icon.jpg delete mode 100644 Panel/modules/billing/docs/wurmu/index.php delete mode 100644 Panel/modules/billing/docs/wurmu/metadata.json delete mode 100644 Panel/modules/billing/docs/xml_notes.php delete mode 100644 Panel/modules/billing/docs/xonotic/icon.png delete mode 100644 Panel/modules/billing/docs/xonotic/index.php delete mode 100644 Panel/modules/billing/docs/xonotic/metadata.json delete mode 100644 Panel/modules/billing/docs/zps/icon.png delete mode 100644 Panel/modules/billing/docs/zps/index.php delete mode 100644 Panel/modules/billing/docs/zps/metadata.json delete mode 100644 Panel/modules/billing/fix_invoices_table_columns.sql delete mode 100644 Panel/modules/billing/forgot_password.php delete mode 100644 Panel/modules/billing/images/banner.png delete mode 100644 Panel/modules/billing/images/bf3_the_russian.jpg delete mode 100644 Panel/modules/billing/images/dark.jpg delete mode 100644 Panel/modules/billing/images/featured/7dtd.jpg delete mode 100644 Panel/modules/billing/images/featured/arkse.jpg delete mode 100644 Panel/modules/billing/images/featured/arma2_operation_arrowhead.jpg delete mode 100644 Panel/modules/billing/images/featured/arma_3.jpg delete mode 100644 Panel/modules/billing/images/featured/cs_go.jpg delete mode 100644 Panel/modules/billing/images/featured/day_z.jpg delete mode 100644 Panel/modules/billing/images/featured/dayz_epochmod.jpg delete mode 100644 Panel/modules/billing/images/featured/dayz_mod.jpg delete mode 100644 Panel/modules/billing/images/featured/eurotruck2.jpg delete mode 100644 Panel/modules/billing/images/featured/fistful_of_frags.jpg delete mode 100644 Panel/modules/billing/images/featured/insurgency.jpg delete mode 100644 Panel/modules/billing/images/featured/insurgency_sandstorm.jpg delete mode 100644 Panel/modules/billing/images/featured/minecraft.jpg delete mode 100644 Panel/modules/billing/images/games/7dtd.jpg delete mode 100644 Panel/modules/billing/images/games/arkse.jpg delete mode 100644 Panel/modules/billing/images/games/arma2.jpg delete mode 100644 Panel/modules/billing/images/games/arma2_operation_arrowhead.jpg delete mode 100644 Panel/modules/billing/images/games/arma_3.jpg delete mode 100644 Panel/modules/billing/images/games/asseto.jpg delete mode 100644 Panel/modules/billing/images/games/avorion.jpg delete mode 100644 Panel/modules/billing/images/games/brainbread_2.jpg delete mode 100644 Panel/modules/billing/images/games/chivalry.jpg delete mode 100644 Panel/modules/billing/images/games/citadel.jpg delete mode 100644 Panel/modules/billing/images/games/colonysurvival.jpg delete mode 100644 Panel/modules/billing/images/games/conanexiles.jpg delete mode 100644 Panel/modules/billing/images/games/cs_go.jpg delete mode 100644 Panel/modules/billing/images/games/cstrike.jpg delete mode 100644 Panel/modules/billing/images/games/cstrikesource.jpg delete mode 100644 Panel/modules/billing/images/games/day_of_defeat_source.jpg delete mode 100644 Panel/modules/billing/images/games/day_z.jpg delete mode 100644 Panel/modules/billing/images/games/dayz_epochmod.jpg delete mode 100644 Panel/modules/billing/images/games/dayz_mod.jpg delete mode 100644 Panel/modules/billing/images/games/deathmatch_classic.jpg delete mode 100644 Panel/modules/billing/images/games/dst.jpg delete mode 100644 Panel/modules/billing/images/games/eco.jpg delete mode 100644 Panel/modules/billing/images/games/eurotruck2.jpg delete mode 100644 Panel/modules/billing/images/games/fistful_of_frags.jpg delete mode 100644 Panel/modules/billing/images/games/garrys_mod.jpg delete mode 100644 Panel/modules/billing/images/games/half-life2_deathmatch.jpg delete mode 100644 Panel/modules/billing/images/games/harsh.jpg delete mode 100644 Panel/modules/billing/images/games/hurt_world.jpg delete mode 100644 Panel/modules/billing/images/games/insurgency.jpg delete mode 100644 Panel/modules/billing/images/games/insurgency_sandstorm.jpg delete mode 100644 Panel/modules/billing/images/games/killing_floor.jpg delete mode 100644 Panel/modules/billing/images/games/killing_floor_2.jpg delete mode 100644 Panel/modules/billing/images/games/left_4_dead.jpg delete mode 100644 Panel/modules/billing/images/games/left_4_dead_2.jpg delete mode 100644 Panel/modules/billing/images/games/minecraft.jpg delete mode 100644 Panel/modules/billing/images/games/miscreated_server.jpg delete mode 100644 Panel/modules/billing/images/games/mordhau.jpg delete mode 100644 Panel/modules/billing/images/games/nomoreroominhell.jpg delete mode 100644 Panel/modules/billing/images/games/ootow.jpg delete mode 100644 Panel/modules/billing/images/games/rust_header.jpg delete mode 100644 Panel/modules/billing/images/games/scp.jpg delete mode 100644 Panel/modules/billing/images/games/squad.jpg delete mode 100644 Panel/modules/billing/images/games/starbound.jpg delete mode 100644 Panel/modules/billing/images/games/stationeers.jpg delete mode 100644 Panel/modules/billing/images/games/team_fortress_2.jpg delete mode 100644 Panel/modules/billing/images/games/terraria.jpg delete mode 100644 Panel/modules/billing/images/games/urt.jpg delete mode 100644 Panel/modules/billing/images/games/valheim.jpg delete mode 100644 Panel/modules/billing/images/games/wurmu.jpg delete mode 100644 Panel/modules/billing/images/logo-sm.png delete mode 100644 Panel/modules/billing/images/logo.jpg delete mode 100644 Panel/modules/billing/images/logo.png delete mode 100644 Panel/modules/billing/includes/README.md delete mode 100644 Panel/modules/billing/includes/admin_auth.php delete mode 100644 Panel/modules/billing/includes/cart_helper.php delete mode 100644 Panel/modules/billing/includes/config.example.php delete mode 100644 Panel/modules/billing/includes/config_loader.php delete mode 100644 Panel/modules/billing/includes/footer.php delete mode 100644 Panel/modules/billing/includes/log.php delete mode 100644 Panel/modules/billing/includes/login_required.php delete mode 100644 Panel/modules/billing/includes/menu.php delete mode 100644 Panel/modules/billing/includes/panel_bridge.php delete mode 100644 Panel/modules/billing/includes/payment_processor.php delete mode 100644 Panel/modules/billing/includes/session_bridge.php delete mode 100644 Panel/modules/billing/includes/top.php delete mode 100644 Panel/modules/billing/index.php delete mode 100644 Panel/modules/billing/invoices.php delete mode 100644 Panel/modules/billing/logfile.txt delete mode 100644 Panel/modules/billing/login.php delete mode 100644 Panel/modules/billing/logout.php delete mode 100644 Panel/modules/billing/migration_to_invoices.sql delete mode 100644 Panel/modules/billing/module.php delete mode 100644 Panel/modules/billing/my_account.php delete mode 100644 Panel/modules/billing/my_orders_panel.php delete mode 100644 Panel/modules/billing/my_servers.php delete mode 100644 Panel/modules/billing/navigation.xml delete mode 100644 Panel/modules/billing/normalize_billing_order_status.sql delete mode 100644 Panel/modules/billing/order.php delete mode 100644 Panel/modules/billing/payment_cancel.php delete mode 100644 Panel/modules/billing/payment_success.php delete mode 100644 Panel/modules/billing/paypal/webhook.php delete mode 100644 Panel/modules/billing/privacy.php delete mode 100644 Panel/modules/billing/register.php delete mode 100644 Panel/modules/billing/renew_server.php delete mode 100644 Panel/modules/billing/reset_password.php delete mode 100644 Panel/modules/billing/return.php delete mode 100644 Panel/modules/billing/server_status.php delete mode 100644 Panel/modules/billing/serverlist.php delete mode 100644 Panel/modules/billing/site_config.example.php delete mode 100644 Panel/modules/billing/site_config.php delete mode 100644 Panel/modules/billing/sql/002_billing_checkout_fixes.sql delete mode 100644 Panel/modules/billing/sql/normalize_billing_order_status.sql delete mode 100644 Panel/modules/billing/test_db_connection.php delete mode 100644 Panel/modules/billing/test_integration.php delete mode 100644 Panel/modules/billing/timestamp.txt delete mode 100644 Panel/modules/billing/tools/check_db_user.php delete mode 100644 Panel/modules/billing/tools/check_invoices_redirect.php delete mode 100644 Panel/modules/billing/tools/check_logout_redirect.php delete mode 100644 Panel/modules/billing/tools/debug_invoices_redirect.php delete mode 100644 Panel/modules/billing/tools/simulate_webhook.php delete mode 100644 Panel/modules/billing/tos.php delete mode 100644 Panel/modules/billing/update_metadata_complete.ps1 delete mode 100644 Panel/modules/billing/webhook.php delete mode 100755 Panel/modules/reseller/account_details.php delete mode 100755 Panel/modules/reseller/accounts.php delete mode 100755 Panel/modules/reseller/add_to_cart.php delete mode 100755 Panel/modules/reseller/assign_server.php delete mode 100755 Panel/modules/reseller/bill.php delete mode 100755 Panel/modules/reseller/cart.css delete mode 100755 Panel/modules/reseller/cart.php delete mode 100755 Panel/modules/reseller/cron-shop.php delete mode 100755 Panel/modules/reseller/ipn_errors.log delete mode 100755 Panel/modules/reseller/ipnlistener.php delete mode 100755 Panel/modules/reseller/module.php delete mode 100755 Panel/modules/reseller/navigation.xml delete mode 100755 Panel/modules/reseller/pack_image.png delete mode 100755 Panel/modules/reseller/paid-ipn.php delete mode 100755 Panel/modules/reseller/paid.php delete mode 100755 Panel/modules/reseller/paypal.class.php delete mode 100755 Panel/modules/reseller/paypal.php delete mode 100755 Panel/modules/reseller/rs_accounts.css delete mode 100755 Panel/modules/reseller/rs_assign_server.css delete mode 100755 Panel/modules/reseller/rs_packs_shop.css delete mode 100755 Panel/modules/reseller/services.php delete mode 100755 Panel/modules/reseller/settings.php delete mode 100755 Panel/modules/reseller/shop.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/functions.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/361580_Windows.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/376030_Linux.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/376030_Windows.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/4020_Linux.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/443030_Windows.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/533830_Linux.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/533830_Windows.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/game_configs/740_Linux.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/main.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/module.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/monitor_buttons.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/navigation.xml delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/steam_workshop.css delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/uninstall.php delete mode 100644 Panel/modules/steam_workshop.bak.20260609-145834/workshop_admin.php delete mode 100644 Panel/modules/tickets/downloadAttachment.php delete mode 100644 Panel/modules/tickets/include/Attachments.php delete mode 100644 Panel/modules/tickets/include/TicketSettings.php delete mode 100644 Panel/modules/tickets/include/array_column.php delete mode 100644 Panel/modules/tickets/include/functions.php delete mode 100644 Panel/modules/tickets/include/mime.types.php delete mode 100644 Panel/modules/tickets/include/ticket.php delete mode 100644 Panel/modules/tickets/js/helpers.js delete mode 100644 Panel/modules/tickets/js/javascript_vars.php delete mode 100644 Panel/modules/tickets/js/rating.js delete mode 100644 Panel/modules/tickets/js/ticket.js delete mode 100644 Panel/modules/tickets/js/ticket_settings.js delete mode 100644 Panel/modules/tickets/module.php delete mode 100644 Panel/modules/tickets/navigation.xml delete mode 100644 Panel/modules/tickets/notificationCount.php delete mode 100644 Panel/modules/tickets/rating.php delete mode 100644 Panel/modules/tickets/submitTicket.php delete mode 100644 Panel/modules/tickets/submitticket.css delete mode 100644 Panel/modules/tickets/supportTickets.php delete mode 100644 Panel/modules/tickets/ticketSettings.php delete mode 100644 Panel/modules/tickets/ticket_settings.css delete mode 100644 Panel/modules/tickets/tickets.css delete mode 100644 Panel/modules/tickets/uploads/.htaccess delete mode 100644 Panel/modules/tickets/uploads/0fba4a38e62886201af21ceb.png delete mode 100644 Panel/modules/tickets/uploads/2218ec951fb162d80c6fdf39.png delete mode 100644 Panel/modules/tickets/uploads/38cb80de51c3ea2744f6b837.png delete mode 100644 Panel/modules/tickets/uploads/3926ed7bd92097bdfae4b348.png delete mode 100644 Panel/modules/tickets/uploads/readme.txt delete mode 100644 Panel/modules/tickets/viewTicket.php delete mode 100644 Panel/modules/tickets/viewticket.css create mode 100644 Panel/modules/website/account.php create mode 100644 Panel/modules/website/logout.php create mode 100644 Panel/modules/website/order.php create mode 100644 Panel/modules/website/pages/account.php create mode 100644 Panel/modules/website/pages/login.php create mode 100644 Panel/modules/website/pages/order.php create mode 100644 Panel/modules/website/sso.php create mode 100644 Panel/sso.php diff --git a/Panel/includes/functions.php b/Panel/includes/functions.php index 71f1ee59..b4d9ad5b 100644 --- a/Panel/includes/functions.php +++ b/Panel/includes/functions.php @@ -66,6 +66,16 @@ function gsp_project_request_url(): string { return 'https://runlevelsystems.com/start-project.php'; } +function gsp_website_url(string $path = ''): string { + $base = 'https://gameservers.world/'; + $path = ltrim($path, '/'); + return $path === '' ? $base : rtrim($base, '/') . '/' . $path; +} + +function gsp_panel_to_website_sso_url(string $returnPath = 'serverlist.php'): string { + return 'sso.php?destination=website&return=' . rawurlencode($returnPath); +} + function gsp_discord_invite_url(): string { return 'https://discord.gg/qt9Hnkj6cv'; } diff --git a/Panel/includes/sso.php b/Panel/includes/sso.php new file mode 100644 index 00000000..cacb60b4 --- /dev/null +++ b/Panel/includes/sso.php @@ -0,0 +1,204 @@ +real_escape_string($prefix . 'sso_tokens'); + $db->query( + "CREATE TABLE IF NOT EXISTS `{$table}` ( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `token_hash` CHAR(64) NOT NULL, + `user_id` INT(11) NOT NULL, + `source` VARCHAR(32) NOT NULL, + `destination` VARCHAR(32) NOT NULL, + `created_at` DATETIME NOT NULL, + `expires_at` DATETIME NOT NULL, + `used_at` DATETIME DEFAULT NULL, + `nonce` VARCHAR(64) NOT NULL, + `originating_ip` VARCHAR(64) DEFAULT NULL, + `user_agent_hash` CHAR(64) DEFAULT NULL, + `return_path` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sso_token_hash` (`token_hash`), + KEY `idx_sso_user_destination` (`user_id`, `destination`), + KEY `idx_sso_expires` (`expires_at`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" + ); +} + +function gsp_sso_cleanup(mysqli $db, string $prefix): void +{ + gsp_sso_ensure_table($db, $prefix); + $table = $db->real_escape_string($prefix . 'sso_tokens'); + $db->query("DELETE FROM `{$table}` WHERE `expires_at` < (UTC_TIMESTAMP() - INTERVAL 10 MINUTE)"); +} + +function gsp_sso_create_token(mysqli $db, string $prefix, int $userId, string $source, string $destination, string $returnPath, int $ttlSeconds = 60): ?string +{ + if (!gsp_sso_is_secure_request()) { + return null; + } + + gsp_sso_cleanup($db, $prefix); + + $token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '='); + $tokenHash = gsp_sso_hash_token($token); + $nonce = bin2hex(random_bytes(16)); + $createdAt = gmdate('Y-m-d H:i:s'); + $expiresAt = gmdate('Y-m-d H:i:s', time() + max(30, min(60, $ttlSeconds))); + $table = $db->real_escape_string($prefix . 'sso_tokens'); + + $stmt = $db->prepare( + "INSERT INTO `{$table}` + (`token_hash`, `user_id`, `source`, `destination`, `created_at`, `expires_at`, `nonce`, `originating_ip`, `user_agent_hash`, `return_path`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + ); + if (!$stmt) { + return null; + } + + $ip = substr(gsp_sso_client_ip(), 0, 64); + $userAgentHash = gsp_sso_user_agent_hash(); + $returnPath = substr($returnPath, 0, 255); + $stmt->bind_param('sissssssss', $tokenHash, $userId, $source, $destination, $createdAt, $expiresAt, $nonce, $ip, $userAgentHash, $returnPath); + $ok = $stmt->execute(); + $stmt->close(); + + return $ok ? $token : null; +} + +function gsp_sso_validate_token(mysqli $db, string $prefix, string $token, string $destination): array +{ + if (!gsp_sso_is_secure_request()) { + return ['success' => false, 'error' => 'SSO requires HTTPS.']; + } + + if (preg_match('/^[A-Za-z0-9_-]{32,128}$/', $token) !== 1) { + return ['success' => false, 'error' => 'Invalid SSO token.']; + } + + gsp_sso_cleanup($db, $prefix); + + $tokenHash = gsp_sso_hash_token($token); + $table = $db->real_escape_string($prefix . 'sso_tokens'); + $stmt = $db->prepare("SELECT * FROM `{$table}` WHERE `token_hash` = ? LIMIT 1"); + if (!$stmt) { + return ['success' => false, 'error' => 'SSO is unavailable.']; + } + + $stmt->bind_param('s', $tokenHash); + $stmt->execute(); + $result = $stmt->get_result(); + $row = $result instanceof mysqli_result ? $result->fetch_assoc() : null; + $stmt->close(); + + if (!$row) { + return ['success' => false, 'error' => 'Invalid SSO token.']; + } + + if (!hash_equals((string)$row['token_hash'], $tokenHash)) { + return ['success' => false, 'error' => 'Invalid SSO token.']; + } + + if ((string)$row['destination'] !== $destination) { + return ['success' => false, 'error' => 'Invalid SSO destination.']; + } + + if (!empty($row['used_at'])) { + return ['success' => false, 'error' => 'This SSO link has already been used.']; + } + + if (strtotime((string)$row['expires_at'] . ' UTC') < time()) { + return ['success' => false, 'error' => 'This SSO link has expired.']; + } + + $currentAgentHash = gsp_sso_user_agent_hash(); + if (!empty($row['user_agent_hash']) && !hash_equals((string)$row['user_agent_hash'], $currentAgentHash)) { + return ['success' => false, 'error' => 'This SSO link is not valid for this browser.']; + } + + $usedAt = gmdate('Y-m-d H:i:s'); + $mark = $db->prepare("UPDATE `{$table}` SET `used_at` = ? WHERE `id` = ? AND `used_at` IS NULL"); + if (!$mark) { + return ['success' => false, 'error' => 'SSO is unavailable.']; + } + + $id = (int)$row['id']; + $mark->bind_param('si', $usedAt, $id); + $mark->execute(); + $updated = $mark->affected_rows === 1; + $mark->close(); + + if (!$updated) { + return ['success' => false, 'error' => 'This SSO link has already been used.']; + } + + return [ + 'success' => true, + 'user_id' => (int)$row['user_id'], + 'source' => (string)$row['source'], + 'destination' => (string)$row['destination'], + 'return_path' => (string)($row['return_path'] ?? ''), + ]; +} diff --git a/Panel/modules/administration/module.php b/Panel/modules/administration/module.php index 6ee79900..fd63f9d1 100644 --- a/Panel/modules/administration/module.php +++ b/Panel/modules/administration/module.php @@ -62,4 +62,25 @@ $install_queries[1] = array( KEY `idx_logger_source_category` (`source_type`,`category`,`severity`), KEY `idx_logger_home` (`home_id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;"); + +$install_queries[2] = array( +"CREATE TABLE IF NOT EXISTS `".OGP_DB_PREFIX."sso_tokens` +( + `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, + `token_hash` CHAR(64) NOT NULL, + `user_id` INT(11) NOT NULL, + `source` VARCHAR(32) NOT NULL, + `destination` VARCHAR(32) NOT NULL, + `created_at` DATETIME NOT NULL, + `expires_at` DATETIME NOT NULL, + `used_at` DATETIME DEFAULT NULL, + `nonce` VARCHAR(64) NOT NULL, + `originating_ip` VARCHAR(64) DEFAULT NULL, + `user_agent_hash` CHAR(64) DEFAULT NULL, + `return_path` VARCHAR(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_sso_token_hash` (`token_hash`), + KEY `idx_sso_user_destination` (`user_id`, `destination`), + KEY `idx_sso_expires` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"); ?> diff --git a/Panel/modules/billing/BILLING_FIX_SUMMARY.md b/Panel/modules/billing/BILLING_FIX_SUMMARY.md deleted file mode 100644 index d25316c8..00000000 --- a/Panel/modules/billing/BILLING_FIX_SUMMARY.md +++ /dev/null @@ -1,242 +0,0 @@ -# Billing Invoice/Order Flow - Fix Summary - -## Problem Statement - -The billing system had several critical issues: - -1. **JSON Error**: "Failed to execute 'json' on 'Response': Unexpected end of JSON input" when returning from PayPal payment -2. **Cart not clearing**: Items remained in cart after payment (invoices stayed as status='due') -3. **No order creation**: Orders were not being created after successful payment -4. **Missing renewal flow**: Renewal invoices (linked to existing orders) were not handled -5. **Free button errors**: The free/claim button was also experiencing errors - -## Invoice-First Flow (Intended Design) - -The system uses an invoice-first architecture: - -1. **Add to Cart**: Creates INVOICE with status='due', order_id=0 (no order yet) -2. **View Cart**: Shows all invoices WHERE status='due' -3. **Payment**: - - For NEW orders (order_id=0): Mark invoice paid + CREATE new order - - For RENEWALS (order_id>0): Mark invoice paid + EXTEND existing order's end_date -4. **Provisioning**: Separate step that provisions servers for paid orders - -## Root Causes Identified - -### 1. Missing Function -- `process_payment_record()` was called but never defined -- Referenced in webhook.php, cart.php (free button), but didn't exist -- This prevented any payment processing from completing - -### 2. JSON Response Corruption -- `capture_order.php` had PHP errors/warnings during DB operations -- These were being output to the response, corrupting the JSON -- JavaScript couldn't parse the malformed JSON → "Unexpected end of JSON input" - -### 3. Incomplete Payment Processing -- `capture_order.php` was supposed to: - - Mark invoices as paid (status: 'due' → 'paid') - - Create new orders OR extend existing orders - - Link invoices to orders -- But the logic was incomplete and had issues - -### 4. Session Compatibility -- capture_order.php used `$_SESSION['user_id']` -- cart.php used `$_SESSION['website_user_id']` -- This mismatch meant user couldn't be identified for payment processing - -### 5. Hardcoded Table Names -- capture_order.php used hardcoded "ogp_billing_invoices" and "ogp_billing_orders" -- Should use `$table_prefix . "billing_invoices"` for flexibility -- Could cause failures if table prefix is different - -## Solutions Implemented - -### 1. Created payment_processor.php Helper -**File**: `modules/billing/includes/payment_processor.php` - -**Function**: `process_payment_record($record)` -- Accepts payment record from webhook or direct capture -- Finds invoices to process by custom_id (invoice_id) or invoice reference -- For each invoice: - - Marks invoice as paid (status='due' → 'paid') - - If NEW order (order_id=0): Creates new order with calculated end_date - - If RENEWAL (order_id>0): Extends existing order's end_date by invoice duration - - Links invoice to order -- Returns true/false and logs all operations -- No HTML output (safe to require from webhook/API endpoints) - -### 2. Fixed capture_order.php -**File**: `modules/billing/api/capture_order.php` - -**Changes**: -- **Disabled error display**: `ini_set('display_errors', '0')` to prevent JSON corruption -- **Session compatibility**: Checks both `website_user_id` and `user_id` -- **Proper JSON errors**: Returns structured JSON on DB connection failure -- **Table prefix usage**: Uses `$table_prefix` instead of hardcoded names -- **Complete invoice processing**: - - Marks all due invoices as paid - - Handles both NEW orders and RENEWALS - - Proper end_date calculation (months from qty + invoice_duration) - - Links invoices to orders - -### 3. Fixed payment_success.php -**File**: `modules/billing/payment_success.php` - -**Changes**: -- Requires `payment_processor.php` helper -- Displays payment confirmation page -- Shows user's recent orders -- No longer contains duplicate/incomplete function definitions - -### 4. Fixed webhook.php -**File**: `modules/billing/webhook.php` - -**Changes**: -- Uses `payment_processor.php` instead of requiring full payment_success.php -- Prevents HTML output that would interfere with webhook response -- Processes payment record after verification - -### 5. Fixed cart.php Free Button -**File**: `modules/billing/cart.php` - -**Changes**: -- Uses `payment_processor.php` for consistent processing -- Free button now properly: - - Marks invoice as paid - - Creates order record - - Calculates end_date - - Processes payment record through shared function - -## Payment Flow (After Fixes) - -### PayPal Payment Flow -``` -1. User clicks "Pay with PayPal" in cart.php - ↓ -2. JavaScript calls api/create_order.php - → Creates PayPal order with custom_id = invoice_id - ↓ -3. User approves payment on PayPal - ↓ -4. JavaScript calls api/capture_order.php - → PayPal captures payment - → capture_order.php: - a) Marks invoices as paid (status='due' → 'paid') - b) For NEW: Creates order in billing_orders - c) For RENEW: Extends existing order's end_date - d) Links invoice to order (sets invoice.order_id) - → Returns JSON: { status: "COMPLETED", ... } - ↓ -5. JavaScript redirects to payment_success.php - → Shows confirmation page - → Displays order details - ↓ -6. PayPal sends webhook to webhook.php (parallel) - → Verifies signature - → Calls process_payment_record() - → Same processing as step 4 (idempotent) - ↓ -7. Cart is empty (invoices now have status='paid', not shown) -``` - -### Free/Claim Flow -``` -1. User clicks "Claim (Free)" button in cart.php - ↓ -2. Cart.php POST handler: - → Marks invoice as paid - → Creates order record with calculated end_date - → Links invoice to order - → Creates simulated webhook file - → Calls process_payment_record() for consistency - ↓ -3. Redirects to return.php - → Shows payment confirmation - ↓ -4. Cart is empty (invoice marked paid) -``` - -### Renewal Flow -``` -1. User has existing order (order_id > 0) - ↓ -2. System creates renewal invoice: - → status = 'due' - → order_id = - → qty = renewal months - ↓ -3. Invoice appears in cart - ↓ -4. User pays (PayPal or Free) - ↓ -5. process_payment_record(): - → Detects order_id > 0 (renewal) - → Fetches current end_date from existing order - → Calculates new end_date: - - If current end_date > now: extend from current end_date - - Otherwise: extend from now - → Updates order with new end_date - → Marks invoice as paid - ↓ -6. Order subscription extended by renewal period -``` - -## Testing Checklist - -Before deployment, verify: - -- [ ] Config setup: Copy `config.inc.php.orig` to `config.inc.php` and configure -- [ ] Database: Ensure `ogp_billing_invoices` and `ogp_billing_orders` tables exist -- [ ] Test NEW order flow: - - [ ] Add item to cart (creates invoice with status='due') - - [ ] View cart (item appears) - - [ ] Click "Claim (Free)" for $0 item (creates order, clears cart) - - [ ] Verify order created in billing_orders - - [ ] Verify invoice marked paid, linked to order -- [ ] Test PayPal flow: - - [ ] Add paid item to cart - - [ ] Click PayPal button - - [ ] Complete payment on PayPal sandbox - - [ ] Verify returns to payment_success.php without errors - - [ ] Verify order created - - [ ] Verify invoice marked paid - - [ ] Verify cart is empty -- [ ] Test RENEWAL flow: - - [ ] Create renewal invoice for existing order - - [ ] Pay renewal invoice - - [ ] Verify order end_date extended correctly - - [ ] Verify invoice marked paid - -## Security Considerations - -All code changes maintain or improve security: - -1. **SQL Injection Protection**: Uses prepared statements where possible -2. **Input Validation**: Validates all user inputs (invoice_id, user_id, etc.) -3. **Session Security**: Maintains separate website/panel sessions -4. **Webhook Verification**: PayPal signature verification still in place -5. **Error Logging**: Errors logged, not displayed to users (prevents information leakage) -6. **Database Credentials**: Configuration file outside web root (best practice) - -## Files Changed - -1. `modules/billing/includes/payment_processor.php` - NEW -2. `modules/billing/api/capture_order.php` - MODIFIED -3. `modules/billing/payment_success.php` - MODIFIED -4. `modules/billing/webhook.php` - MODIFIED -5. `modules/billing/cart.php` - MODIFIED - -## Known Limitations - -1. **Config file required**: System requires `includes/config.inc.php` to be created from .orig template -2. **Multi-item cart matching**: If cart has multiple items, all are processed together (could improve to match specific invoice_id) -3. **No transaction rollback**: If order creation fails, invoice may still be marked paid (could improve with DB transactions) - -## Future Enhancements - -1. Add database transactions for atomic invoice→order operations -2. Improve invoice matching in process_payment_record (more specific matching) -3. Add unit tests for payment processing logic -4. Add admin UI for viewing/managing invoice-order relationships -5. Add email notifications for payment confirmations diff --git a/Panel/modules/billing/COLUMN_RENAME_SUMMARY.md b/Panel/modules/billing/COLUMN_RENAME_SUMMARY.md deleted file mode 100644 index 0bce43fa..00000000 --- a/Panel/modules/billing/COLUMN_RENAME_SUMMARY.md +++ /dev/null @@ -1,110 +0,0 @@ -# Column Rename: finish_date → end_date - -## Overview -Renamed the `finish_date` column to `end_date` across the entire billing module for better semantic clarity. The column represents when a server's subscription ends/expires, so "end_date" is more descriptive. - -## Files Modified - -### Database Schema -1. **module.php** - Line 77 - - Updated schema definition: `finish_date` DATETIME NULL → `end_date` DATETIME NULL - -2. **migration_to_invoices.sql** - - Line 26: Updated AFTER clause in ADD COLUMN statement - - Lines 49-60: Updated column conversion logic from VARCHAR to DATETIME - - All references to the column name updated - -### PHP Application Code -3. **cron-shop.php** (19 occurrences) - - Lines 78-80: Updated query conditions checking end_date IS NOT NULL - - Lines 97, 121, 124: Updated email notification date formatting - - Lines 142, 150-151: Updated suspension query conditions - - Lines 218, 226-227: Updated deletion query conditions - - Lines 283, 288: Updated legacy code comments and queries - - Lines 301, 304: Updated developer notes - - Lines 336, 341: Updated suspension logic - - Line 395: Updated final cleanup query - -4. **cart.php** (14 occurrences) - - Lines 89-106: Updated variable names from $finish_date to $end_date - - Line 111: Updated column existence check - - Lines 117, 119, 121, 127: Updated SQL UPDATE statements - - Line 148-149: Updated audit logging - -5. **my_account.php** (4 occurrences) - - Line 128: Updated SELECT query field - - Line 328: Updated display formatting (3 references in same line) - -6. **my_servers.php** (2 occurrences) - - Line 43: Updated SQL comment - - Line 44: Updated column alias - -7. **admin_invoices.php** (1 occurrence) - - Line 99: Updated display column - -8. **add_to_cart.php** (10 occurrences) - - Lines 134-151: Updated variable names, column checks, INSERT queries, logging - -9. **create_servers.php** (12 occurrences) - - Line 244: Updated condition check - - Lines 295-296: Updated comments - - Lines 301-330: Updated variable names in date calculation logic - - Line 342: Updated SET clause in UPDATE query (2 references) - -10. **payment_success.php** (11 occurrences) - - Lines 35-102: Updated all references in payment processing logic - - Variable renamed: $finish_date_val → $end_date_val - - Updated column existence checks and SQL generation - -### Documentation -11. **INVOICE_SYSTEM.md** (6 occurrences) - - Line 27: Updated field description - - Line 67: Updated workflow documentation - - Line 74: Updated renewal process - - Line 84: Updated expiration logic - - Line 113: Updated payment completion notes - - Line 124: Updated My Account display notes - -12. **MIGRATION_SUMMARY.md** (4 occurrences) - - Line 11: Updated changelog entry - - Line 18: Updated bug fix description - - Lines 30, 36: Updated cron process descriptions - - Line 87: Updated SQL schema example - - Line 141: Updated verification notes - -## Database Impact - -### For Fresh Installations -- New installations will create the `ogp_billing_orders` table with `end_date` DATETIME NULL - -### For Existing Installations -- Run the updated `migration_to_invoices.sql` script -- The script will handle the column rename automatically using dynamic SQL: - ```sql - -- Checks if column exists as 'finish_date' and renames to 'end_date' - -- Then converts data type from VARCHAR to DATETIME - ``` - -## Testing Checklist -- [x] Module schema updated (module.php) -- [x] Migration script updated (migration_to_invoices.sql) -- [x] All PHP files using the column updated -- [x] All SQL queries updated -- [x] All variable names updated -- [x] All comments and documentation updated -- [x] Verified no remaining `finish_date` references (except log files) - -## Backwards Compatibility -⚠️ **BREAKING CHANGE**: This rename requires running the migration script on existing databases. - -**Migration Path:** -1. Backup database -2. Run updated `migration_to_invoices.sql` -3. The script will automatically rename `finish_date` to `end_date` -4. Verify column exists: `SHOW COLUMNS FROM ogp_billing_orders LIKE 'end_date';` - -## Notes -- Log files may still contain old references to `finish_date` - this is expected and harmless -- The semantic meaning of the column is unchanged (server expiration date) -- All date calculations remain identical -- No functional changes, only naming improvement for clarity diff --git a/Panel/modules/billing/COUPON_SYSTEM.md b/Panel/modules/billing/COUPON_SYSTEM.md deleted file mode 100644 index 8fe1b4ec..00000000 --- a/Panel/modules/billing/COUPON_SYSTEM.md +++ /dev/null @@ -1,364 +0,0 @@ -# Coupon System Documentation - -## Overview - -The billing module now includes a comprehensive coupon system that allows administrators to create discount codes that customers can apply to their orders. The system supports: - -- **Percentage-based discounts** (e.g., 10%, 25%, 50% off) -- **One-time or permanent discounts** (one-time applies to first invoice only, permanent applies to all renewals) -- **Game-specific filtering** (apply coupons to all games or specific games only) -- **Usage limits** (optional maximum number of uses per coupon) -- **Expiration dates** (optional expiry date for time-limited promotions) -- **Automatic usage tracking** (system tracks how many times each coupon has been used) - -## Database Schema - -### Table: `ogp_billing_coupons` - -The main coupon table stores all coupon definitions: - -```sql -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', - `max_uses` INT(11) DEFAULT NULL COMMENT 'NULL for unlimited', - `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`) -); -``` - -### Updated Tables - -#### `ogp_billing_invoices` -Added columns: -- `coupon_id` INT(11) - Links to the coupon used -- `discount_amount` DECIMAL(10,2) - Actual discount amount applied - -#### `ogp_billing_orders` -Added columns: -- `coupon_id` INT(11) - Links to the coupon used (for permanent discounts) -- `discount_amount` DECIMAL(10,2) - Discount amount for renewals - -## Installation - -1. **Run the SQL migration:** - ```bash - mysql -u [username] -p [database_name] < modules/billing/create_coupons_table.sql - ``` - -2. **Verify installation:** - - Check that the `ogp_billing_coupons` table exists - - Verify that `coupon_id` and `discount_amount` columns were added to both `ogp_billing_invoices` and `ogp_billing_orders` - -## Admin Interface - -### Accessing Coupon Management - -1. Log in as an administrator -2. Navigate to `/modules/billing/admin.php` -3. Click on "Manage Coupons" button -4. Or go directly to `/modules/billing/admin_coupons.php` - -### Creating a New Coupon - -1. On the Manage Coupons page, scroll to "Add New Coupon" section -2. Fill in the required fields: - - **Coupon Code**: Unique alphanumeric code (e.g., "SUMMER2025", "WELCOME10") - - **Display Name**: User-friendly name shown in admin interface - - **Description**: Internal notes about the coupon - - **Discount Percentage**: Number between 0-100 (e.g., 25 for 25% off) - - **Usage Type**: - - **One Time**: Discount applies only to the first invoice - - **Permanent**: Discount applies to initial order AND all future renewals - - **Apply To**: - - **All Games**: Works for any game server - - **Specific Games**: Works only for selected games - - **Maximum Uses**: Optional limit on total uses (blank = unlimited) - - **Expiration Date**: Optional expiry date (blank = never expires) - -3. Click "Add Coupon" to save - -### Example Coupons - -#### Welcome Discount (One-Time, All Games) -``` -Code: WELCOME10 -Name: Welcome 10% Off -Discount: 10% -Usage Type: One Time -Apply To: All Games -Max Uses: (unlimited) -Expires: (none) -``` - -#### Arma Series Promotion (Permanent, Specific Games) -``` -Code: ARMA25 -Name: Arma Series 25% Off -Discount: 25% -Usage Type: Permanent -Apply To: Specific Games - - arma2_win32 - - arma2oa_win32 - - arma3_linux32 - - arma3_linux64 - - arma3_win64 - - arma-reforger_linux64 - - arma-reforger_win64 -Max Uses: 100 -Expires: 2025-12-31 -``` - -### Editing Coupons - -1. On the Manage Coupons page, find the coupon in the list -2. Click the "Edit" button -3. Modify any fields (except code uniqueness is enforced) -4. Click "Save Changes" - -### Deactivating Coupons - -1. Click "Edit" on the coupon -2. Uncheck the "Active" checkbox -3. Click "Save Changes" - -Note: Deactivating prevents new uses but doesn't affect existing orders. - -### Deleting Coupons - -1. Find the coupon in the list -2. Click "Delete" button -3. Confirm the deletion - -Warning: This permanently removes the coupon. Orders that used it will retain the discount but lose the coupon reference. - -## Customer Usage - -### Applying a Coupon - -1. Customer adds items to cart at `/modules/billing/cart.php` -2. In the coupon section, enter coupon code in the input field -3. Click "Apply Coupon" -4. If valid, a success message appears showing: - - Coupon code - - Discount percentage - - Whether it's one-time or permanent -5. Cart totals update automatically with discounted prices -6. Proceed to checkout with PayPal as normal - -### Coupon Validation - -The system validates: -- ✅ Code exists and is active -- ✅ Coupon hasn't expired -- ✅ Usage limit hasn't been reached -- ✅ Game matches filter (if game-specific) - -Error messages shown if: -- ❌ Code is invalid or expired -- ❌ Usage limit reached -- ❌ Coupon doesn't apply to games in cart - -### Removing a Coupon - -1. On cart page, click "Remove" button next to active coupon -2. Cart prices revert to original amounts - -## Coupon Behavior - -### One-Time Coupons - -- Applied to the initial invoice only -- When order is renewed, renewal invoice uses original price -- Coupon is cleared from session after first payment -- Example: "WELCOME10" gives 10% off first month only - -### Permanent Coupons - -- Applied to initial invoice AND stored in order record -- When order is renewed, the discount is automatically applied to renewal invoices -- Coupon stays associated with the order forever -- Example: "VIP50" gives 50% off forever for that specific server - -### Game Filtering - -#### All Games -- Coupon applies to any game server in the cart -- All cart items receive the discount - -#### Specific Games -- Coupon checks each cart item's `home_name` field -- Only matching games receive the discount -- Uses partial string matching (e.g., "arma3" matches "arma3_linux64") -- Non-matching games show original price - -Example: -``` -Cart contains: -1. Arma 3 Server → ARMA25 coupon applies (25% off) -2. Minecraft Server → ARMA25 doesn't apply (full price) -3. Arma Reforger → ARMA25 applies (25% off) - -Total discount = 25% off Arma servers only -``` - -## Technical Implementation - -### Session Storage - -Coupons are stored in `$_SESSION['applied_coupon']` when applied: -```php -$_SESSION['applied_coupon'] = [ - 'coupon_id' => 1, - 'code' => 'ARMA25', - 'discount_percent' => 25.00, - 'usage_type' => 'permanent', - 'game_filter_type' => 'specific_games', - 'game_filter_list' => '["arma3_linux64","arma2_win32"]', - // ... other fields -]; -``` - -### Cart Calculation - -In `cart.php`, the `couponAppliesTo()` function checks if a coupon applies to a specific game: - -```php -function couponAppliesTo($coupon, $game_name) { - if (!$coupon || $coupon['game_filter_type'] === 'all_games') { - return true; - } - - if ($coupon['game_filter_type'] === 'specific_games') { - $allowed_games = json_decode($coupon['game_filter_list'], true); - foreach ($allowed_games as $allowed_game) { - if (stripos($game_name, $allowed_game) !== false) { - return true; - } - } - } - - return false; -} -``` - -Discount calculation: -```php -$rowtotal = $row['amount'] * $row['qty'] * $row['max_players']; - -if ($applied_coupon && couponAppliesTo($applied_coupon, $row['home_name'])) { - $discountPercent = floatval($applied_coupon['discount_percent']); - $itemDiscount = ($rowtotal * $discountPercent) / 100; - $rowtotal = $rowtotal - $itemDiscount; -} -``` - -### Payment Processing - -In `api/capture_order.php`, when PayPal payment completes: - -1. Coupon info is retrieved from session -2. Invoices are updated with `coupon_id` -3. Coupon usage count is incremented -4. For one-time coupons, cleared from session -5. For permanent coupons, stored in order record - -```php -// Update invoice with coupon -UPDATE ogp_billing_invoices -SET status='paid', coupon_id=?, discount_amount=? -WHERE user_id=? AND status='due' - -// Increment usage count -UPDATE ogp_billing_coupons -SET current_uses = current_uses + 1 -WHERE coupon_id = ? - -// For permanent coupons, store in order -INSERT INTO ogp_billing_orders ( - ..., coupon_id, discount_amount -) VALUES ( - ..., ?, ? -) -``` - -## Display - -### Cart Page -- Shows applied coupon with code and percentage -- Displays success/error messages -- Updates prices in real-time - -### My Servers Page -- Shows original price (strikethrough) -- Shows discounted price (bold) -- Shows coupon code and percentage (green text) - -### Admin Invoices Page -- Same display as My Servers -- Visible to administrators for all orders - -## Troubleshooting - -### Coupon not applying -- Check if code is typed correctly (case-sensitive) -- Verify coupon is active in admin panel -- Check expiration date hasn't passed -- Verify usage limit hasn't been reached -- For game-specific coupons, ensure game matches filter - -### Discount not showing after payment -- Check `discount_amount` column exists in both tables -- Verify coupon_id was saved to invoice/order -- Clear browser cache and refresh page - -### Permanent coupon not applying to renewals -- Verify `usage_type` is set to "permanent" -- Check order record has `coupon_id` populated -- Ensure renewal invoice creation copies coupon from order - -## Security Considerations - -1. **Code uniqueness**: System enforces unique coupon codes -2. **Usage tracking**: Prevents abuse by tracking total uses -3. **Expiration**: Automatic validation prevents expired coupon use -4. **Admin-only creation**: Only admins can create/edit coupons -5. **SQL injection protection**: All inputs are sanitized with `mysqli_real_escape_string()` -6. **CSRF protection**: Admin forms include CSRF tokens - -## Future Enhancements - -Potential features for future development: -- Minimum purchase amount requirements -- First-time customer restrictions -- User-specific coupons (assign to individual users) -- Combination rules (allow/prevent stacking) -- Auto-generated unique codes for campaigns -- Email notification when coupon is used -- Analytics dashboard for coupon performance -- Referral system integration - -## Support - -For issues or questions: -1. Check the troubleshooting section above -2. Review error logs in `/modules/billing/logs/` -3. Verify database schema matches documentation -4. Contact system administrator - ---- - -**Last Updated**: 2025-10-29 -**Version**: 1.0 -**Module**: Billing/Coupons diff --git a/Panel/modules/billing/FIXES_APPLIED.md b/Panel/modules/billing/FIXES_APPLIED.md deleted file mode 100644 index 760ca498..00000000 --- a/Panel/modules/billing/FIXES_APPLIED.md +++ /dev/null @@ -1,248 +0,0 @@ -# Billing Module Fixes - Complete Report - -**Date**: November 10, 2025 -**Branch**: copilot/fix-billing-module-errors -**Status**: ✅ COMPLETE - -## Issues Resolved - -### 1. Critical Syntax Error in cart.php ✅ - -**Problem**: -- cart.php had a missing closing brace on line 98 (coupon validation logic) -- This caused a complete failure of the cart page -- PHP parser error: "Unclosed '{' on line 98" -- Even debug mode (cart.php?debug_cart=1) failed - -**Root Cause**: -- The `else` block starting at line 107 (handling database connection for coupon validation) was not properly closed -- The if statement on line 113 (`if ($coupon_result && mysqli_num_rows($coupon_result) === 1)`) was inside the else block -- Missing closing brace after the coupon validation logic completed - -**Fix Applied**: -- Added missing closing brace at line 181 -- Properly closes the else block from line 107 -- Brace structure now balances correctly (22 opening, 22 closing) - -**Verification**: -```bash -$ php -l cart.php -No syntax errors detected in cart.php -``` - -```bash -$ cat data/debug_cart.log -[2025-11-10 03:16:07] SHUTDOWN: no error -``` - ---- - -### 2. VS Code "Undefined Variable" Warnings ✅ - -**Problem**: -- VS Code showed warnings: "$table_prefix is unassigned" -- Similar warnings for $db_host, $db_user, $db_pass, $db_name -- These warnings appeared even though config.inc.php was properly included -- Affected developer experience and code review - -**Root Cause**: -- IDEs like VS Code don't trace through dynamic `require_once` includes -- Variables defined in config.inc.php were not visible to static analysis -- This is a limitation of IDE static analysis, not an actual code error - -**Fix Applied**: -- Added PHPDoc `@var` annotations after config.inc.php includes -- Annotations help IDEs understand variable scope -- Pattern used: -```php -// Variables from config.inc.php (helps IDEs understand scope) -/** @var string $db_host Database host */ -/** @var string $db_user Database user */ -/** @var string $db_pass Database password */ -/** @var string $db_name Database name */ -/** @var string $table_prefix Table prefix for database tables */ -``` - -**Files Updated** (16 total): - -**Main Website Files**: -1. cart.php -2. add_to_cart.php -3. admin_coupons.php -4. my_servers.php -5. my_account.php -6. renew_server.php -7. forgot_password.php -8. reset_password.php -9. login.php -10. register.php -11. serverlist.php -12. payment_success.php -13. order.php - -**Include Files**: -14. includes/admin_auth.php -15. includes/payment_processor.php -16. includes/menu.php - -**Coverage**: 16 out of 25 files using $table_prefix now have PHPDoc annotations (64%) - ---- - -### 3. Housekeeping ✅ - -**Added to .gitignore**: -- `modules/billing/data/*.log` - Prevents debug logs from being committed - ---- - -## Validation Results - -### Syntax Validation -- ✅ All 36 PHP files in modules/billing/ pass syntax check -- ✅ No parse errors detected -- ✅ All brace pairs balanced correctly - -### Functional Testing -- ✅ cart.php loads without errors -- ✅ Debug mode (cart.php?debug_cart=1) works correctly -- ✅ Debug log shows "no error" status -- ✅ Shutdown function executes properly - -### Code Quality -- ✅ PHPDoc annotations added for IDE support -- ✅ All key user-facing files updated -- ✅ No changes to business logic -- ✅ Minimal, surgical changes only - ---- - -## Files Modified - -### Commit 1: Fix cart.php syntax error and add PHPDoc hints -- modules/billing/cart.php (syntax fix + PHPDoc) -- modules/billing/add_to_cart.php (PHPDoc) -- modules/billing/admin_coupons.php (PHPDoc) -- modules/billing/my_servers.php (PHPDoc) -- modules/billing/my_account.php (PHPDoc) -- modules/billing/renew_server.php (PHPDoc) -- modules/billing/forgot_password.php (PHPDoc) -- modules/billing/reset_password.php (PHPDoc) - -### Commit 2: Add PHPDoc hints to additional files -- modules/billing/login.php (PHPDoc) -- modules/billing/register.php (PHPDoc) -- modules/billing/serverlist.php (PHPDoc) -- modules/billing/payment_success.php (PHPDoc) -- modules/billing/order.php (PHPDoc) -- modules/billing/includes/admin_auth.php (PHPDoc) -- modules/billing/includes/payment_processor.php (PHPDoc) -- modules/billing/includes/menu.php (PHPDoc) - -### Commit 3: Add billing data logs to gitignore -- .gitignore (added modules/billing/data/*.log) - -**Total Files Changed**: 17 files -**Total Lines Changed**: ~120 lines (mostly documentation) -**Breaking Changes**: None -**Business Logic Changes**: None - ---- - -## Testing Recommendations - -To fully test the cart functionality in a live environment: - -1. **Configure Database Connection**: - - Edit `modules/billing/includes/config.inc.php` - - Set correct database credentials - - Ensure $table_prefix matches your panel installation - -2. **Test Basic Cart Access**: - ``` - http://yoursite.com/modules/billing/cart.php - ``` - - Should redirect to login if not authenticated - - Should show cart after login - -3. **Test Debug Mode**: - ``` - http://yoursite.com/modules/billing/cart.php?debug_cart=1 - ``` - - Should display detailed error messages - - Check data/debug_cart.log for shutdown messages - -4. **Test Coupon Functionality**: - - Add items to cart - - Apply a test coupon code - - Verify discount calculation - - Verify coupon validation (expiry, usage limits, game filters) - -5. **Test PayPal Integration**: - - Complete checkout flow - - Verify PayPal buttons render - - Test payment capture - ---- - -## Notes for Developers - -### About $table_prefix Variable -- Defined in `modules/billing/includes/config.inc.php` -- Default value: `"gsp_"` -- Used for database table prefixes -- Must match the panel installation's table prefix - -### About PHPDoc Annotations -- These are ONLY for IDE support -- Do NOT change runtime behavior -- Safe to add to all files that include config.inc.php -- Pattern is consistent across all files - -### Standalone Architecture -The billing module is designed to be standalone and relocatable: -- Uses ONLY standard PHP libraries (mysqli, json, curl, session) -- Does NOT include panel files (like includes/functions.php) -- Connects directly to MySQL using mysqli_connect() -- Can be deployed on same machine as panel OR external web host -- Sessions are separate: "opengamepanel_web" namespace - ---- - -## Additional Notes - -### Files That Could Benefit from PHPDoc (Not Critical) -These files use $table_prefix but don't have PHPDoc annotations yet: -- admin_invoices.php (4 uses) -- adminserverlist.php (8 uses) -- cart_old.php (4 uses) -- check_table.php (4 uses) -- create_servers.php (4 uses) - NOTE: This is a panel module, uses OGP_DB_PREFIX -- cron-shop.php (30 uses) - NOTE: This is a panel cron job -- server_status.php (4 uses) -- test_db_connection.php (9 uses) - -These can be updated in a future enhancement if needed. - -### create_servers.php Note -This file is actually a PANEL module (not a standalone billing website file): -- Uses panel's $db object -- Includes panel files (includes/lib_remote.php) -- Uses OGP_DB_PREFIX placeholder in some queries -- Inconsistently uses {$table_prefix} in a few places -- Should eventually be updated to use OGP_DB_PREFIX consistently - ---- - -## Conclusion - -✅ **All issues resolved successfully** - -The billing module is now functional with: -1. cart.php working correctly (syntax error fixed) -2. VS Code warnings suppressed (PHPDoc added) -3. Debug logging configured properly -4. All files validated for syntax correctness - -The changes are minimal, surgical, and follow the repository guidelines for standalone billing module architecture. - diff --git a/Panel/modules/billing/GAME_DOCS_TODO_REFERENCE.md b/Panel/modules/billing/GAME_DOCS_TODO_REFERENCE.md deleted file mode 100644 index 78cccedf..00000000 --- a/Panel/modules/billing/GAME_DOCS_TODO_REFERENCE.md +++ /dev/null @@ -1,137 +0,0 @@ -# Game Documentation TODO System - Quick Reference - -## System Overview -All game documentation folders now have a "complete" status field. Incomplete documentation displays with "TODO: " prefix on the docs.php page for easy visual identification. - -## Current Status (December 19, 2024) - -### ✅ Complete Documentation (1 game) -- **Minecraft Server** - Full comprehensive documentation with all sections - -### ❌ Incomplete Documentation (146 games) -All other games display with "TODO: " prefix and need comprehensive research - -## Priority Order for Completion - -### PHASE 2: ARMA Family + DayZ (NEXT - HIGH PRIORITY) -1. **Arma 3** - Modern ARMA platform, highly popular -2. **Arma 2: Operation Arrowhead** - Required for DayZ Mod -3. **Arma 2** - Base game (if separate from OA) -4. **Arma 2: Combined Operations** - ARMA2 + OA combo for DayZ Mod -5. **DayZ Standalone** - Standalone survival game -6. **DayZ Mod** (if exists) - Original mod version - -**Research Sources for ARMA/DayZ:** -- Bohemia Interactive Wiki (https://community.bistudio.com/wiki) -- LGSM scripts (LinuxGSM game configs) -- r/arma, r/dayzservers Reddit communities -- BI Forums (https://forums.bohemia.net/) -- DayZ Forums (https://forums.dayz.com/) -- Steam Community Guides (highly-rated) - -### PHASE 3: Popular Multiplayer Games -**Batch 1 (Counter-Strike Family):** -- Counter-Strike 1.6 -- Counter-Strike: Source -- Counter-Strike 2 -- Counter-Strike: Global Offensive - -**Batch 2 (Survival/Building Games):** -- Rust -- Terraria -- Valheim -- Garry's Mod -- ARK: Survival Evolved -- 7 Days to Die - -**Batch 3 (Co-op Shooters):** -- Left 4 Dead -- Left 4 Dead 2 -- Killing Floor -- Killing Floor 2 -- Team Fortress 2 - -**Batch 4 (Tactical Shooters):** -- Insurgency -- Insurgency: Sandstorm -- Squad - -### PHASE 4: Remaining Games (50+ games) -All other game folders in alphabetical order - -## Documentation Template Requirements - -Each game must include (following Minecraft template): - -### Required Sections: -1. **Navigation Box** - Quick links to all sections with emoji icons -2. **Quick Info** - Game overview and key details in styled box -3. **Comprehensive Ports Table:** - - Port number - - Protocol (TCP/UDP) - - Purpose/Description - - Required or Optional status -4. **Firewall Configuration Examples:** - - UFW (Ubuntu/Debian) - - FirewallD (CentOS/RHEL) - - Windows Firewall - - iptables (generic Linux) -5. **Startup Parameters Section:** - - Command syntax - - Parameter explanations - - Common configurations - - Examples with descriptions -6. **Troubleshooting Section:** - - Server won't start - - Connection issues - - Performance problems - - Mod/plugin conflicts (if applicable) - - Common error messages with solutions -7. **Performance Optimization** -8. **Security Best Practices** -9. **Additional Resources** - Links to official docs, wikis, community guides -10. **Important Notes** - Warning box with critical information - -### Research Requirements: -- Search official game wikis -- Check LGSM scripts for accurate port/parameter info -- Review Steam Community guides (highly-rated) -- Check Reddit communities (r/gameservers, game-specific subs) -- Look for GitHub repos with server configs -- Include user-contributed solutions from forums -- Cite all sources used - -## How to Mark Documentation Complete - -When a game's documentation is finished: - -1. **Edit metadata.json** in the game folder: - ```json - { - "name": "Game Name", - "description": "Description", - "category": "game", - "order": 10, - "complete": true - } - ``` - -2. **Change** `"complete": false` to `"complete": true` - -3. **Verify** on docs.php - game name should no longer show "TODO: " prefix - -## Estimated Time Per Game -- **Research:** 15-30 minutes (official docs, wikis, LGSM, Reddit, Steam) -- **Writing:** 20-30 minutes (following template structure) -- **Testing/Review:** 5-10 minutes -- **Total:** 40-70 minutes per game for comprehensive documentation - -## Files Modified in TODO System Implementation -- `modules/billing/docs.php` - Added TODO prefix logic -- `modules/billing/docs/*/metadata.json` - Added complete field to 146 files -- `update_metadata_complete.ps1` - Batch update script -- `RECENT_FIXES_SUMMARY.md` - Updated with TODO system details -- `GAME_DOCS_TODO_REFERENCE.md` - This reference file - -## Next Immediate Action -Begin Phase 2: Research and complete ARMA family + DayZ documentation (6 games total) diff --git a/Panel/modules/billing/INVOICE_FIRST_FLOW.md b/Panel/modules/billing/INVOICE_FIRST_FLOW.md deleted file mode 100644 index 1fb57d52..00000000 --- a/Panel/modules/billing/INVOICE_FIRST_FLOW.md +++ /dev/null @@ -1,190 +0,0 @@ -# Invoice-First Billing Flow - -## Overview -The billing system now follows an **invoice-first** workflow where invoices are created BEFORE orders. Orders are only created after successful payment. - -## Workflow - -### 1. Add to Cart (order.php → add_to_cart.php) -**What happens:** -- User clicks "Add to Cart" button on order page -- System creates a **billing_invoices** record with: - - `status` = 'due' - - `order_id` = 0 (no order exists yet) - - All server details (service_id, home_name, ip, max_players, passwords, etc.) - - Customer details (name, email from ogp_users) - - Pricing (amount, qty, invoice_duration) - - `due_date` = now + 3 days - -**Database changes:** -- INSERT into `ogp_billing_invoices` -- NO changes to `ogp_billing_orders` (order doesn't exist yet) - -### 2. Cart Display (cart.php) -**What shows:** -- Query: `SELECT * FROM ogp_billing_invoices WHERE status = 'due' AND user_id = ?` -- Displays all **unpaid invoices** (status='due') -- Shows invoice_id, home_name, ip, max_players, amount, qty -- Free items show "Claim (Free)" button -- Paid items show PayPal button - -**Actions available:** -- Delete invoice (removes from cart, no order cleanup needed) -- Pay invoice (via PayPal or Free button) - -### 3. Payment (PayPal or Free) - -#### 3a. Free/Claim Flow (cart.php POST handler) -**When:** User clicks "Claim (Free)" or admin clicks "Create (Free)" - -**What happens:** -1. Mark invoice as paid: - - UPDATE `ogp_billing_invoices` SET status='paid', paid_date=NOW() -2. Create order record: - - Calculate end_date (qty * invoice_duration) - - INSERT into `ogp_billing_orders` with status='paid' - - Get new order_id from INSERT -3. Link invoice to order: - - UPDATE `ogp_billing_invoices` SET order_id=? WHERE invoice_id=? - -**Database changes:** -- UPDATE `ogp_billing_invoices`: status='due' → 'paid', paid_date=NOW(), order_id=(new) -- INSERT `ogp_billing_orders`: New record with status='paid', end_date calculated - -#### 3b. PayPal Flow (api/capture_order.php) -**When:** User pays via PayPal - -**What should happen:** -1. PayPal sends capture webhook -2. System marks invoice as paid (same as Free flow) -3. System creates order record (same as Free flow) -4. System links invoice to order (same as Free flow) - -**Database changes:** (Same as Free flow above) - -### 4. Server Provisioning (create_servers.php) -**What happens:** -- Cron job or manual trigger finds orders with status='paid' -- Creates actual game server (home_id) -- Updates order: status='paid' → 'installed', home_id=(assigned) - -**Database changes:** -- UPDATE `ogp_billing_orders`: status='paid' → 'installed', home_id=(assigned) - -## Status Values - -### Invoice Status -- **'due'** - Unpaid invoice (shows in cart) -- **'paid'** - Paid invoice (payment confirmed) -- **'cancelled'** - Deleted/cancelled invoice - -### Order Status -- **'paid'** - Payment confirmed, awaiting provisioning -- **'installed'** - Server provisioned and running -- **'suspended'** - Server stopped for non-payment -- **'expired'** - Service ended - -## Database Schema - -### ogp_billing_invoices (INVOICE-FIRST) -```sql -invoice_id INT AUTO_INCREMENT PRIMARY KEY -order_id INT DEFAULT 0 -- Links to order AFTER payment (0 = not yet paid) -user_id INT NOT NULL -service_id INT NOT NULL -- Server package being purchased -home_name VARCHAR(255) -- Server name -ip INT -- IP assignment -max_players INT -- Player count -remote_control_password VARCHAR(255) -- Server RCON password -ftp_password VARCHAR(255) -- FTP password -customer_name VARCHAR(255) -- Billing name -customer_email VARCHAR(255) -- Billing email -amount FLOAT(15,2) -- Total price -currency VARCHAR(3) DEFAULT 'USD' -status VARCHAR(16) DEFAULT 'due' -- 'due', 'paid', 'cancelled' -invoice_date DATETIME DEFAULT NOW() -due_date DATETIME -- Payment deadline -paid_date DATETIME -- When paid -payment_txid VARCHAR(255) -- PayPal transaction ID -payment_method VARCHAR(50) -- 'paypal', 'free', etc. -description VARCHAR(500) -- Invoice description -invoice_duration VARCHAR(16) DEFAULT 'month' -- 'month', 'year', 'day' -qty INT DEFAULT 1 -- Quantity/duration multiplier -``` - -### ogp_billing_orders (ORDER-AFTER-PAYMENT) -```sql -order_id INT AUTO_INCREMENT PRIMARY KEY -user_id INT NOT NULL -service_id INT NOT NULL -home_name VARCHAR(255) -home_id VARCHAR(255) -- Panel game server ID (after provisioning) -ip INT -max_players INT -qty INT -invoice_duration VARCHAR(16) -price FLOAT(15,2) -remote_control_password VARCHAR(255) -ftp_password VARCHAR(255) -status VARCHAR(16) DEFAULT 'paid' -- 'paid', 'installed', 'suspended', 'expired' -order_date DATETIME DEFAULT NOW() -end_date DATETIME -- Subscription expiration -payment_txid VARCHAR(255) -paid_ts DATETIME -``` - -## Key Differences from Old Flow - -### OLD (Order-First) -1. Add to cart → Create ORDER (status='in-cart') -2. View cart → Show orders WHERE status='in-cart' -3. Pay → UPDATE order status='in-cart' → 'paid' -4. Provision → UPDATE order status='paid' → 'installed' - -### NEW (Invoice-First) -1. Add to cart → Create INVOICE (status='due', order_id=0) -2. View cart → Show invoices WHERE status='due' -3. Pay → Mark invoice paid + CREATE ORDER (status='paid') + Link invoice to order -4. Provision → UPDATE order status='paid' → 'installed' - -## Benefits - -1. **Clean Separation:** Invoices = payment requests, Orders = actual services -2. **Better Audit Trail:** Invoice IDs never change, order IDs created only after payment -3. **Renewal Support:** Can create multiple invoices for same order (renewals) -4. **Cart Simplicity:** Cart only shows unpaid invoices (single source of truth) -5. **Payment History:** All payments have invoice records, even free ones - -## Migration Notes - -**Existing orders with status='in-cart' need to be migrated:** -```sql --- Convert existing cart items to invoices -INSERT INTO ogp_billing_invoices ( - order_id, user_id, service_id, home_name, ip, max_players, - remote_control_password, ftp_password, customer_name, customer_email, - amount, status, invoice_duration, qty, description -) -SELECT - 0, -- No order exists yet - o.user_id, - o.service_id, - o.home_name, - o.ip, - o.max_players, - o.remote_control_password, - o.ftp_password, - CONCAT(u.users_fname, ' ', u.users_lname), - u.users_email, - o.price, - 'due', -- Convert 'in-cart' to 'due' - o.invoice_duration, - o.qty, - CONCAT('Migrated cart item: ', o.home_name) -FROM ogp_billing_orders o -LEFT JOIN ogp_users u ON o.user_id = u.user_id -WHERE o.status = 'in-cart'; - --- Delete old cart items (now converted to invoices) -DELETE FROM ogp_billing_orders WHERE status = 'in-cart'; -``` diff --git a/Panel/modules/billing/INVOICE_SYSTEM.md b/Panel/modules/billing/INVOICE_SYSTEM.md deleted file mode 100644 index f60daf9f..00000000 --- a/Panel/modules/billing/INVOICE_SYSTEM.md +++ /dev/null @@ -1,133 +0,0 @@ -# Billing System - Invoice-Based Architecture - -## Overview -The billing system now uses a **dual-table architecture** separating orders (ongoing services) from invoices (payment records). - -## Database Tables - -### 1. `ogp_billing_services` -**Purpose:** Available game server packages/products -**Key Fields:** -- `service_id` - Unique identifier -- `service_name` - Display name -- `remote_server_id` - Target server(s) -- `price_monthly`, `price_year` - Pricing tiers -- `enabled` - Availability flag - -### 2. `ogp_billing_orders` (formerly just cart items) -**Purpose:** Active game server instances (ongoing services) -**Key Fields:** -- `order_id` - Unique identifier -- `user_id` - Owner -- `service_id` - Product reference -- `home_id` - Panel game home ID (after provisioning) -- `home_name` - Server name -- `status` - Current state (see Status Flow below) -- `order_date` - When created -- `end_date` - Expiration date -- `payment_txid` - Last payment transaction -- `paid_ts` - Last payment timestamp - -**Status Values:** -- `in-cart` - User added to cart, not yet paid -- `paid` - Payment received, awaiting provisioning -- `installed` - ✅ Server provisioned and running -- `suspended` - Server stopped due to non-payment -- `expired` - Service ended -- `renew` - Renewal pending in cart - -### 3. `ogp_billing_invoices` (NEW) -**Purpose:** Payment records (one invoice per payment) -**Key Fields:** -- `invoice_id` - Unique identifier -- `order_id` - Links to the server order -- `user_id` - Customer -- `customer_name` - Full name -- `customer_email` - Email address -- `amount` - Total due -- `currency` - USD, EUR, etc. -- `status` - `unpaid` or `paid` -- `invoice_date` - When created -- `due_date` - Payment deadline -- `paid_date` - When paid -- `payment_txid` - PayPal/Stripe transaction ID -- `payment_method` - PayPal, Stripe, etc. -- `description` - Invoice line items -- `invoice_duration` - Billing period (month/year) -- `qty` - Quantity/duration multiplier - -## Workflow - -### Initial Purchase -1. User selects game server package → Creates row in `billing_orders` (status: `in-cart`) -2. System creates `billing_invoices` entry (status: `unpaid`, linked to order_id) -3. Cart page shows unpaid invoices -4. User pays → Invoice status becomes `paid`, order status becomes `paid` -5. Provisioning happens → Order status becomes `installed` -6. Server is active until `end_date` - -### Renewal Process -1. User clicks "Renew" on active server (My Account page) -2. System creates NEW invoice in `billing_invoices` (status: `unpaid`, same order_id) -3. Cart shows the unpaid renewal invoice -4. User pays → Invoice status becomes `paid` -5. Order `end_date` is extended by the renewal period - -### Cron Automation (`cron-shop.php`) -The cron job checks invoice status to manage servers: - -**7 days before expiration:** -- Check if order has unpaid invoice for upcoming period -- If NO unpaid invoice exists → Create one (status: `unpaid`) -- Email customer about upcoming renewal - -**On expiration (end_date reached):** -- Check if order has unpaid invoice -- If YES → Suspend server (stop, disable FTP, unassign from user) -- Order status → `suspended` - -**7 days after suspension:** -- If still unpaid → Delete server permanently -- Order status → `expired` - -## Key Advantages - -1. **Clear Payment History:** Each invoice represents one payment -2. **Audit Trail:** Can track when/how much each renewal cost -3. **Flexible Pricing:** Can adjust price per renewal (discounts, promotions) -4. **Multi-Payment Support:** One order can have many invoices -5. **Accurate Status:** Order status reflects server state, invoice status reflects payment -6. **No Race Conditions:** Webhook updates invoice, provisioning updates order - -## Cart Logic - -**Cart page displays:** -- All invoices with `status = 'unpaid'` for the current user -- Groups by order_id to show which server each invoice is for -- Total amount = SUM of all unpaid invoice amounts - -**After payment:** -- Invoice `status` → `paid` -- Invoice `paid_date` → NOW() -- Invoice `payment_txid` → transaction ID from PayPal/Stripe -- Order `status` → `paid` (if new order) or `end_date` extended (if renewal) - -## My Account Logic - -**Show Invoices Section:** -- Group invoices by status (unpaid, paid, overdue) -- Display invoice_date, amount, status -- Link to view invoice details - -**Show Current Servers Section:** -- Display orders with `status = 'installed'` -- Show end_date (expiration) -- "Renew" button creates new invoice - -## Migration Notes - -- Run `migration_to_invoices.sql` on existing installations -- Creates `billing_invoices` table -- Adds missing columns to `billing_orders` -- Migrates existing paid orders to have invoices -- Removes obsolete `billing_carts` table diff --git a/Panel/modules/billing/LOGGING_CHANGES_SUMMARY.md b/Panel/modules/billing/LOGGING_CHANGES_SUMMARY.md deleted file mode 100644 index ce923abb..00000000 --- a/Panel/modules/billing/LOGGING_CHANGES_SUMMARY.md +++ /dev/null @@ -1,223 +0,0 @@ -# PayPal Payment Flow Logging Enhancement - Summary - -## Problem Addressed - -Users were experiencing intermittent errors when clicking "Pay from PayPal" button: -- **JSON parsing errors** -- **HTTP ERROR 500** -- **"Currently unable to handle this request"** errors - -These errors would "flip-flop" between different types, making diagnosis difficult without proper logging. - -## Solution Implemented - -Added comprehensive logging throughout the entire PayPal payment flow to capture: -- All request/response data -- Error details with full context -- Unique request IDs for tracking -- Database operations and results -- Client-side JavaScript errors - -## What Changed - -### Modified Files - -1. **`api/create_order.php`** - Enhanced with comprehensive logging - - Logs every step of order creation - - Captures request data, OAuth process, PayPal API calls - - Returns request IDs in error messages for tracking - - Logs to: `logs/paypal_create_order.log` - -2. **`api/capture_order.php`** - Enhanced existing logging - - Logs payment capture process - - Tracks database operations (invoice updates, order creation) - - Captures all error conditions - - Logs to: `logs/paypal_capture.log` - -3. **`cart.php`** - Improved client-side error handling - - Better error messages with reference IDs - - Enhanced console logging for debugging - - Sends errors to server for centralized logging - - Better user feedback during payment process - -4. **`api/log_error.php`** - NEW: Client error logging endpoint - - Captures JavaScript errors from browser - - Logs to: `logs/client_errors.log` - -### New Files - -1. **`PAYPAL_DEBUGGING_GUIDE.md`** - Comprehensive debugging guide - - How to read logs - - Common issues and solutions - - Request flow documentation - - Monitoring commands - -2. **`QUICK_DEBUG_REFERENCE.md`** - Quick reference card - - Common commands - - Error patterns - - Quick fixes - - Troubleshooting checklist - -## How to Use - -### When an error occurs: - -1. **User will see an error message with a reference ID**, for example: - ``` - Failed to create order: API error 500 (Ref: req_abc123) - ``` - -2. **Search the logs for that reference ID**: - ```bash - cd /home/runner/work/GSP/GSP/modules/billing/logs - grep "req_abc123" paypal_create_order.log - ``` - -3. **Review the full request flow** to identify where it failed - -4. **Refer to the debugging guide** for common solutions - -### Monitor logs in real-time: - -```bash -cd /home/runner/work/GSP/GSP/modules/billing/logs -tail -f paypal_*.log -``` - -### Check for errors: - -```bash -cd /home/runner/work/GSP/GSP/modules/billing/logs -grep -i error paypal_create_order.log -grep -i failed paypal_capture.log -``` - -## Log Files - -All logs are written to: `/modules/billing/logs/` - -| Log File | Purpose | When Created | -|----------|---------|--------------| -| `paypal_create_order.log` | Order creation requests | When user clicks "Pay with PayPal" | -| `paypal_capture.log` | Payment capture process | After PayPal approval, during payment capture | -| `client_errors.log` | JavaScript/browser errors | When browser encounters errors | - -## Request Tracking - -Each request has a unique ID: -- **Create order**: `req_XXXXXXXXXXXXX` -- **Capture order**: `cap_XXXXXXXXXXXXX` - -These IDs: -- Appear in error messages shown to users -- Are logged in every log entry for that request -- Can be used to track a request through the entire flow - -## Log Entry Format - -``` -[TIMESTAMP] [REQUEST_ID] LOG_LABEL -key => value -key => value --------------------------------------------------------------------------------- -``` - -Example: -``` -[2025-10-29 21:30:00] [req_abc123] OAUTH_SUCCESS -token_length => 1024 --------------------------------------------------------------------------------- -``` - -## What Gets Logged - -### Create Order Flow (`api/create_order.php`): -- ✓ Incoming request data (amount, currency, items) -- ✓ JSON parsing status -- ✓ OAuth token acquisition -- ✓ PayPal order creation request/response -- ✓ All error conditions with full details - -### Capture Order Flow (`api/capture_order.php`): -- ✓ Payment capture request -- ✓ OAuth process -- ✓ Database connection status -- ✓ Invoice update queries and results -- ✓ Order creation/renewal operations -- ✓ All error conditions with full details - -### Client-Side (`cart.php` → `log_error.php`): -- ✓ JavaScript errors -- ✓ PayPal SDK errors -- ✓ Network failures -- ✓ JSON parsing errors - -## Benefits - -1. **Full Visibility**: Every step of payment flow is now logged -2. **Easy Troubleshooting**: Request IDs link user reports to log entries -3. **Root Cause Analysis**: Can identify exactly where and why failures occur -4. **Pattern Detection**: Can identify if errors are consistent or intermittent -5. **Better User Experience**: Users get reference IDs to report issues - -## Next Steps - -1. **Monitor the logs** after deploying this change -2. **Analyze error patterns** to identify the root cause -3. **Review common errors** in the debugging guide -4. **Fix underlying issues** once identified - -## Documentation - -- **Full Guide**: `PAYPAL_DEBUGGING_GUIDE.md` -- **Quick Reference**: `QUICK_DEBUG_REFERENCE.md` -- **This Summary**: `LOGGING_CHANGES_SUMMARY.md` - -## Testing - -The logging system has been tested and verified to work correctly. All components: -- ✓ Write to correct log files -- ✓ Include proper timestamps and request IDs -- ✓ Format data correctly -- ✓ Handle errors gracefully - -## Maintenance - -### Log Rotation - -Logs will grow over time. Consider setting up log rotation: - -```bash -# Manual rotation -cd /home/runner/work/GSP/GSP/modules/billing/logs -gzip paypal_create_order.log -mv paypal_create_order.log.gz paypal_create_order.$(date +%Y%m%d).log.gz -touch paypal_create_order.log -``` - -Or use `logrotate` (see `PAYPAL_DEBUGGING_GUIDE.md` for details). - -### Monitoring - -Set up automated monitoring to alert on: -- High error rates -- Specific error patterns (OAuth failures, DB connection issues) -- Unusual request volumes - -## Support - -If you encounter issues or need help interpreting logs: - -1. Check `PAYPAL_DEBUGGING_GUIDE.md` for common issues -2. Review `QUICK_DEBUG_REFERENCE.md` for quick fixes -3. Provide log excerpts (with request IDs) when asking for help - -## Changes Made By - -- Enhanced logging system - Added 2025-10-29 -- Documentation created - 2025-10-29 -- Testing completed - 2025-10-29 - ---- - -**The intermittent JSON/HTTP 500 errors should now be fully traceable and debuggable with this comprehensive logging system.** diff --git a/Panel/modules/billing/MIGRATION_SUMMARY.md b/Panel/modules/billing/MIGRATION_SUMMARY.md deleted file mode 100644 index b748eda3..00000000 --- a/Panel/modules/billing/MIGRATION_SUMMARY.md +++ /dev/null @@ -1,201 +0,0 @@ -# Billing System Migration Summary - -## Files Modified - -### 1. `module.php` - Database Schema -**Changes:** -- Removed all legacy `ALTER TABLE` migration queries (db_version reset to 1) -- Updated to single clean install with current schema -- Added `ogp_billing_invoices` table definition -- Added missing columns to `billing_orders`: `order_date`, `payment_txid`, `paid_ts` -- Changed `end_date` from VARCHAR to DATETIME -- Removed obsolete columns: `cart_id`, `extended` -- Removed `billing_carts` table (replaced by invoices) -- Added proper indexes for performance - -### 2. `cron-shop.php` - Server Lifecycle Automation -**Fixed Logic Errors:** -- OLD BUG: Was deleting servers with `status='paid'` or `status='installed'` if end_date was close -- NEW: Only processes servers based on **invoice payment status**, not just order status -- Now uses `billing_invoices` table to determine if payment is due - -**New 3-Step Process:** -1. **Create Renewal Invoices** (7 days before expiration) - - Find `installed` servers expiring soon - - Check if unpaid invoice exists - - If not, create renewal invoice - - Send email reminder - -2. **Suspend Servers** (on expiration with unpaid invoice) - - Find `installed` servers past end_date - - Check if they have unpaid invoices - - Stop server, disable FTP, unassign from user - - Status → `suspended` - -3. **Delete Servers** (7 days after suspension) - - Find `suspended` servers 7+ days past end_date - - Still have unpaid invoices - - Permanently delete files and database - - Status → `deleted` - -## New Files Created - -### 1. `migration_to_invoices.sql` -**Purpose:** Upgrade existing installations -**What it does:** -- Adds new columns to `billing_orders` -- Creates `billing_invoices` table -- Migrates existing paid orders to have invoice records -- Removes obsolete `billing_carts` table -- Adds performance indexes - -### 2. `INVOICE_SYSTEM.md` -**Purpose:** Documentation -**Contents:** -- Table schemas explained -- Workflow diagrams -- Status field definitions -- Cron automation logic -- Migration instructions - -## SQL for Fresh Install - -The `module.php` now contains clean CREATE TABLE statements for: - -### ogp_billing_services -```sql -CREATE TABLE `ogp_billing_services` ( - service_id INT AUTO_INCREMENT PRIMARY KEY, - service_name VARCHAR(255), - remote_server_id VARCHAR(255), - price_monthly FLOAT(15,4), - enabled INT DEFAULT 1, - ... [other fields] -); -``` - -### ogp_billing_orders -```sql -CREATE TABLE `ogp_billing_orders` ( - order_id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - service_id INT NOT NULL, - home_name VARCHAR(255), - home_id VARCHAR(255), - status VARCHAR(16) DEFAULT 'in-cart', - order_date DATETIME DEFAULT CURRENT_TIMESTAMP, - end_date DATETIME NULL, - payment_txid VARCHAR(255) NULL, - paid_ts DATETIME NULL, - ... [other fields] - KEY (user_id), - KEY (status), - KEY (home_id) -); -``` - -### ogp_billing_invoices (NEW) -```sql -CREATE TABLE `ogp_billing_invoices` ( - invoice_id INT AUTO_INCREMENT PRIMARY KEY, - order_id INT NOT NULL, - user_id INT NOT NULL, - customer_name VARCHAR(255), - customer_email VARCHAR(255), - amount FLOAT(15,2), - currency VARCHAR(3) DEFAULT 'USD', - status VARCHAR(16) DEFAULT 'unpaid', - invoice_date DATETIME DEFAULT CURRENT_TIMESTAMP, - due_date DATETIME NULL, - paid_date DATETIME NULL, - payment_txid VARCHAR(255), - payment_method VARCHAR(50), - description VARCHAR(500), - invoice_duration VARCHAR(16), - qty INT DEFAULT 1, - KEY (order_id), - KEY (user_id), - KEY (status), - KEY (due_date) -); -``` - -## Migration Steps for Existing Installations - -1. **Backup Database** - ```bash - mysqldump -u root -p ogp_panel > backup_before_invoice_migration.sql - ``` - -2. **Run Migration Script** - ```bash - mysql -u root -p ogp_panel < modules/billing/migration_to_invoices.sql - ``` - -3. **Verify Tables** - ```sql - SHOW TABLES LIKE 'ogp_billing%'; - -- Should show: billing_services, billing_orders, billing_invoices - - DESCRIBE ogp_billing_orders; - -- Should have: order_date, payment_txid, paid_ts, end_date (DATETIME) - - DESCRIBE ogp_billing_invoices; - -- Should exist with all invoice fields - ``` - -4. **Test Cron Job** - ```bash - cd /path/to/ogp/web - php modules/billing/cron-shop.php - ``` - -5. **Check Logs** - ```sql - SELECT * FROM ogp_logger WHERE type LIKE '%BILLING-CRON%' ORDER BY date DESC LIMIT 20; - ``` - -## Key Improvements - -1. **Accurate Server Management** - - Servers only suspended if they have **unpaid invoices** - - Active paid servers are never touched - - Clear separation between order state and payment state - -2. **Audit Trail** - - Every payment creates an invoice record - - Can track payment history per server - - Know exactly when/why server was suspended - -3. **Flexible Pricing** - - Each renewal can have different price - - Support for discounts and promotions - - Currency per invoice (multi-currency support ready) - -4. **Better Customer Experience** - - Clear invoice emails with due dates - - 7-day warning before expiration - - 7-day grace period before deletion - -## Status Field Values Reference - -### billing_orders.status -- `in-cart` - Initial state, unpaid -- `paid` - Payment received, awaiting provisioning -- `installed` - Server active and running ✅ -- `suspended` - Stopped due to non-payment -- `deleted` - Permanently removed -- `expired` - Service ended -- `renew` - Renewal in cart (legacy, now uses invoices) - -### billing_invoices.status -- `unpaid` - Invoice created, awaiting payment -- `paid` - Invoice paid successfully - -## Next Steps for Implementation - -1. Update cart.php to show invoices instead of orders -2. Update my_account.php "Renew" button to create invoices -3. Update payment success flow to mark invoices paid -4. Add invoice viewing page -5. Test full workflow: order → pay → renew → pay renewal diff --git a/Panel/modules/billing/PANEL_INTEGRATION.md b/Panel/modules/billing/PANEL_INTEGRATION.md deleted file mode 100644 index 7a3577a0..00000000 --- a/Panel/modules/billing/PANEL_INTEGRATION.md +++ /dev/null @@ -1,300 +0,0 @@ -# GSP Billing Module - Panel Integration Complete - -## Overview -The GSP billing module has been successfully integrated with the panel-side provisioning system. The standalone website handles public orders and payments, while the panel manages server provisioning. - -## Changes Made - -### 1. Navigation Configuration (`navigation.xml`) -**File:** `modules/billing/navigation.xml` - -Created XML configuration to expose billing pages in the panel: -- `provision_servers` → `create_servers.php` (admin,user) -- `my_orders` → `my_orders_panel.php` (admin,user) -- `admin_orders` → `admin_orders.php` (admin only) - -**Access URLs:** -- `home.php?m=billing&p=provision_servers` - Provision paid servers -- `home.php?m=billing&p=my_orders` - View user's orders -- `home.php?m=billing&p=admin_orders` - Admin order management - -### 2. User Order Management (`my_orders_panel.php`) -**File:** `modules/billing/my_orders_panel.php` - -User-facing page displaying paid but unprovisioned orders: -- Shows order details (service name, players, price, duration) -- "Provision Server" button for individual orders -- "Provision All My Servers" button for bulk provisioning -- Admin view includes username column -- Filters to show only `status='paid'` orders - -### 3. Server Provisioning Updates (`create_servers.php`) -**File:** `modules/billing/create_servers.php` - -Enhanced provisioning script to handle multiple workflows: - -**NEW: provision_all Support** -```php -if (isset($_POST['provision_all'])) { - // Query all paid orders for user - // Process each in foreach loop -} -``` - -**NEW: provision_single Support** -```php -if (isset($_POST['provision_single']) && $_POST['order_id']) { - // Query specific order_id - // Process single order -} -``` - -**Improvements:** -- Added provisioning counters (`$provisioned_count`, `$failed_count`) -- Success message shows count of provisioned servers -- Auto-redirect to game monitor after 3 seconds -- Better error handling for missing order_id -- Clear feedback messages for all scenarios - -### 4. Admin Order Management (`admin_orders.php`) -**File:** `modules/billing/admin_orders.php` - -Comprehensive admin interface for managing all orders: - -**Features:** -- View all orders across all users -- Filter by status (in-cart, paid, installed, invoiced, suspended, deleted) -- Search by order ID, username, email, server name -- Bulk actions: - - Provision multiple servers at once - - Activate (set to paid) - - Suspend orders - - Delete orders -- Quick links to provision or view active servers -- Order statistics summary (count and total value by status) - -**Display Columns:** -- Order ID, Username, Email -- Server Name, Game Service, Players -- Price, Duration, Status -- Order Date, End Date, Home ID -- Action buttons - -## Multi-Server Cart Support - -The system already supports multiple servers in a single cart: - -**How it works:** -1. Customer adds multiple services to cart on standalone website -2. Payment processed, all items marked `status='paid'` -3. User logs into panel → navigates to "My Orders" -4. Clicks "Provision All My Servers" button -5. `create_servers.php` queries all `WHERE status='paid' AND user_id=X` -6. `foreach ($orders as $order)` loop processes each: - - Creates game_home - - Assigns IP:Port - - Installs files via Steam/rsync/manual - - Calculates end_date based on duration - - Updates `status='installed'`, saves `home_id`, sets `end_date` - - Sends email and Discord notifications -7. All servers appear in Game Monitor as active - -**Database Flow:** -``` -billing_orders table: -status='in-cart' → (payment) → status='paid' → (provision) → status='installed' - ↓ - (renewal invoice) → status='invoiced' - ↓ - (non-payment) → status='suspended' -``` - -## Testing Workflow - -### User Perspective: -1. Order servers on standalone website at `example.com/modules/billing/` -2. Complete payment (PayPal, Stripe, etc.) -3. Orders marked `status='paid'` in database -4. Log into panel at `panel.example.com/` -5. Navigate to `home.php?m=billing&p=my_orders` -6. Click "Provision Server" for individual order OR "Provision All" for bulk -7. Wait for provisioning (creates server, installs files) -8. Redirected to Game Monitor showing active servers - -### Admin Perspective: -1. Log into panel with admin account -2. Navigate to `home.php?m=billing&p=admin_orders` -3. View all orders across all users -4. Filter by status or search for specific orders -5. Select multiple orders with checkboxes -6. Choose bulk action (provision, suspend, activate, delete) -7. Click individual "Provision" buttons for specific orders -8. Monitor order statistics at bottom of page - -## Order Status Lifecycle - -``` -in-cart → User shopping, not paid yet -paid → Payment received, awaiting provisioning -installed → Server created and active -invoiced → Renewal invoice generated -suspended → Server suspended (non-payment, violation) -deleted → Order soft-deleted -``` - -## Database Schema Reference - -### billing_orders Table (Key Fields) -- `order_id` - Primary key -- `user_id` - Links to ogp_users.user_id -- `service_id` - Links to billing_services.service_id -- `home_id` - Links to game_homes.home_id (after provisioning) -- `status` - Current order status (in-cart, paid, installed, etc.) -- `home_name` - Display name for server -- `max_players` - Player slot limit -- `price` - Order amount paid -- `qty` - Duration quantity (e.g., 3 for "3 months") -- `invoice_duration` - Duration unit (day, month, year) -- `order_date` - When order was created -- `end_date` - When service expires (calculated after provisioning) -- `extended` - Boolean flag for renewals vs new orders -- `ip` - Contains remote_server_id (target node) - -### billing_services Table -- `service_id` - Primary key -- `service_name` - Display name (e.g., "Minecraft 25 Players") -- `home_cfg_id` - Links to game configs -- `mod_cfg_id` - Specific mod/version -- `install_method` - steam, rsync, manual -- `manual_url` - Direct download URL for manual installs - -## Key Files Overview - -``` -modules/billing/ -├── navigation.xml [NEW] - Panel page routing -├── my_orders_panel.php [NEW] - User order list -├── admin_orders.php [NEW] - Admin order management -├── create_servers.php [UPDATED] - Server provisioning -├── module.php [EXISTING] - Module metadata & schema -├── index.php [STANDALONE SITE] - Public storefront -├── cart.php [STANDALONE SITE] - Shopping cart -├── order.php [STANDALONE SITE] - Order checkout -├── payment_success.php [STANDALONE SITE] - Payment return -└── ... [STANDALONE SITE] - Other public pages -``` - -## Access Control - -**User (admin,user):** -- Can provision their own paid orders -- View only their own orders -- Cannot manage other users' orders - -**Admin (admin):** -- Full access to all pages -- Can provision any user's orders -- View and manage all orders across all users -- Bulk actions on multiple orders -- Access to order statistics - -## Next Steps - -1. **Test provisioning workflow:** - - Create test order with `status='paid'` in database - - Log in as that user - - Navigate to My Orders page - - Click "Provision Server" and verify server creation - -2. **Test multi-server scenario:** - - Create multiple orders for same user with `status='paid'` - - Use "Provision All" button - - Verify all servers created and statuses updated - -3. **Admin testing:** - - Log in as admin - - Access admin_orders page - - Test filters and search - - Test bulk actions - - Verify order statistics display - -4. **Optional: Add menu items** - Edit `modules/billing/module.php` to add navigation menu: - ```php - $module_menus = array( - array('subpage' => 'my_orders', 'name'=>'My Orders', 'group'=>'user'), - array('subpage' => 'admin_orders', 'name'=>'Manage Orders', 'group'=>'admin') - ); - ``` - -## Troubleshooting - -**Issue: "No paid orders found"** -- Check database: `SELECT * FROM billing_orders WHERE status='paid'` -- Verify user_id matches logged-in user -- Ensure order_id is correct if using provision_servers directly - -**Issue: Provisioning fails** -- Check create_servers.php for errors -- Verify remote_server_id (stored in ip field) is valid -- Ensure install_method is configured correctly (steam, rsync, manual) -- Check manual_url is accessible if using manual install - -**Issue: Page not accessible** -- Verify navigation.xml is in `modules/billing/` directory -- Check XML syntax is valid -- Ensure file permissions allow reading -- Verify includes/navig.php is loading the module correctly - -**Issue: "Access Denied"** -- Check user session: `$_SESSION['users_group']` must match page access -- admin_orders requires `$_SESSION['users_group'] = 'admin'` -- Regular pages need 'admin' or 'user' group - -## Architecture Notes - -**Separation of Concerns:** -- Standalone website: Public ordering, payment processing, cart management -- Panel: Server provisioning, lifecycle management, admin controls -- Database: Shared MySQL tables for order/service data - -**Module Loading Pattern:** -1. User requests `home.php?m=billing&p=my_orders` -2. `includes/navig.php` validates module exists -3. Loads `modules/billing/navigation.xml` -4. Finds page with `key="my_orders"` -5. Checks user access against `access="admin,user"` -6. Includes `modules/billing/my_orders_panel.php` -7. Calls `exec_ogp_module()` function -8. Renders output in panel layout - -**Multi-Server Processing:** -The `foreach ($orders as $order)` loop in create_servers.php handles multiple servers naturally: -- Query returns all paid orders for user -- Loop processes each order sequentially -- Each iteration creates one game_home -- Each iteration updates one order to 'installed' -- No special cart logic needed - works automatically - -## Success Criteria Checklist - -✅ navigation.xml created with 3 page definitions -✅ my_orders_panel.php displays user's paid orders -✅ Provision buttons link to create_servers.php with order_id -✅ create_servers.php handles provision_all and provision_single -✅ Multi-server support via foreach loop (already existed) -✅ admin_orders.php provides comprehensive order management -✅ Bulk actions for admin (provision, suspend, activate, delete) -✅ Status updates: paid → installed with end_date calculation -✅ home_id saved back to billing_orders after provisioning -✅ Success messages and auto-redirect after provisioning -✅ Access control enforced via navigation.xml attributes - -## Conclusion - -The GSP billing module is now fully integrated with the panel provisioning system. Users can order servers on the standalone website, then log into the panel to provision them. Admins have comprehensive tools to manage all orders. The multi-server cart functionality works automatically via the existing foreach loop. - -**Panel URLs:** -- User Orders: `home.php?m=billing&p=my_orders` -- Admin Orders: `home.php?m=billing&p=admin_orders` -- Provision: `home.php?m=billing&p=provision_servers&order_id=X` diff --git a/Panel/modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md b/Panel/modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 790361e9..00000000 --- a/Panel/modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,282 +0,0 @@ -# Payment System Implementation Summary -**Date:** November 5, 2025 -**Status:** ✅ COMPLETED - Ready for Testing - -## What Was Done - -### 1. **Updated Copilot Instructions** ✅ -- Added explicit standalone/relocatable requirements for `modules/billing/` -- Emphasized: NEVER include panel files, use only standard PHP mysqli -- Documented that billing module can be deployed on separate web host -- All URLs must be root-relative (no `/modules/billing/` in runtime paths) - -### 2. **Documented Status Values** ✅ -**Invoice Status** (`ogp_billing_invoices.status`): -- `due` - Unpaid invoice, awaiting payment -- `paid` - Invoice paid, order created -- `pending` - Legacy status (some admin pages use this) -- `renew` - Renewal invoice - -**Order Status** (`ogp_billing_orders.status`): -- `paid` - Payment received, awaiting server provisioning (panel auto-creates and marks `active`) -- `active` - Server provisioned and running -- `suspended` - Payment overdue, server stopped (grace period) -- `deleted` - Server permanently removed -- `renew` - Active but needs renewal payment - -### 3. **Rebuilt Cart System** ✅ -**File:** `modules/billing/cart.php` - -**Features:** -- Displays all unpaid invoices (`status='due'`) for logged-in user -- Shows: Game type, server name, duration, quantity, price -- Professional table layout with totals -- PayPal JS SDK integration (client-side payment) -- Calls `/api/capture_order.php` backend after PayPal approval -- Handles empty cart gracefully -- Uses only standard mysqli (standalone compatible) - -**Payment Flow:** -1. User clicks PayPal button -2. PayPal JS SDK creates order and processes payment -3. On approval, calls our `/api/capture_order.php` with order_id -4. Backend marks invoices paid, creates orders -5. Redirects to `/payment_success.php` - -### 4. **Rewrote Payment Capture Backend** ✅ -**File:** `modules/billing/api/capture_order.php` (old version backed up as `.backup`) - -**Features:** -- Simplified from 461 lines to ~250 lines -- Clean output buffering (prevents JSON corruption) -- Comprehensive logging to `logs/payment_capture.log` -- Verifies PayPal order capture -- Marks all `due` invoices as `paid` -- Creates `billing_orders` records with `status='paid'` -- Stores full PayPal response JSON in `paypal_data` column -- Returns minimal JSON response (no truncation issues) - -**Security:** -- No output before JSON response -- Validates session user_id -- Logs all steps for debugging/audit trail -- Stores PayPal transaction ID for refunds - -### 5. **Enhanced Success Page** ✅ -**File:** `modules/billing/payment_success.php` - -**Features:** -- Professional confirmation page with success icon -- Shows recent orders with details -- Explains next steps (panel auto-provisioning) -- Links to account management and order pages -- Uses only standard mysqli (standalone compatible) - -## Database Schema - -### Required Tables (Already Exist) -- ✅ `ogp_billing_invoices` - Stores invoices (due/paid) -- ✅ `ogp_billing_orders` - Stores orders (paid/active/suspended/deleted) -- ✅ `ogp_billing_services` - Game server packages/pricing -- ✅ `ogp_billing_coupons` - Discount coupons - -### New Column Required -**Run this SQL:** -```sql -ALTER TABLE `ogp_billing_orders` -ADD COLUMN `paypal_data` TEXT NULL AFTER `payment_txid` -COMMENT 'Full PayPal API response JSON for tracking/refunds'; -``` -**File:** `modules/billing/add_paypal_data_column.sql` - -## Payment Flow Diagram - -``` -User → order.php (select server) - ↓ -add_to_cart.php (create invoice with status='due') - ↓ -cart.php (show unpaid invoices + PayPal button) - ↓ -PayPal Checkout (user pays) - ↓ -api/capture_order.php (backend processing): - - Verify PayPal payment - - Mark invoices status='paid' - - Create orders with status='paid' - - Store PayPal JSON data - ↓ -payment_success.php (confirmation) - ↓ -User logs into Panel - ↓ -Panel auto-provisions servers (paid → active) -``` - -## Configuration - -### PayPal Credentials -**Location:** `modules/billing/api/capture_order.php` (lines 44-45) -```php -$sandbox = true; // Set to false for live -$client_id = 'YOUR_CLIENT_ID'; -$client_secret = 'YOUR_CLIENT_SECRET'; -``` - -**Also update in:** `modules/billing/cart.php` (line 47) - -### Database Connection -**Location:** `modules/billing/includes/config.inc.php` -```php -$db_host = "your_host"; -$db_user = "your_user"; -$db_pass = "your_password"; -$db_name = "panel"; -$table_prefix = "ogp_"; -``` - -## Testing Checklist - -### Pre-Test Setup -- [ ] Run SQL: `add_paypal_data_column.sql` -- [ ] Verify PayPal sandbox credentials are set -- [ ] Confirm database connection works -- [ ] Ensure user is logged in (session has `website_user_id`) - -### Test Flow -1. **Order Creation** - - [ ] Go to `/order.php` - - [ ] Select a game server - - [ ] Configure settings - - [ ] Click "Add to Cart" - - [ ] Verify invoice created in `ogp_billing_invoices` with `status='due'` - -2. **Cart Display** - - [ ] Go to `/cart.php` - - [ ] Verify invoice(s) displayed with correct details - - [ ] Verify total amount is correct - - [ ] Verify PayPal button appears - -3. **Payment Processing** - - [ ] Click PayPal button - - [ ] Complete sandbox payment - - [ ] Check `logs/payment_capture.log` for processing details - - [ ] Verify no JSON errors in browser console - - [ ] Verify redirected to `/payment_success.php` - -4. **Database Verification** - - [ ] Check `ogp_billing_invoices`: `status='paid'`, `payment_txid` set - - [ ] Check `ogp_billing_orders`: New record with `status='paid'` - - [ ] Check `paypal_data` column contains JSON - - [ ] Verify `order_id` in invoice links to order - -5. **Success Page** - - [ ] Verify order(s) displayed - - [ ] Verify correct amounts shown - - [ ] Verify all links work - -6. **Panel Provisioning** (Future - Not Implemented Yet) - - [ ] Log into panel - - [ ] Panel detects orders with `status='paid'` - - [ ] Panel creates game server homes - - [ ] Panel updates order `status='active'` - -## What's NOT Done Yet (Todo) - -### High Priority -- [ ] **Email Notifications** - Send confirmation email after payment -- [ ] **Invoice History Page** - Show user's paid invoices (`my_invoices.php`) -- [ ] **Suspended Status Support** - Verify cron job handles suspended orders correctly - -### Medium Priority -- [ ] **Refund System** - Admin interface to issue PayPal refunds using stored JSON data -- [ ] **Webhook Support** - Add PayPal webhook handler for payment verification (more secure than client-side) -- [ ] **Coupon Application** - Apply discount coupons during checkout - -### Low Priority -- [ ] **Multi-currency Support** - Currently USD only -- [ ] **Tax Calculation** - Add tax/VAT support -- [ ] **Payment Plans** - Recurring subscriptions via PayPal - -## Files Modified - -### Core Payment Files -- ✅ `modules/billing/cart.php` - Complete rewrite -- ✅ `modules/billing/api/capture_order.php` - Simplified rewrite (old backed up) -- ✅ `modules/billing/payment_success.php` - Enhanced with order display - -### Configuration -- ✅ `.github/copilot-instructions.md` - Added standalone/relocatable requirements - -### Database -- ✅ `modules/billing/add_paypal_data_column.sql` - New migration file - -### Existing Files (Not Modified) -- `modules/billing/add_to_cart.php` - Already working correctly -- `modules/billing/order.php` - Already working correctly -- `modules/billing/includes/config.inc.php` - Config file (no changes needed) - -## Troubleshooting - -### Issue: JSON Parse Error -**Cause:** Output before JSON response (whitespace, errors, warnings) -**Fix:** Check `logs/payment_capture.log` for errors. Ensure `ob_start()` at top of `capture_order.php` - -### Issue: No Orders Created -**Cause:** User not logged in or session lost -**Fix:** Verify session contains `website_user_id` or `user_id` - -### Issue: Invoices Not Marked Paid -**Cause:** Database connection failed or SQL error -**Fix:** Check `logs/payment_capture.log` for database errors - -### Issue: PayPal Button Doesn't Appear -**Cause:** Empty cart or JS error -**Fix:** Check browser console. Verify invoices exist with `status='due'` - -### Issue: 500 Error on capture_order.php -**Cause:** PHP error in capture script -**Fix:** Check `logs/payment_capture.log` and PHP error logs - -## Deployment Notes - -### Same Host Deployment -Files already at correct location: `modules/billing/` - -### External Host Deployment -1. Copy entire `modules/billing/` directory to external web host -2. Deploy at website root (not in subdirectory) -3. Update `includes/config.inc.php` with panel database credentials -4. Ensure external host can connect to panel database (firewall/network) -5. Update PayPal return URLs to external domain - -## Security Considerations - -✅ **Implemented:** -- Output buffering prevents JSON corruption -- SQL injection protection (mysqli_real_escape_string) -- Session validation (user_id required) -- PayPal OAuth token authentication -- Comprehensive audit logging - -⚠️ **Recommended (Not Implemented):** -- CSRF token validation on payment endpoints -- Rate limiting on API endpoints -- PayPal webhook signature verification -- IP whitelisting for admin functions - -## Support & Maintenance - -### Log Files -- `modules/billing/logs/payment_capture.log` - Payment processing log -- `modules/billing/logs/add_to_cart.log` - Cart/invoice creation log -- `modules/billing/logs/site.log` - General site log - -### Key Functions -- `capture_order.php::log_payment()` - Payment logging function -- Database schema in `create_invoices_table.sql` - -### Contact -For issues or questions, refer to: -- GitHub repo: `GameServerPanel/GSP` branch `Panel-unstable` -- This summary: `modules/billing/PAYMENT_IMPLEMENTATION_SUMMARY.md` diff --git a/Panel/modules/billing/PAYPAL_DEBUGGING_GUIDE.md b/Panel/modules/billing/PAYPAL_DEBUGGING_GUIDE.md deleted file mode 100644 index 4ca1a132..00000000 --- a/Panel/modules/billing/PAYPAL_DEBUGGING_GUIDE.md +++ /dev/null @@ -1,316 +0,0 @@ -# PayPal Payment Flow Debugging Guide - -## Overview - -This guide explains how to diagnose and troubleshoot PayPal payment errors using the comprehensive logging system that has been added to the payment flow. - -## Problem Being Addressed - -Users were experiencing intermittent errors when clicking "Pay from PayPal" button: -- JSON parsing errors -- HTTP ERROR 500 -- "Currently unable to handle this request" errors - -These errors would "flip-flop" between different error types, making it difficult to diagnose the root cause. - -## Log Files Location - -All logs are stored in: `/modules/billing/logs/` - -### Available Log Files - -1. **`paypal_create_order.log`** - Logs all PayPal order creation requests - - When: Created when user clicks "Pay with PayPal" button - - Contains: Request data, OAuth tokens, PayPal API responses - -2. **`paypal_capture.log`** - Logs all payment capture attempts - - When: Created when PayPal redirects user back after approving payment - - Contains: Capture requests, database operations, order creation - -3. **`client_errors.log`** - Logs JavaScript errors from browser - - When: Created when browser encounters errors during checkout - - Contains: Client-side errors, PayPal SDK issues, network failures - -## How to Debug Payment Issues - -### Step 1: Identify the Request - -Each request has a unique ID for tracking: -- Create order requests: `req_XXXXX` -- Capture order requests: `cap_XXXXX` - -Look for these IDs in error messages shown to users. - -### Step 2: Check the Logs - -#### For "Failed to create order" errors: - -```bash -tail -100 /modules/billing/logs/paypal_create_order.log -``` - -Look for: -- `JSON_DECODE_ERROR` - Invalid input from cart.php -- `OAUTH_CURL_ERROR` or `OAUTH_HTTP_ERROR` - Can't connect to PayPal -- `CREATE_ORDER_HTTP_ERROR` - PayPal rejected the order - -#### For "Payment capture failed" errors: - -```bash -tail -100 /modules/billing/logs/paypal_capture.log -``` - -Look for: -- `OAUTH_*_ERROR` - Authentication issues -- `CAPTURE_HTTP_ERROR` - PayPal rejected capture -- `DB_CONNECTION_FAILED` - Database issues -- `UPDATE_INVOICES_FAILED` - Can't mark invoices as paid -- `ORDER_CREATE_FAILED` - Can't create order record - -#### For client-side errors: - -```bash -tail -100 /modules/billing/logs/client_errors.log -``` - -Look for: -- Network errors (fetch failed) -- PayPal SDK errors -- JSON parsing errors - -### Step 3: Common Issues and Solutions - -#### Issue: OAuth fails (OAUTH_HTTP_ERROR) - -**Log entry example:** -``` -[2025-10-29 21:30:00] [req_12345] OAUTH_HTTP_ERROR -http_code => 401 -``` - -**Cause:** Invalid PayPal credentials - -**Solution:** Check that `$client_id` and `$client_secret` in `api/create_order.php` and `api/capture_order.php` are correct. - ---- - -#### Issue: JSON decode error - -**Log entry example:** -``` -[2025-10-29 21:30:00] [req_12345] JSON_DECODE_ERROR -error => Syntax error -``` - -**Cause:** Malformed JSON from cart.php or corrupted request - -**Solution:** -1. Check the `RAW_INPUT` entry before the error -2. Verify cart.php is sending valid JSON -3. Check for PHP errors that might corrupt the output - ---- - -#### Issue: PayPal returns error creating order - -**Log entry example:** -``` -[2025-10-29 21:30:00] [req_12345] CREATE_ORDER_HTTP_ERROR -http_code => 400 -response => {"name":"INVALID_REQUEST","details":[{"issue":"..."}]} -``` - -**Cause:** Invalid order data sent to PayPal - -**Solution:** -1. Look at `PAYPAL_ORDER_PAYLOAD` entry to see what was sent -2. Common issues: - - Invalid amount format (must be 2 decimals) - - Invalid currency code - - Malformed items array - - Invalid URLs (return_url, cancel_url must be absolute URLs) - ---- - -#### Issue: Database connection failed - -**Log entry example:** -``` -[2025-10-29 21:30:00] [cap_12345] DB_CONNECTION_FAILED -error => Access denied for user -``` - -**Cause:** Can't connect to database - -**Solution:** -1. Check database credentials in `includes/config.inc.php` -2. Verify database server is running -3. Check database permissions - ---- - -#### Issue: Invoice update failed - -**Log entry example:** -``` -[2025-10-29 21:30:00] [cap_12345] UPDATE_INVOICES_FAILED -error => Table 'ogp_billing_invoices' doesn't exist -``` - -**Cause:** Database schema issue - -**Solution:** -1. Verify table exists and has correct name -2. Check `$table_prefix` variable in config -3. Run database migrations if needed - -## Log Entry Structure - -Each log entry includes: - -``` -[TIMESTAMP] [REQUEST_ID] LOG_LABEL -key => value -key => value --------------------------------------------------------------------------------- -``` - -- **TIMESTAMP**: When the event occurred (Y-m-d H:i:s format) -- **REQUEST_ID**: Unique identifier for tracking the request -- **LOG_LABEL**: What happened (e.g., OAUTH_SUCCESS, CREATE_ORDER_FAILED) -- **Data**: Relevant data for the event (arrays/objects pretty-printed) - -## Request Flow with Logging - -### Creating an Order - -1. User clicks "Pay with PayPal" in cart.php -2. JavaScript calls `api/create_order.php` -3. Logs generated: - - `REQUEST_START` - Initial request info - - `RAW_INPUT` - What was received - - `PARSED_INPUT` - Decoded data - - `OAUTH_REQUEST_START` - Starting OAuth - - `OAUTH_RESPONSE` - OAuth result - - `OAUTH_SUCCESS` or `OAUTH_*_ERROR` - - `CREATE_ORDER_REQUEST_START` - Sending to PayPal - - `CREATE_ORDER_RESPONSE` - PayPal's response - - `CREATE_ORDER_SUCCESS` or `CREATE_ORDER_*_ERROR` - -### Capturing Payment - -1. User approves payment on PayPal -2. PayPal redirects back to site -3. JavaScript calls `api/capture_order.php` -4. Logs generated: - - `REQUEST_START` - Initial request - - `RAW_INPUT` - Order ID received - - `PARSED_INPUT` - Decoded data - - `OAUTH_*` - Authentication steps - - `CAPTURE_REQUEST_START` - Starting capture - - `CAPTURE_RESPONSE` - PayPal's response - - `CAPTURE_SUCCESS` or `CAPTURE_*_ERROR` - - `PAYMENT_DETAILS` - Extracted transaction info - - `STARTING_DB_PROCESSING` - Beginning database work - - `DB_CONNECTED` - Database ready - - `SESSION_INFO` - User session details - - `PROCESSING_INVOICES` - Starting invoice processing - - `UPDATE_INVOICES_*` - Invoice update results - - `PROCESSING_INVOICE` - For each invoice - - `NEW_ORDER_DETECTED` or `RENEWAL_DETECTED` - - `ORDER_CREATE_*` or `ORDER_EXTENDED_*` - - `PROCESSING_COMPLETE` - Done - -## Monitoring Tips - -### Watch logs in real-time - -```bash -# Watch create order logs -tail -f /modules/billing/logs/paypal_create_order.log - -# Watch capture logs -tail -f /modules/billing/logs/paypal_capture.log - -# Watch all logs -tail -f /modules/billing/logs/*.log -``` - -### Filter for errors only - -```bash -grep -i error /modules/billing/logs/paypal_create_order.log -grep -i failed /modules/billing/logs/paypal_capture.log -``` - -### Find specific request by ID - -```bash -grep "req_abc123" /modules/billing/logs/paypal_create_order.log -grep "cap_xyz789" /modules/billing/logs/paypal_capture.log -``` - -### Count successful vs failed requests - -```bash -grep -c "CREATE_ORDER_SUCCESS" /modules/billing/logs/paypal_create_order.log -grep -c "CREATE_ORDER.*ERROR" /modules/billing/logs/paypal_create_order.log -``` - -## Log Rotation - -Logs will grow over time. Consider implementing log rotation: - -```bash -# Archive old logs -cd /modules/billing/logs -gzip paypal_create_order.log -mv paypal_create_order.log.gz paypal_create_order.$(date +%Y%m%d).log.gz -touch paypal_create_order.log -``` - -Or use logrotate: - -``` -/path/to/modules/billing/logs/*.log { - daily - rotate 7 - compress - delaycompress - notifempty - create 0644 www-data www-data -} -``` - -## Error Messages to Users - -When errors occur, users now see messages with request IDs: - -- "Failed to create order: API error 500 (Ref: req_abc123)" -- "Payment capture failed: oauth_fail (Ref: cap_xyz789)" - -Use these reference IDs to search the logs for the full details. - -## Getting Help - -When reporting issues, include: - -1. The exact error message shown to user (including Ref ID) -2. Relevant log entries (search by Ref ID) -3. What the user was trying to do -4. Whether it's consistent or intermittent -5. Browser console output (F12 → Console tab) - -## Additional Resources - -- PayPal API Documentation: https://developer.paypal.com/api/rest/ -- PayPal Sandbox Testing: https://developer.paypal.com/developer/accounts/ -- PayPal Error Codes: https://developer.paypal.com/api/rest/reference/orders/v2/errors/ - -## Changelog - -### 2025-10-29 -- Added comprehensive logging to create_order.php -- Enhanced logging in capture_order.php -- Added client-side error logging -- Created debugging guide diff --git a/Panel/modules/billing/PHASE1_COMPLETE_SUMMARY.md b/Panel/modules/billing/PHASE1_COMPLETE_SUMMARY.md deleted file mode 100644 index e7661f82..00000000 --- a/Panel/modules/billing/PHASE1_COMPLETE_SUMMARY.md +++ /dev/null @@ -1,149 +0,0 @@ -# Phase 1 Complete: Visual TODO System Implementation - -## Date: December 19, 2024 - -## Summary -Successfully implemented a comprehensive visual identification system for incomplete game documentation. All 146 game folders now have completion tracking, with "TODO: " prefix displayed for incomplete documentation. - -## What Was Accomplished - -### 1. PowerShell Automation Script Created -**File:** `update_metadata_complete.ps1` -- Scans all game documentation folders -- Adds "complete" field to metadata.json files -- Marks Minecraft as complete (true), all others as incomplete (false) -- Executed successfully: 146 files updated, 2 skipped (already had field) - -### 2. Documentation Display System Enhanced -**File:** `modules/billing/docs.php` -- Added logic to read "complete" status from metadata -- Automatically prefixes "TODO: " to incomplete game names -- No visual change for complete documentation -- Maintains proper sorting and categorization - -### 3. Metadata Files Updated -**Files Modified:** 146 metadata.json files -- `minecraft/metadata.json` - complete: true ✅ -- All other games - complete: false (displays with TODO prefix) - -### 4. Documentation Created -- `RECENT_FIXES_SUMMARY.md` - Updated with Phase 1 details -- `GAME_DOCS_TODO_REFERENCE.md` - Complete reference guide for next phases - -## Visual Result - -### Before: -``` -Game Servers (148) -├── 7 Days to Die -├── Aliens vs Predator -├── Arma 3 -├── DayZ -├── Minecraft Server -├── Rust -└── ... -``` - -### After: -``` -Game Servers (148) -├── TODO: 7 Days to Die -├── TODO: Aliens vs Predator -├── TODO: Arma 3 -├── TODO: DayZ -├── Minecraft Server (✓ complete) -├── TODO: Rust -└── ... -``` - -## Benefits -1. **Instant Visibility** - Users/developers immediately see which games lack comprehensive docs -2. **Progress Tracking** - As games are completed, TODO prefix disappears -3. **Quality Control** - Clear standard (Minecraft template) vs incomplete stubs -4. **Systematic Completion** - Easy to prioritize and track remaining work - -## Minecraft Template Reference (Complete Documentation Standard) -The only game marked complete serves as the template for all others: -- ✅ Comprehensive ports table (ALL ports with purposes) -- ✅ Firewall configurations (4 platforms) -- ✅ Startup parameters (detailed explanations) -- ✅ Troubleshooting sections (specific common issues) -- ✅ Performance optimization -- ✅ Security best practices -- ✅ Resource links with citations -- ✅ ~550 lines of comprehensive content - -## Next Phase: ARMA Family + DayZ Documentation - -### Priority Games (Phase 2): -1. Arma 3 -2. Arma 2: Operation Arrowhead -3. Arma 2 -4. Arma 2: Combined Operations (DayZ Mod base) -5. DayZ Standalone -6. DayZ Mod - -### Research Sources: -- Bohemia Interactive Wiki -- LGSM (LinuxGSM) scripts and configs -- Reddit: r/arma, r/dayzservers -- BI Forums, DayZ Forums -- Steam Community Guides (highly-rated) -- GitHub repositories with server configurations -- User comments and community solutions - -### Time Estimate: -- 6 games × 60 minutes average = ~6 hours total -- Each game: 15-30 min research + 20-30 min writing + 5-10 min review - -## Technical Implementation Details - -### Metadata Structure: -```json -{ - "name": "Game Name", - "description": "Brief description", - "category": "game", - "order": 10, - "complete": false -} -``` - -### Display Logic (docs.php): -```php -$isComplete = isset($metadata['complete']) ? (bool)$metadata['complete'] : false; -$displayName = $metadata['name'] ?? ucfirst($folder); - -if (!$isComplete) { - $displayName = 'TODO: ' . $displayName; -} -``` - -### Marking Complete: -When documentation is finished, change in metadata.json: -```json -"complete": true -``` - -## Files Modified Summary -- ✅ `modules/billing/docs.php` - Display logic -- ✅ `modules/billing/update_metadata_complete.ps1` - Automation script -- ✅ `modules/billing/docs/*/metadata.json` - 146 files updated -- ✅ `modules/billing/RECENT_FIXES_SUMMARY.md` - Updated -- ✅ `modules/billing/GAME_DOCS_TODO_REFERENCE.md` - Created -- ✅ `modules/billing/PHASE1_COMPLETE_SUMMARY.md` - This file - -## Success Metrics -- ✅ 146 games marked with completion status -- ✅ Visual TODO system working on docs.php -- ✅ 1 complete game (Minecraft) serves as template -- ✅ Clear reference documentation for next phases -- ✅ Systematic approach established for remaining 146 games - -## Approval & Sign-off -Phase 1 is complete and ready for Phase 2 (ARMA family research and documentation). - ---- -**Prepared by:** GitHub Copilot -**Date:** December 19, 2024 -**Status:** Phase 1 Complete ✅ diff --git a/Panel/modules/billing/QUICK_DEBUG_REFERENCE.md b/Panel/modules/billing/QUICK_DEBUG_REFERENCE.md deleted file mode 100644 index 5822eb26..00000000 --- a/Panel/modules/billing/QUICK_DEBUG_REFERENCE.md +++ /dev/null @@ -1,186 +0,0 @@ -# PayPal Payment Flow - Quick Debug Reference - -## Quick Commands - -### View recent errors: -```bash -cd /home/runner/work/GSP/GSP/modules/billing/logs - -# Last 50 lines of create order log -tail -50 paypal_create_order.log - -# Last 50 lines of capture log -tail -50 paypal_capture.log - -# Last 50 lines of client errors -tail -50 client_errors.log -``` - -### Watch logs live: -```bash -# In terminal, run: -tail -f /home/runner/work/GSP/GSP/modules/billing/logs/paypal_*.log -``` - -### Search for specific error: -```bash -# Find all OAuth errors -grep "OAUTH.*ERROR" paypal_create_order.log paypal_capture.log - -# Find database errors -grep "DB.*FAILED" paypal_capture.log - -# Find a specific request by ID -grep "req_12345" paypal_create_order.log -``` - -## Common Error Patterns - -### ❌ "JSON error" or "unable to handle this request" - -**What to check:** -1. Browser console (F12 → Console tab) for JavaScript errors -2. `client_errors.log` for client-side issues -3. `paypal_create_order.log` for `JSON_DECODE_ERROR` - -**Quick fix:** -- Check if cart items are valid -- Verify amount calculations are correct -- Look for PHP errors that might corrupt JSON output - ---- - -### ❌ HTTP ERROR 500 - -**What to check:** -1. `paypal_create_order.log` for `CREATE_ORDER_HTTP_ERROR` -2. `paypal_capture.log` for `CAPTURE_HTTP_ERROR` -3. Look for `OAUTH.*ERROR` entries - -**Quick fix:** -- Verify PayPal credentials are correct -- Check PayPal API status: https://www.paypal-status.com/ -- Verify sandbox vs live mode settings match credentials - ---- - -### ❌ Payment seems successful but no order created - -**What to check:** -1. `paypal_capture.log` for `DB_CONNECTION_FAILED` -2. Look for `UPDATE_INVOICES_FAILED` -3. Check `ORDER_CREATE_FAILED` - -**Quick fix:** -- Verify database connection settings -- Check if `ogp_billing_invoices` table exists -- Verify `ogp_billing_orders` table exists -- Check table permissions - ---- - -### ❌ Intermittent failures (works sometimes, fails sometimes) - -**What to check:** -1. Compare successful vs failed requests in logs -2. Look for timeout errors (`CURL.*ERROR`) -3. Check for database connection pool exhaustion - -**Quick fix:** -- Check server load/resources -- Verify network connectivity to PayPal API -- Check for rate limiting - -## Log File Locations - -``` -/home/runner/work/GSP/GSP/modules/billing/logs/ -├── paypal_create_order.log # Order creation (when clicking "Pay") -├── paypal_capture.log # Payment capture (after PayPal approval) -└── client_errors.log # JavaScript/browser errors -``` - -## Request ID Format - -- Create order: `req_XXXXXXXXXXXXX` -- Capture order: `cap_XXXXXXXXXXXXX` - -When user sees an error with `(Ref: req_abc123)`, search logs for that ID. - -## Important Log Labels - -### Create Order Flow: -- `REQUEST_START` → `RAW_INPUT` → `PARSED_INPUT` -- `OAUTH_REQUEST_START` → `OAUTH_SUCCESS` -- `CREATE_ORDER_REQUEST_START` → `CREATE_ORDER_SUCCESS` - -### Capture Order Flow: -- `REQUEST_START` → `PARSED_INPUT` -- `OAUTH_SUCCESS` → `CAPTURE_SUCCESS` -- `DB_CONNECTED` → `PROCESSING_INVOICES` -- `ORDER_CREATED_SUCCESS` or `ORDER_EXTENDED_SUCCESS` - -### Error Labels: -- `*_ERROR` - Something went wrong -- `*_FAILED` - Operation failed -- `INVALID_*` - Invalid input/data - -## Browser Console Debugging - -1. Open cart page -2. Press F12 to open DevTools -3. Go to Console tab -4. Click "Pay with PayPal" -5. Watch for: - - Red error messages - - `PayPal Error:` logs - - Network errors (check Network tab) - -## Testing Checklist - -When testing payments: - -- [ ] Check browser console for errors -- [ ] Note the Ref ID if error occurs -- [ ] Check `paypal_create_order.log` for the request -- [ ] Check `paypal_capture.log` if got past order creation -- [ ] Verify database tables exist and have data -- [ ] Check PayPal sandbox account activity - -## Need More Help? - -See full guide: `PAYPAL_DEBUGGING_GUIDE.md` - -## Key Configuration Files - -- PayPal credentials: `api/create_order.php` and `api/capture_order.php` - - Lines 5-6: `$client_id` and `$client_secret` - - Line 4: `$sandbox` (true/false) - -- Database config: `includes/config.inc.php` - - `$db_host`, `$db_user`, `$db_pass`, `$db_name` - - `$table_prefix` - -## Status Checklist for Issues - -When user reports error: - -1. **Get details:** - - [ ] What error message did they see? - - [ ] What was the Ref ID (if shown)? - - [ ] Can they reproduce it? - -2. **Check logs:** - - [ ] Find the request by Ref ID - - [ ] Look for ERROR or FAILED labels - - [ ] Check surrounding context (before/after) - -3. **Verify config:** - - [ ] PayPal credentials valid? - - [ ] Database connection working? - - [ ] Correct sandbox/live mode? - -4. **Test:** - - [ ] Try creating test order - - [ ] Watch logs in real-time - - [ ] Check database for created records diff --git a/Panel/modules/billing/QUICK_START.md b/Panel/modules/billing/QUICK_START.md deleted file mode 100644 index 794ff58c..00000000 --- a/Panel/modules/billing/QUICK_START.md +++ /dev/null @@ -1,261 +0,0 @@ -# Quick Start Guide - GSP Billing Panel Integration - -## What Was Completed - -✅ Created `navigation.xml` - Routes panel URLs to billing pages -✅ Created `my_orders_panel.php` - User view of paid orders -✅ Updated `create_servers.php` - Enhanced provisioning with multi-server support -✅ Created `admin_orders.php` - Admin order management interface -✅ Created `test_integration.php` - Integration testing tool -✅ Created `PANEL_INTEGRATION.md` - Complete documentation - -## How to Test Right Now - -### Step 1: Test Integration -1. Log into your GSP panel -2. Navigate to: `home.php?m=billing&p=test_integration` -3. Review all checks - everything should show green ✓ - -### Step 2: Create a Test Order (Database) -If you don't have paid orders yet, create one in the database: - -```sql -INSERT INTO billing_orders -(user_id, service_id, home_name, max_players, price, qty, invoice_duration, status, order_date) -VALUES -(1, 1, 'Test Minecraft Server', 25, 9.99, 1, 'month', 'paid', NOW()); -``` - -Replace: -- `user_id = 1` with your actual user ID -- `service_id = 1` with a valid service_id from billing_services table - -### Step 3: View Your Orders -Navigate to: `home.php?m=billing&p=my_orders` - -You should see: -- Table with your paid orders -- "Provision Server" button for each order -- "Provision All My Servers" button if multiple orders - -### Step 4: Provision a Server -Click "Provision Server" button - -Expected behavior: -- Redirects to provision_servers page -- Creates game_home entry -- Assigns IP and port -- Installs game files (Steam/rsync/manual) -- Updates order status to 'installed' -- Shows success message -- Auto-redirects to Game Monitor after 3 seconds - -### Step 5: Admin Testing (Admin Only) -Navigate to: `home.php?m=billing&p=admin_orders` - -Features to test: -- View all orders across all users -- Filter by status dropdown -- Search by username/order ID/server name -- Select multiple orders with checkboxes -- Bulk actions dropdown -- Individual provision/view buttons - -## Multi-Server Cart Testing - -### Setup: -Create multiple paid orders for the same user: - -```sql -INSERT INTO billing_orders -(user_id, service_id, home_name, max_players, price, qty, invoice_duration, status, order_date) -VALUES -(1, 1, 'Minecraft Server 1', 25, 9.99, 1, 'month', 'paid', NOW()), -(1, 2, 'Minecraft Server 2', 50, 14.99, 1, 'month', 'paid', NOW()), -(1, 3, 'ARK Server', 100, 19.99, 1, 'month', 'paid', NOW()); -``` - -### Test: -1. Navigate to: `home.php?m=billing&p=my_orders` -2. Click "Provision All My Servers (3)" button -3. Wait for provisioning to complete -4. Verify all 3 orders changed to status='installed' -5. Check Game Monitor - all 3 servers should appear - -## Panel URLs Reference - -| Page | URL | Access | Purpose | -|------|-----|--------|---------| -| Test Integration | `home.php?m=billing&p=test_integration` | user, admin | Verify setup | -| My Orders | `home.php?m=billing&p=my_orders` | user, admin | View paid orders | -| Provision Servers | `home.php?m=billing&p=provision_servers&order_id=X` | user, admin | Create servers | -| Admin Orders | `home.php?m=billing&p=admin_orders` | admin only | Manage all orders | - -## Common Issues & Solutions - -### "No paid orders found" -**Problem:** No orders with status='paid' in database -**Solution:** Check database: `SELECT * FROM billing_orders WHERE status='paid'` -**Fix:** Update test order: `UPDATE billing_orders SET status='paid' WHERE order_id=X` - -### "Page not found" / 404 error -**Problem:** navigation.xml not loaded or file missing -**Solution 1:** Verify navigation.xml exists in `modules/billing/` -**Solution 2:** Check file permissions (must be readable by web server) -**Solution 3:** Verify XML syntax is valid (no typos) - -### "Access Denied" -**Problem:** User group doesn't match page access requirements -**Solution:** Check `$_SESSION['users_group']` matches navigation.xml access attribute -**Fix for admin pages:** Only 'admin' group can access admin_orders - -### Provisioning fails silently -**Problem:** create_servers.php encounters error but doesn't show it -**Solution:** Check PHP error logs -**Common causes:** -- Invalid remote_server_id (stored in ip field) -- Missing game server files -- SteamCMD not configured -- Permissions issues on game directories - -### Multiple servers provision but some fail -**Problem:** Foreach loop continues even if one fails -**Solution:** Check individual order details in admin_orders -**Fix:** Provision failed orders individually to see specific error - -## Architecture Quick Reference - -### Order Status Flow -``` -in-cart → paid → installed → invoiced → suspended/deleted - ↑ ↑ - | | - (payment) (renewal or non-payment) -``` - -### Provisioning Flow -``` -User orders on website → Payment processed → status='paid' - ↓ -User logs into panel → My Orders → Click "Provision Server" - ↓ -create_servers.php → Query WHERE status='paid' → foreach order - ↓ -Create game_home → Assign IP:Port → Install files → Update status='installed' - ↓ - Email + Discord notification → Redirect to Game Monitor -``` - -### File Locations -``` -modules/billing/ -├── navigation.xml [Panel routing config] -├── my_orders_panel.php [User order list] -├── admin_orders.php [Admin management] -├── create_servers.php [Server provisioning] -├── test_integration.php [Testing tool] -├── PANEL_INTEGRATION.md [Full documentation] -└── QUICK_START.md [This file] -``` - -## Next Steps After Testing - -### 1. Optional: Add Menu Items -Edit `modules/billing/module.php` around line 20: - -```php -$module_menus = array( - array('subpage' => 'my_orders', 'name'=>'My Orders', 'group'=>'user'), - array('subpage' => 'admin_orders', 'name'=>'Manage Orders', 'group'=>'admin') -); -``` - -This adds menu items to panel sidebar navigation. - -### 2. Customize Success Messages -Edit `create_servers.php` around line 385 to customize redirect behavior: -- Change auto-redirect delay (currently 3 seconds) -- Add custom success messages -- Modify redirect destination - -### 3. Add Email Templates -Enhance email notifications in create_servers.php: -- Custom HTML email templates -- Include server connection details -- Add next steps for users - -### 4. Discord Webhook Formatting -Improve Discord notifications with: -- Rich embeds with server details -- Color coding by status -- Direct links to Game Monitor - -### 5. Production Deployment -Before going live: -- Test with real payment gateway (PayPal/Stripe) -- Verify SteamCMD and game installs work -- Test with multiple concurrent users -- Set up monitoring and logging -- Configure backup system - -## Support & Troubleshooting - -### Debug Mode -To see detailed errors, enable PHP error reporting temporarily: - -In `create_servers.php` at the top of exec_ogp_module(): -```php -error_reporting(E_ALL); -ini_set('display_errors', 1); -``` - -### Database Debugging -Check order details: -```sql -SELECT o.*, s.service_name, u.users_login -FROM billing_orders o -LEFT JOIN billing_services s ON o.service_id = s.service_id -LEFT JOIN users u ON o.user_id = u.user_id -WHERE o.status = 'paid'; -``` - -### Log Files to Check -- PHP error log: `/var/log/php_errors.log` (or server equivalent) -- Apache/Nginx error log: `/var/log/apache2/error.log` -- OGP agent log: Check agent output for remote commands -- Game server logs: In each game_home directory - -## Questions? - -Refer to: -- `PANEL_INTEGRATION.md` - Complete technical documentation -- `test_integration.php` - Run diagnostics: `home.php?m=billing&p=test_integration` -- OGP documentation - For panel-specific questions -- `create_servers.php` - Source code with comments - -## Success Checklist - -Before considering integration complete: - -- [ ] test_integration.php shows all green checks -- [ ] Can view orders at my_orders page -- [ ] Can provision single order successfully -- [ ] Can provision multiple orders at once -- [ ] Orders update to status='installed' in database -- [ ] home_id saved correctly after provisioning -- [ ] end_date calculated and saved -- [ ] Servers appear in Game Monitor -- [ ] Admin can view all orders -- [ ] Admin can filter and search orders -- [ ] Bulk actions work (provision multiple) -- [ ] Email notifications sent (if configured) -- [ ] Discord webhooks work (if configured) - ---- - -**Integration Status: COMPLETE** -**Multi-Server Support: FUNCTIONAL** -**Admin Tools: READY** -**Testing Tool: AVAILABLE** - -Start with: `home.php?m=billing&p=test_integration` diff --git a/Panel/modules/billing/README.md b/Panel/modules/billing/README.md deleted file mode 100644 index 6f81cea4..00000000 --- a/Panel/modules/billing/README.md +++ /dev/null @@ -1,196 +0,0 @@ -# GameServers.World - Billing Module - -## Overview -The billing module is a complete standalone website for selling game servers. It can be deployed on the same machine as the GSP panel or on a completely separate external web host. - -## Runtime location and portability - -- Primary runtime path: `Panel/modules/billing/` -- Legacy compatibility wrappers: `Website/` (key entrypoints proxy into `Panel/modules/billing`) -- Canonical human-facing timestamp source: `Website/timestamp.txt` -- Runtime timestamp file: `Panel/modules/billing/timestamp.txt` (synced from the canonical file at runtime) - -### Standalone configuration values - -Set one of the following (priority top-to-bottom) when running billing outside the panel tree: - -1. Environment variables: - - `GSP_PANEL_PATH` (or `BILLING_PANEL_PATH`) for panel root - - `BILLING_BASE_PATH` for storefront URL base (e.g. `/billing`) -2. `Panel/modules/billing/site_config.local.php` overrides (git-ignored) -3. `Panel/modules/billing/site_config.php` defaults - -`site_config.example.php` documents the expected keys and examples. - -## Documentation System - -### Visual TODO System ✅ -As of December 19, 2024, all game documentation includes completion tracking: - -- **Complete Documentation:** Displays with normal name (e.g., "Minecraft Server") -- **Incomplete Documentation:** Displays with "TODO: " prefix (e.g., "TODO: Arma 3") - -### Current Status -- **Complete:** 1 game (Minecraft - comprehensive template) -- **Incomplete:** 146 games (marked with TODO prefix) - -### Documentation Template Standard -All complete documentation should match the Minecraft template: -- Comprehensive ports table (ALL ports with purposes) -- Firewall configurations (UFW, FirewallD, Windows, iptables) -- Startup parameters with detailed explanations -- Troubleshooting sections with specific solutions -- Performance optimization tips -- Security best practices -- Resource links with citations - -### Viewing Documentation -- Browse to `docs.php` to see all game documentation -- Games with "TODO: " prefix need comprehensive research and writing -- Click any game to view its documentation page - -### Marking Documentation Complete -When game documentation is finished: -1. Edit `docs/{game}/metadata.json` -2. Change `"complete": false` to `"complete": true` -3. The TODO prefix will automatically disappear - -## Recent Updates - -### December 19, 2024 - Visual TODO System -- ✅ Implemented completion tracking for all 148 game folders -- ✅ Created PowerShell automation script (`update_metadata_complete.ps1`) -- ✅ Updated docs.php with automatic TODO prefix display -- ✅ Minecraft documentation completed as template example - -### November 10, 2025 - Critical Fixes -- ✅ Fixed PayPal payment capture session issue -- ✅ Removed cart debug logging code -- ✅ Fixed cart page header/footer consistency -- ✅ Implemented AJAX invoice removal with Font Awesome icons - -## Key Files - -### Documentation System -- `docs.php` - Documentation browser with TODO system -- `docs/*/index.php` - Individual game documentation pages -- `docs/*/metadata.json` - Game metadata with completion status -- `update_metadata_complete.ps1` - Batch metadata update script - -### Reference Documents -- `PHASE1_COMPLETE_SUMMARY.md` - Phase 1 implementation summary -- `GAME_DOCS_TODO_REFERENCE.md` - Complete reference for documentation system -- `RECENT_FIXES_SUMMARY.md` - All recent fixes and enhancements - -### Payment Integration -- `api/capture_order.php` - PayPal payment capture (fixed session handling) -- `payment_success.php` - Payment success redirect -- `payment_cancel.php` - Payment cancellation handler - -### Shopping Cart -- `cart.php` - Shopping cart UI with PayPal integration (cleaned up) -- `add_to_cart.php` - Add items to cart -- `remove_from_cart.php` - AJAX removal endpoint - -## Development Guidelines - -### Adding New Game Documentation -1. Create folder: `docs/{game-slug}/` -2. Create `metadata.json`: - ```json - { - "name": "Game Name", - "description": "Brief description", - "category": "game", - "order": 100, - "complete": false - } - ``` -3. Create `index.php` following Minecraft template -4. Add optional `icon.png` or `icon.jpg` -5. When complete, set `"complete": true` in metadata - -### Research Sources for Game Documentation -- Official game wikis and documentation -- LGSM (LinuxGSM) scripts and configuration files -- Steam Community Guides (highly-rated) -- Reddit communities (r/gameservers, game-specific) -- GitHub repositories with server configurations -- Official game forums -- User-contributed solutions and fixes - -### Documentation Quality Standards -- **Comprehensive Ports:** List ALL ports with purposes (TCP/UDP) -- **Startup Parameters:** Full parameter explanations with examples -- **Troubleshooting:** Specific common issues with tested solutions -- **Firewall Configs:** Multiple platform examples (Linux + Windows) -- **Citations:** Link to all sources used in research -- **Testing:** Verify all commands and configurations are accurate - -## Next Priorities - -### Phase 2: ARMA Family + DayZ (HIGH PRIORITY) -1. Arma 3 -2. Arma 2: Operation Arrowhead -3. Arma 2: Combined Operations -4. DayZ Standalone -5. DayZ Mod - -### Phase 3: Popular Multiplayer Games -- Counter-Strike family (1.6, Source, CS2, CS:GO) -- Survival/Building (Rust, Terraria, Valheim, ARK) -- Co-op Shooters (L4D series, Killing Floor series, TF2) -- Tactical Shooters (Insurgency series, Squad) - -### Phase 4: Remaining Games -- All other 50+ game folders in alphabetical order - -## Testing Checklist - -### PayPal Integration -- [ ] User can add servers to cart -- [ ] PayPal checkout button works -- [ ] Payment completes successfully -- [ ] Success page displays correctly -- [ ] No `NO_USER_SESSION` errors in logs - -### Documentation System -- [ ] docs.php displays all games correctly -- [ ] TODO prefix shows for incomplete docs -- [ ] Complete docs show without TODO prefix -- [ ] Individual game pages load correctly -- [ ] Navigation links work within documentation - -### Cart Functionality -- [ ] Cart displays all items correctly -- [ ] Remove button deletes items (AJAX) -- [ ] Header and footer display consistently -- [ ] Fonts match other billing pages - -## Technical Notes - -### Session Management -- **CRITICAL:** Always use `session_name("opengamepanel_web")` before `session_start()` -- Sessions are separate from panel sessions -- User authentication stored in `$_SESSION['website_user_id']` - -### Database Connection -- Uses mysqli with credentials from `includes/config.inc.php` -- All database operations use native mysqli functions -- Never use panel-specific functions - -### Standalone Design -- Module must work on external hosting -- No dependencies on panel files -- All paths use `__DIR__` relative references -- MySQL connection direct (not through panel) - -## Support & Resources -- Main project: GameServerPanel/GSP -- Branch: Panel-unstable -- Documentation: Browse `docs.php` for game-specific guides -- Issues: Check logs in `modules/billing/logs/` - ---- -**Last Updated:** December 19, 2024 -**Version:** 2.0 (with Visual TODO System) diff --git a/Panel/modules/billing/README_COUPON_UPDATE.md b/Panel/modules/billing/README_COUPON_UPDATE.md deleted file mode 100644 index eec8a74c..00000000 --- a/Panel/modules/billing/README_COUPON_UPDATE.md +++ /dev/null @@ -1,287 +0,0 @@ -# Billing Module Standalone & Coupon System - Implementation Summary - -## Overview - -This update addresses two major requirements: - -1. **Standalone Billing Module**: The billing module can now operate independently from the panel, either on the same server or on a separate web host. -2. **Enhanced Coupon System**: A comprehensive coupon system with game filters, usage tracking, and permanent/one-time discount options. - -## Changes Made - -### 1. Standalone Database Connection (Critical Fix) - -**Problem**: The billing module was trying to use panel database functions that don't exist when deployed on a separate server, causing PayPal payment processing to fail with "Unexpected end of JSON input" error. - -**Solution**: -- Removed all `require_once` statements that reference panel files like `includes/database_mysqli.php` -- Replaced panel database functions with native mysqli functions -- Created standalone `config.inc.php` file for database credentials -- Updated `api/capture_order.php` to use `mysqli_connect()` instead of `createDatabaseConnection()` - -**Files Modified**: -- `.github/copilot-instructions.md` - Added standalone requirement documentation -- `modules/billing/includes/config.inc.php` - Created from template (should be gitignored in production) -- `modules/billing/api/capture_order.php` - Fixed database connection - -### 2. Enhanced Coupon System - -**Features Implemented**: -- ✅ Create, edit, delete coupons through admin interface -- ✅ Percentage-based discounts (0-100%) -- ✅ One-time vs. permanent discount types -- ✅ Game-specific filtering (all games or specific games) -- ✅ Usage limits and tracking -- ✅ Expiration dates -- ✅ Coupon application in cart with real-time price updates -- ✅ Automatic discount application on payment -- ✅ Discount display in My Servers and Admin Invoices views - -**Files Created**: -- `modules/billing/create_coupons_table.sql` - Database schema -- `modules/billing/admin_coupons.php` - Admin management interface -- `modules/billing/COUPON_SYSTEM.md` - Comprehensive documentation - -**Files Modified**: -- `modules/billing/admin.php` - Added "Manage Coupons" link -- `modules/billing/cart.php` - Added coupon application form and discount logic -- `modules/billing/api/capture_order.php` - Apply coupons on payment, track usage -- `modules/billing/my_servers.php` - Display discount information -- `modules/billing/admin_invoices.php` - Display discount information - -### 3. Database Schema Updates - -**New Table**: `ogp_billing_coupons` -```sql -- coupon_id (primary key) -- code (unique) -- name, description -- discount_percent -- usage_type (one_time/permanent) -- game_filter_type (all_games/specific_games) -- game_filter_list (JSON array of game keys) -- max_uses, current_uses -- expires, is_active -``` - -**Updated Tables**: -- `ogp_billing_invoices`: Added `coupon_id`, `discount_amount` -- `ogp_billing_orders`: Added `coupon_id`, `discount_amount` - -## Installation Instructions - -### Prerequisites -- MySQL/MariaDB database -- PHP 7.4 or higher -- Existing billing module installation - -### Step 1: Create Configuration File - -If deploying on a separate server (not co-located with panel): - -```bash -cd modules/billing/includes/ -cp config.inc.php.orig config.inc.php -``` - -Edit `config.inc.php` with your database credentials: -```php -$db_host = "your-db-host"; -$db_user = "your-db-user"; -$db_pass = "your-db-password"; -$db_name = "your-db-name"; -$table_prefix = "ogp_"; -``` - -**Important**: Add `config.inc.php` to `.gitignore` to prevent committing sensitive credentials. - -### Step 2: Run Database Migration - -```bash -mysql -u [username] -p [database] < modules/billing/create_coupons_table.sql -``` - -Or import via phpMyAdmin. - -### Step 3: Verify Installation - -1. Log in as admin: `/modules/billing/admin.php` -2. Click "Manage Coupons" -3. You should see the coupon management interface with 2 sample coupons - -### Step 4: Test Coupon System - -1. Create a test coupon or use existing "WELCOME10" -2. Add a server to cart: `/modules/billing/order.php` -3. View cart: `/modules/billing/cart.php` -4. Apply coupon code -5. Verify discount is calculated correctly -6. Complete payment (or use free server button if admin) -7. Check My Servers page for discount display - -## Usage - -### For Administrators - -**Create a Coupon**: -1. Navigate to Admin → Manage Coupons -2. Scroll to "Add New Coupon" form -3. Fill in details: - - Code (e.g., "SUMMER25") - - Discount percentage (e.g., 25 for 25% off) - - Usage type (one-time or permanent) - - Game filter (all games or specific) -4. Click "Add Coupon" - -**Monitor Usage**: -- View current uses vs. max uses in coupon list -- Edit or deactivate coupons as needed -- Delete expired or unused coupons - -### For Customers - -**Apply a Coupon**: -1. Add servers to cart -2. On cart page, find "Have a coupon code?" section -3. Enter coupon code -4. Click "Apply Coupon" -5. Prices update automatically -6. Proceed to PayPal checkout - -**View Discounts**: -- Cart page shows applied discount -- My Servers page shows original price (strikethrough) and discounted price -- Coupon code displayed with percentage - -## Coupon Types Explained - -### One-Time Coupons -- Applied to first invoice only -- Renewals use original price -- Example: "WELCOME10" for new customers - -### Permanent Coupons -- Applied to initial purchase AND all renewals -- Discount stored in order record -- Example: "VIP50" for permanent 50% off - -### Game Filters - -**All Games**: -- Coupon applies to any game in cart -- Simplest option for general promotions - -**Specific Games**: -- Define list of game keys -- Only matching games get discount -- Uses partial matching (e.g., "arma3" matches "arma3_linux64") -- Example: Arma-only promotion - -## Troubleshooting - -### PayPal Payment Returns JSON Error - -**Symptom**: "Unexpected end of JSON input" on cart page after PayPal payment - -**Cause**: Missing `config.inc.php` or incorrect database credentials - -**Fix**: -1. Check `/modules/billing/includes/config.inc.php` exists -2. Verify credentials are correct -3. Test database connection: `/modules/billing/test_db_connection.php` -4. Check error logs: `/modules/billing/logs/` and server error log - -### Coupon Not Applying - -**Checks**: -- Code is correct (case-sensitive) -- Coupon is active -- Not expired -- Usage limit not reached -- Game matches filter (for game-specific coupons) - -### Discount Not Showing After Payment - -**Checks**: -- Database schema includes `discount_amount` columns -- `coupon_id` was saved to invoice/order -- Clear browser cache - -## Security Notes - -1. **Sensitive Files**: Add `modules/billing/includes/config.inc.php` to `.gitignore` -2. **Database Credentials**: Use read-only credentials if possible (billing only needs read/write to billing tables) -3. **CSRF Protection**: All admin forms include CSRF tokens -4. **Input Sanitization**: All user inputs are sanitized with `mysqli_real_escape_string()` -5. **SQL Injection**: Parameterized queries or escaped strings throughout - -## File Structure - -``` -modules/billing/ -├── api/ -│ ├── capture_order.php (Modified - standalone DB connection) -│ └── create_order.php -├── includes/ -│ ├── config.inc.php (Created - DB config) -│ └── config.inc.php.orig (Template) -├── admin_coupons.php (Created - Coupon management UI) -├── admin_invoices.php (Modified - Show discounts) -├── cart.php (Modified - Coupon application) -├── my_servers.php (Modified - Show discounts) -├── admin.php (Modified - Added coupon link) -├── create_coupons_table.sql (Created - DB schema) -└── COUPON_SYSTEM.md (Created - Documentation) -``` - -## Testing Checklist - -- [ ] Database migration ran successfully -- [ ] Admin can access coupon management page -- [ ] Can create new coupon (all games) -- [ ] Can create game-specific coupon -- [ ] Can edit existing coupon -- [ ] Can delete coupon -- [ ] Customer can apply coupon in cart -- [ ] Cart prices update with discount -- [ ] Free server creation works (if admin) -- [ ] PayPal payment processes successfully -- [ ] Coupon usage count increments -- [ ] One-time coupon clears after payment -- [ ] Permanent coupon stays in order -- [ ] Discount shows on My Servers page -- [ ] Discount shows on Admin Invoices page -- [ ] Expired coupons are rejected -- [ ] Max uses limit is enforced -- [ ] Game filter works correctly - -## Known Limitations - -1. Coupons are percentage-based only (no fixed-amount discounts) -2. No minimum purchase requirement -3. No user-specific targeting (all users can use any active coupon) -4. No coupon stacking (one coupon per order) -5. Game matching uses partial string match (may need refinement) - -## Future Enhancements - -- Fixed-amount coupons (e.g., $5 off) -- Minimum purchase requirements -- User-specific or group-specific coupons -- Referral system integration -- Automatic coupon generation for campaigns -- Analytics dashboard -- Email notifications on coupon usage - -## Support & Documentation - -- Full documentation: `modules/billing/COUPON_SYSTEM.md` -- Copilot instructions: `.github/copilot-instructions.md` -- Issue tracker: GitHub Issues - ---- - -**Version**: 1.0 -**Date**: October 29, 2025 -**Author**: Copilot Agent -**Tested**: Manual testing completed diff --git a/Panel/modules/billing/RECENT_FIXES_SUMMARY.md b/Panel/modules/billing/RECENT_FIXES_SUMMARY.md deleted file mode 100644 index 8735db3d..00000000 --- a/Panel/modules/billing/RECENT_FIXES_SUMMARY.md +++ /dev/null @@ -1,326 +0,0 @@ -# Recent Fixes & Enhancements Summary -**Date:** December 19, 2024 (Updated) - -## Phase 1: Visual TODO System Implementation ✅ **NEW** - -### Overview -Implemented comprehensive system to visually identify incomplete game documentation across the entire billing website. All game documentation folders now have completion tracking. - -### Changes Made - -#### 1. Metadata Enhancement System -- **Created:** `update_metadata_complete.ps1` - PowerShell script for batch metadata updates -- **Updated:** 146 metadata.json files across all game documentation folders -- **New Field:** Added `"complete": false` to mark documentation status -- **Exception:** Minecraft marked as `"complete": true` (serves as complete template) - -#### 2. Documentation Display Logic -- **File:** `modules/billing/docs.php` -- **Enhancement:** Added automatic "TODO: " prefix for incomplete documentation -- **Logic:** - ```php - $isComplete = isset($metadata['complete']) ? (bool)$metadata['complete'] : false; - if (!$isComplete) { - $displayName = 'TODO: ' . $displayName; - } - ``` -- **Result:** Users immediately see which games need comprehensive documentation - -#### 3. Visual Impact on docs.php -**Complete Documentation (no prefix):** -- ✅ Minecraft Server - -**Incomplete Documentation (TODO prefix):** -- ❌ TODO: Arma 3 -- ❌ TODO: Arma 2: Operation Arrowhead -- ❌ TODO: Arma 2: Combined Operations -- ❌ TODO: DayZ -- ❌ TODO: Rust -- ❌ TODO: Counter-Strike: Global Offensive -- ❌ TODO: Garry's Mod -- ❌ TODO: Valheim -- ❌ TODO: Terraria -- ❌ TODO: Left 4 Dead 2 -- ❌ TODO: Team Fortress 2 -- ❌ TODO: ARK: Survival Evolved -- ...and 134 more games - -### Minecraft Documentation Template (Complete Example) -**File:** `modules/billing/docs/minecraft/index.php` -**Status:** Complete (~550 lines) -**Includes:** -- 📚 Navigation with anchor links -- 🔌 Comprehensive ports table (all ports with purposes) -- ⚙️ Startup parameters (JVM flags, optimizations) -- 🔧 Troubleshooting sections (specific solutions) -- 🔥 Firewall configs (UFW, FirewallD, Windows, iptables) -- 🔒 Security best practices -- ⚡ Performance optimization tips -- 🔗 Resource links with citations - ---- - -## Critical Fixes Completed ✅ - -### 1. PayPal Payment Capture Session Issue (FIXED) -**Problem:** Payment capture was failing with `NO_USER_SESSION` error even though user was logged in. - -**Root Cause:** The `api/capture_order.php` file was calling `session_start()` without setting the session name first, so it couldn't access the `opengamepanel_web` session where the user_id is stored. - -**Solution:** Added `session_name("opengamepanel_web")` before `session_start()` in `capture_order.php`. - -**File Modified:** `modules/billing/api/capture_order.php` (line ~148) - -**Test Steps:** -1. Log into the billing site -2. Add a server to cart -3. Click PayPal checkout button -4. Complete payment in PayPal sandbox -5. Verify payment completes successfully and redirects to success page -6. Check `modules/billing/logs/payment_capture.log` - should no longer show `NO_USER_SESSION` error - ---- - -### 2. Cart Page Debug Logging Removed (COMPLETED) -**What Was Removed:** -- Shutdown function that logged to `data/debug_cart.log` -- `?debug_cart=1` parameter handling -- Debug error display code - -**File Modified:** `modules/billing/cart.php` (lines 1-30) - -**Result:** Cart page now runs in production mode without debug overhead. - ---- - -### 3. Cart Page Header/Footer Consistency (FIXED) -**Problem:** Cart page had different fonts and styling than other billing pages; missing footer entirely. - -**Solutions Applied:** -1. Added `include(__DIR__ . '/includes/top.php');` before menu -2. Added `include(__DIR__ . '/includes/footer.php');` at page end -3. Removed global `font-family` and `background` override from inline CSS -4. Added favicon links to match other pages - -**Files Modified:** -- `modules/billing/cart.php` (head section and body closing) - -**Result:** Cart page now has consistent header/menu/footer with rest of billing module. - ---- - -## Documentation Enhancements Started 📚 - -### 4. Minecraft Documentation Updated (TEMPLATE CREATED) -**What Was Added:** -- Comprehensive **Ports section** with table showing all ports (TCP 25565, UDP 25565, TCP 25575, UDP 19132) -- Port purposes clearly explained -- Firewall configuration examples for multiple platforms -- Security notes for RCON and port protection -- Enhanced navigation with icons (🔌 Ports, ⚙️ Startup Parameters, 🔧 Troubleshooting) - -**File Modified:** `modules/billing/docs/minecraft/index.php` - -**Template Pattern Established:** -- ✅ Quick Info section (at top) -- ✅ Ports section with complete table -- ✅ Installation steps -- ✅ Configuration examples -- ✅ Startup Parameters section (already excellent) -- ✅ Troubleshooting section (already comprehensive) -- ✅ Performance optimization -- ✅ Security best practices - ---- - -## Remaining Documentation Work 📋 - -### Games Needing Full Port/Parameter/Troubleshooting Docs - -The following games need their `docs/{game}/index.php` files updated with the Minecraft template pattern: - -#### High Priority Games (Popular): -1. **Counter-Strike: Global Offensive** (`csgo/`) -2. **Team Fortress 2** (`tf2/`) -3. **Garry's Mod** (`garrysmod/`) -4. **Rust** (`rust/`) -5. **ARK: Survival Evolved** (`arkse/`) -6. **Terraria** (`terraria/`) -7. **Valheim** (`valheim/`) -8. **7 Days to Die** (`7daystodie/`) -9. **DayZ** (`dayz/`) -10. **Left 4 Dead 2** (`left4dead2/`) - -#### Medium Priority: -11. Counter-Strike Source (`css/`) -12. Arma 3 (`arma3/`) -13. Squad (`squad/`) -14. Insurgency Sandstorm (`insurgencysandstorm/`) -15. Space Engineers (`space_engineers/`) -16. Conan Exiles (`conanexiles/`) -17. The Forest (`theforest/`) -18. Don't Starve Together (`dontstarvetogether/`) -19. Factorio (`factorio/`) -20. TeamSpeak 3 (`teamspeak3/`) - -#### Lower Priority (Legacy/Niche): -21. All remaining games in `modules/billing/docs/` - ---- - -### Research Needed Per Game - -For each game, research and document: - -1. **All Network Ports:** - - Game port (TCP/UDP) - - Query port - - RCON/Admin port - - Voice chat ports (if applicable) - - Steam port (if Steam-based) - - Additional service ports (web interfaces, etc.) - -2. **Startup Parameters:** - - Command-line flags - - Memory allocation - - Server configuration switches - - Performance optimization flags - -3. **Common Issues (from internet research):** - - "Server won't start" specific to that game - - Connection problems - - Performance/lag issues specific to game engine - - Mod/plugin conflicts - - Save corruption issues - - Update/patch problems - -4. **Game-Specific Configuration:** - - Main config file locations - - Critical settings - - Player limits - - World/map settings - ---- - -### Documentation Template Structure - -Each game's `index.php` should follow this structure: - -```php - - - -
-

📚 Quick Navigation

-
- Quick Info - 🔌 Ports - Installation - Configuration - ⚙️ Startup Parameters - 🔧 Troubleshooting - Performance -
-
- -

{Game Name} Server Hosting Guide

- -

Quick Info

- - -

🔌 Network Ports Used

- - - - -

Installation & Setup

- - -

Server Configuration

- - -

⚙️ Startup Parameters

- - - -

🔧 Troubleshooting

- - - - - - -

Performance Optimization

- - - - -``` - ---- - -## Testing Checklist - -### PayPal Payment Flow: -- [ ] Log into billing site -- [ ] Add server to cart -- [ ] Apply coupon (optional) -- [ ] Click PayPal button -- [ ] Complete sandbox payment -- [ ] Verify success page loads -- [ ] Check invoice marked as paid in database -- [ ] Verify no `NO_USER_SESSION` in `logs/payment_capture.log` - -### Cart Page: -- [ ] Cart page loads with correct header/menu (same font as index.php) -- [ ] Footer appears with timestamp -- [ ] Favicon displays in browser tab -- [ ] Remove item (trash icon) works via AJAX -- [ ] Cart refreshes without full page reload after removal -- [ ] Database row hard-deleted (invoice removed from table) - -### Documentation: -- [ ] Navigate to `/docs.php` (or docs index) -- [ ] Click on Minecraft documentation -- [ ] Verify new Ports section displays correctly -- [ ] Verify navigation links jump to correct sections -- [ ] Test on mobile/tablet for responsive layout - ---- - -## Next Steps (Priority Order) - -1. **Test PayPal payment flow end-to-end** (sandbox environment) -2. **Verify cart removal functionality** (AJAX + database deletion) -3. **Begin documentation expansion:** - - Start with top 10 popular games - - Research ports/parameters/issues for each - - Update docs using Minecraft template - - Test navigation and layout -4. **Consider automation:** - - Script to validate all game docs have required sections - - Port information database/reference - - Common troubleshooting template generator - ---- - -## Files Modified in This Session - -1. `modules/billing/api/capture_order.php` - Fixed session name issue -2. `modules/billing/cart.php` - Removed debug logging, fixed header/footer -3. `modules/billing/docs/minecraft/index.php` - Added ports section, enhanced navigation - -## Files to Review - -- `modules/billing/logs/payment_capture.log` - Check for successful captures -- `modules/billing/data/debug_cart.log` - Should no longer be written to -- Database table `{$table_prefix}billing_invoices` - Verify removals are hard-deleted - ---- - -**End of Summary** - diff --git a/Panel/modules/billing/STATUS_REPORT.md b/Panel/modules/billing/STATUS_REPORT.md deleted file mode 100644 index 784f0d00..00000000 --- a/Panel/modules/billing/STATUS_REPORT.md +++ /dev/null @@ -1,176 +0,0 @@ -# Billing Module Status Report -**Date:** November 7, 2025 -**Branch:** copilot/update-billing-table-prefix - -## ✅ Completed Tasks - -### 1. Table Prefix Updates -- **Status:** ✅ COMPLETE -- **Changes:** - - All SQL files updated to use hardcoded `gsp_` prefix - - `config.inc.php` default changed from `ogp_` to `gsp_` - - Panel tables (like `ogp_users`) correctly left unchanged - - All references properly updated in: - - create_invoices_table.sql - - create_coupons_table.sql - - migration_to_invoices.sql - - add_paypal_data_column.sql - - add_service_id_column.sql - - fix_invoices_table_columns.sql - -### 2. Documentation System -- **Status:** ✅ COMPLETE -- **Implementation:** - - New `/modules/billing/docs.php` browser created - - Category-based organization (game, panel, mods, troubleshooting, other) - - Each doc folder contains: - - `index.php` - Documentation content - - `metadata.json` - Category, name, description, order - - `icon.png/jpg` - Visual icon - - Smart sorting by category and order number - - Clean, dark-themed UI matching site design - - Back button navigation - - "Documentation" link added to main menu - - Old docs preserved in `/docs_old/` for reference - - Complete README.md with instructions - -**Example Documentation Created:** -- Minecraft Server Guide (game category) -- Getting Started (panel category) -- Common Issues & Solutions (troubleshooting category) - -### 3. PayPal Integration -- **Status:** ✅ COMPLETE (Core Functionality) -- **Components:** - - `api/create_order.php` - Creates PayPal orders with comprehensive logging - - `api/capture_order.php` - Captures payments and marks invoices paid - - `webhook.php` - Handles PayPal webhooks with signature verification - - All use standalone mysqli (no panel dependencies) - - Full logging system for debugging - - Secure error handling - -**Payment Flow:** -1. User views cart with unpaid invoices -2. Clicks PayPal button → creates order via API -3. Completes payment on PayPal -4. capture_order.php marks invoices paid, creates orders -5. Webhook confirms payment asynchronously -6. Success page shows confirmation - -## ⚠️ Partially Complete - -### Coupon System -- **Status:** ⚠️ BACKEND READY, FRONTEND MISSING -- **What Exists:** - - ✅ Database schema (`gsp_billing_coupons` table) - - ✅ Admin interface (`admin_coupons.php`) - - ✅ Coupon CRUD operations - - ✅ Fields in invoices/orders for coupon tracking - - ✅ Comprehensive documentation (COUPON_SYSTEM.md) - -- **What's Missing:** - - ❌ Coupon input/validation in cart.php - - ❌ Discount calculation in checkout - - ❌ Session storage of applied coupons - - ❌ Coupon usage tracking on payment - -**Impact:** Coupons can be created by admins but customers cannot apply them during checkout. - -**Recommendation:** The problem statement asks to "verify all the paypal payment works and is complete with coupons". The PayPal payment WORKS but coupon integration in the checkout flow needs to be implemented to match the COUPON_SYSTEM.md documentation. - -## 📋 Other Findings - -### Inconsistencies Found - -1. **Mixed URL Patterns** - - Some files use absolute URLs correctly - - create_order.php has hardcoded site base URL instead of using config - - Recommendation: Use `$SITE_BASE_URL` from config consistently - -2. **Session Namespaces** - - Most files use `website_user_id` session variable - - Some fallback to `user_id` - - Recommendation: Standardize on `website_user_id` - -3. **Error Handling** - - Most files have good error handling - - A few older files could use try/catch blocks - - Recommendation: Audit older PHP files for error handling - -4. **Documentation Markdown Files** - - Multiple .md files in root of billing module - - Could be consolidated or moved to docs folder - - Recommendation: Create a `/docs/developer/` category for technical docs - -### SQL Files Status -All SQL files properly use `gsp_` prefix: -- ✅ create_invoices_table.sql -- ✅ create_coupons_table.sql -- ✅ migration_to_invoices.sql -- ✅ add_paypal_data_column.sql -- ✅ add_service_id_column.sql -- ✅ fix_invoices_table_columns.sql - -### Configuration Files -- ✅ `config.inc.php` - Default prefix is `gsp_` -- ✅ Standalone compatible (no panel includes) -- ✅ Database connection using mysqli - -## 🎯 Recommended Next Steps - -### Priority 1: Complete Coupon Integration -To match COUPON_SYSTEM.md documentation, implement in cart.php: -1. Add coupon input field -2. AJAX endpoint to validate and apply coupons -3. Discount calculation in cart totals -4. Store applied coupon in session -5. Pass coupon to payment processor -6. Update invoices with coupon_id on payment -7. Increment usage counter -8. Handle one-time vs permanent coupons - -### Priority 2: Testing -1. Test PayPal sandbox end-to-end -2. Test invoice creation → cart → payment → success -3. Test webhook signature verification -4. Test error scenarios (payment failure, timeout, etc.) -5. Once coupons implemented, test coupon application - -### Priority 3: Documentation -1. Move developer .md files to `/docs/developer/` category -2. Create user-facing coupon documentation in docs system -3. Add payment troubleshooting guide - -### Priority 4: Code Quality -1. Audit older PHP files for error handling -2. Standardize session variable names -3. Use config SITE_BASE_URL consistently -4. Add input validation where missing - -## 📊 Summary - -### What Works Now -- ✅ Table prefixes corrected to `gsp_` -- ✅ Documentation system fully functional -- ✅ PayPal payment processing complete -- ✅ Coupon admin management ready -- ✅ Standalone deployment compatible - -### What Needs Work -- ❌ Coupon checkout integration -- ⚠️ Some minor inconsistencies (URLs, sessions) -- ⚠️ Testing needed for full payment flow - -### Files Modified in This PR -- SQL files (6 files) - table prefix updates -- config.inc.php - default prefix change -- docs.php (new) - documentation browser -- docs/ folder - restructured with examples -- includes/menu.php - added Documentation link -- STATUS_REPORT.md (this file) - -### Files in docs_old/ (preserved for reference) -- 206 game markdown files -- Old docs.php, server.php, game.php -- all_hostable_games_union.csv - diff --git a/Panel/modules/billing/TESTING_CHECKLIST.md b/Panel/modules/billing/TESTING_CHECKLIST.md deleted file mode 100644 index 2ca51a55..00000000 --- a/Panel/modules/billing/TESTING_CHECKLIST.md +++ /dev/null @@ -1,339 +0,0 @@ -# Testing Checklist for Billing Invoice/Order Flow Fixes - -## Prerequisites - -1. **Database Setup** - - [ ] Verify `ogp_billing_invoices` table exists - - [ ] Verify `ogp_billing_orders` table exists - - [ ] Verify tables have all required columns (see create_invoices_table.sql) - -2. **Configuration** - - [ ] Copy `modules/billing/includes/config.inc.php.orig` to `modules/billing/includes/config.inc.php` - - [ ] Update database credentials in config.inc.php - - [ ] Verify `$table_prefix` is set correctly (default: "ogp_") - - [ ] Verify `$SITE_DATA_DIR` path is writable - -3. **PayPal Configuration** - - [ ] Verify sandbox client_id and client_secret in api/create_order.php - - [ ] Verify sandbox client_id and client_secret in api/capture_order.php - - [ ] Verify webhook_id in webhook.php - -## Test 1: Add to Cart (Invoice Creation) - -**Test NEW Order Flow** - -1. Navigate to order.php -2. Select a game server configuration -3. Set price to $0.00 for testing (or use regular price) -4. Fill in all required fields -5. Click "Add to Cart" - -**Expected Results:** -- [ ] Redirects to cart.php -- [ ] Item appears in cart -- [ ] Database check: Invoice created in `ogp_billing_invoices` - - [ ] status = 'due' - - [ ] order_id = 0 (no order yet) - - [ ] user_id matches logged-in user - - [ ] amount, qty, service_id populated correctly - -**Verification SQL:** -```sql -SELECT * FROM ogp_billing_invoices WHERE status='due' ORDER BY invoice_id DESC LIMIT 5; -``` - -## Test 2: Free Button (Manual Order Creation) - -**Test Free/Claim Flow** - -1. Ensure you have item in cart with amount = 0.00 -2. Click "Claim (Free)" button - -**Expected Results:** -- [ ] Redirects to return.php -- [ ] Shows payment confirmation -- [ ] Invoice marked as paid -- [ ] Order created -- [ ] Cart is empty - -**Verification SQL:** -```sql --- Check invoice was marked paid -SELECT invoice_id, status, paid_date, order_id FROM ogp_billing_invoices -WHERE status='paid' ORDER BY invoice_id DESC LIMIT 1; - --- Check order was created -SELECT order_id, user_id, status, end_date, payment_txid FROM ogp_billing_orders -ORDER BY order_id DESC LIMIT 1; - --- Verify link -SELECT i.invoice_id, i.order_id, o.order_id -FROM ogp_billing_invoices i -LEFT JOIN ogp_billing_orders o ON i.order_id = o.order_id -WHERE i.status='paid' ORDER BY i.invoice_id DESC LIMIT 5; -``` - -**Check Logs:** -```bash -tail -50 modules/billing/logs/site.log | grep -E "(payment|free_create)" -``` - -## Test 3: PayPal Payment Flow - -**Test PayPal Checkout** - -1. Add paid item to cart (e.g., $5.00) -2. Click PayPal button in cart -3. Should redirect to PayPal sandbox -4. Login with sandbox buyer account -5. Approve payment -6. Should return to payment_success.php - -**Expected Results:** -- [ ] PayPal button renders correctly -- [ ] Creates PayPal order (check browser console for order ID) -- [ ] Redirects to PayPal sandbox -- [ ] After approval, returns to payment_success.php -- [ ] No JavaScript errors in console -- [ ] No "Unexpected end of JSON input" error -- [ ] Invoice marked as paid -- [ ] Order created -- [ ] Cart is empty - -**Browser Console Checks:** -``` -Look for: -✓ "PayPal cart debug: ..." - Shows cart data -✓ "Creating order..." - Order creation started -✓ "Order created." - Order creation succeeded -✓ "Capturing payment..." - Capture started -✗ Any errors - Should be none -``` - -**Verification SQL:** -```sql --- Check invoice -SELECT invoice_id, status, paid_date, payment_txid, payment_method, order_id -FROM ogp_billing_invoices -WHERE payment_method='paypal' -ORDER BY invoice_id DESC LIMIT 1; - --- Check order -SELECT order_id, user_id, status, price, end_date, payment_txid -FROM ogp_billing_orders -WHERE payment_txid LIKE '%' -ORDER BY order_id DESC LIMIT 1; -``` - -**Check API Logs:** -```bash -# Check create_order.php payload -cat modules/billing/data/create_order_payload.log - -# Check corrected URLs -cat modules/billing/data/corrected_urls.log - -# Check for errors -cat modules/billing/data/create_order_errors.log -``` - -## Test 4: Webhook Processing - -**Test Webhook Handler** - -1. Trigger a PayPal payment (from Test 3) -2. PayPal will send webhook to webhook.php - -**Expected Results:** -- [ ] Webhook receives POST from PayPal -- [ ] Signature verification succeeds -- [ ] Payment record processed -- [ ] Invoice marked paid (if not already) -- [ ] Order created/updated (if not already) - -**Verification:** -```bash -# Check webhook log -tail -50 modules/billing/data/webhook.log - -# Check for payment processing -grep "process_payment" modules/billing/data/webhook.log -``` - -**Check Data Files:** -```bash -ls -lah modules/billing/data/*.json -cat modules/billing/data/INV-*.json # Check payment record format -``` - -## Test 5: Renewal Flow - -**Setup Renewal Invoice** - -1. Create a test order manually: -```sql -INSERT INTO ogp_billing_orders ( - user_id, service_id, home_name, ip, max_players, qty, invoice_duration, - price, remote_control_password, ftp_password, status, order_date, end_date, - payment_txid, paid_ts -) VALUES ( - 1, 1, 'Test Server', 1, 10, 1, 'month', - 5.00, 'rconpass', 'ftppass', 'paid', NOW(), DATE_ADD(NOW(), INTERVAL 1 MONTH), - 'TEST-INITIAL', NOW() -); -``` - -2. Get the order_id from the insert: -```sql -SELECT LAST_INSERT_ID(); -``` - -3. Create renewal invoice: -```sql -INSERT INTO ogp_billing_invoices ( - order_id, user_id, service_id, home_name, ip, max_players, qty, invoice_duration, - amount, status, customer_name, customer_email, due_date, description -) VALUES ( - LAST_INSERT_ID(), -- Use order_id from step 2 - 1, 1, 'Test Server', 1, 10, 1, 'month', - 5.00, 'due', 'Test User', 'test@test.com', DATE_ADD(NOW(), INTERVAL 3 DAY), - 'Renewal invoice' -); -``` - -**Test Renewal Payment** - -1. Log in as user who owns the order -2. View cart - should show renewal invoice -3. Pay using free button or PayPal - -**Expected Results:** -- [ ] Invoice marked as paid -- [ ] Original order's end_date extended by 1 month -- [ ] No duplicate order created -- [ ] Invoice.order_id still points to original order - -**Verification SQL:** -```sql --- Check order end_date was extended -SELECT order_id, end_date, status, payment_txid -FROM ogp_billing_orders -WHERE order_id = ; - --- Should show end_date = original end_date + 1 month - --- Check invoice -SELECT invoice_id, order_id, status, paid_date -FROM ogp_billing_invoices -WHERE order_id = ; - --- Should show paid invoice linked to same order_id -``` - -## Test 6: Error Handling - -**Test Invalid Scenarios** - -1. **Missing session**: Try to pay without being logged in - - [ ] Should redirect to login or show error - -2. **Database connection failure**: Temporarily break DB config - - [ ] capture_order.php should return JSON error, not crash - - [ ] Error should be logged - -3. **PayPal API failure**: Use invalid credentials - - [ ] Should show error in console - - [ ] Should log error - - [ ] Should not corrupt database - -## Common Issues and Solutions - -### Issue: "Config file not found" -**Solution**: Copy config.inc.php.orig to config.inc.php - -### Issue: "Table doesn't exist" -**Solution**: Run create_invoices_table.sql - -### Issue: "Permission denied writing to data/" -**Solution**: -```bash -chmod 775 modules/billing/data -chown www-data:www-data modules/billing/data # Or your web server user -``` - -### Issue: "PayPal button doesn't render" -**Solution**: Check browser console for errors, verify client_id - -### Issue: "Unexpected end of JSON input" -**Solution**: -- Check PHP error log: `tail -f /var/log/php/error.log` -- Verify display_errors=0 in capture_order.php -- Check for syntax errors: `php -l api/capture_order.php` - -### Issue: "Cart still shows items after payment" -**Solution**: -- Check if invoice status changed to 'paid' -- Check if process_payment_record was called -- Check logs for errors - -## Performance Testing - -**Test with Multiple Items** - -1. Add 5 items to cart -2. Pay with PayPal -3. Verify all 5 invoices marked paid -4. Verify all 5 orders created -5. Verify all linked correctly - -**Test Concurrent Payments** - -1. Add item to cart in two different browsers (same user) -2. Attempt to pay both simultaneously -3. Verify both process correctly -4. Check for race conditions - -## Security Testing - -**Test SQL Injection** - -1. Try adding special characters to form fields -2. Try manipulating invoice_id in POST requests -3. Verify all inputs are sanitized/escaped - -**Test Session Hijacking** - -1. Try accessing cart with invalid session -2. Try paying for someone else's invoice -3. Verify proper authorization checks - -**Test Webhook Signature** - -1. Send fake webhook without valid signature -2. Verify it's rejected -3. Check logs for security events - -## Cleanup - -After testing, clean up test data: - -```sql --- Remove test invoices -DELETE FROM ogp_billing_invoices WHERE customer_email = 'test@test.com'; - --- Remove test orders -DELETE FROM ogp_billing_orders WHERE remote_control_password = 'rconpass'; -``` - -## Sign-off - -- [ ] All tests passed -- [ ] No errors in logs -- [ ] Documentation reviewed -- [ ] Security checks completed -- [ ] Ready for production deployment - -**Tested by**: _______________ -**Date**: _______________ -**Environment**: _______________ (Dev/Staging/Production) -**Notes**: _______________ diff --git a/Panel/modules/billing/_archived/CONFIGURATION.md b/Panel/modules/billing/_archived/CONFIGURATION.md deleted file mode 100644 index b5970707..00000000 --- a/Panel/modules/billing/_archived/CONFIGURATION.md +++ /dev/null @@ -1,165 +0,0 @@ -# Website Configuration Guide - -## Overview - -The `_website` folder is now a standalone site with centralized database configuration. All database connection settings are managed in a single location: `includes/config.inc.php`. - -## Directory Structure - -``` -_website/ -├── includes/ -│ ├── config.inc.php # Central database configuration -│ └── README.md # Documentation for includes directory -├── db.php # Database connection (loads config.inc.php) -├── login.php # Uses db.php -├── logout.php # Uses db.php -├── cart.php # Uses db.php -├── order.php # Uses db.php -├── serverlist.php # Uses db.php -└── ...other files -``` - -## Configuration File - -### Location -`_website/includes/config.inc.php` - -### Contents -```php - -``` - -## How It Works - -1. **Configuration Loading** - - Website files include `db.php` - - `db.php` loads `includes/config.inc.php` - - Configuration variables are available to all files - -2. **Configuration Flow** - ``` - includes/config.inc.php → db.php → website files - ``` - -3. **Database Connection** - - `db.php` uses the configuration variables to establish a connection - - Returns `$db` variable containing the mysqli connection - -## Setup Instructions - -### For Standalone Use - -1. **Copy the _website folder** to your web server -2. **Edit configuration**: - ```bash - nano _website/includes/config.inc.php - ``` -3. **Update database credentials**: - - Set `$db_host` to your database server - - Set `$db_user` to your database username - - Set `$db_pass` to your database password - - Set `$db_name` to your database name -4. **Verify permissions**: - ```bash - chmod 600 _website/includes/config.inc.php - ``` - -### For Panel Integration - -The configuration in `_website/includes/config.inc.php` should match the panel's configuration in `/includes/config.inc.php` to ensure both the website and panel access the same database. - -## Security Best Practices - -1. **File Permissions**: Set `config.inc.php` to read-only for the web server user - ```bash - chmod 600 includes/config.inc.php - ``` - -2. **Web Server Configuration**: Ensure the `includes/` directory is not directly accessible via HTTP - ```apache - - Require all denied - - ``` - -3. **Backup Configuration**: Keep a secure backup of your configuration file - -## Troubleshooting - -### Connection Errors - -If you see database connection errors: - -1. **Verify credentials** in `includes/config.inc.php` -2. **Check database server** is running -3. **Verify database exists** -4. **Check user permissions** in the database - -### File Not Found Errors - -If you see errors about missing `config.inc.php`: - -1. **Verify the file exists** at `_website/includes/config.inc.php` -2. **Check file permissions** are readable by the web server -3. **Verify path** in `db.php` uses `__DIR__` for relative paths - -### Include Errors - -If website files can't include `db.php`: - -1. **Check file paths** are correct -2. **Verify `db.php`** exists in the `_website/` root -3. **Check PHP include paths** in php.ini if needed - -## Migration from Old Configuration - -The old `db.php` had hardcoded credentials: -```php -// OLD (hardcoded) -$servername = "panel.iaregamer.com"; -$username = "remoteuser"; -``` - -The new `db.php` uses centralized config: -```php -// NEW (centralized) -require_once(__DIR__ . '/includes/config.inc.php'); -$servername = $db_host; -$username = $db_user; -``` - -**No changes needed** to files that include `db.php` - they work automatically with the new configuration. - -## Files Using Database Connection - -The following files include `db.php` and use the centralized configuration: -- `login.php` - User authentication -- `logout.php` - Session termination -- `cart.php` - Shopping cart -- `order.php` - Order processing -- `serverlist.php` - Server listings -- `adminserverlist.php` - Admin server management -- `test_db_connection.php` - Database testing - -## Benefits - -1. **Single Source of Truth**: All database settings in one file -2. **Easy Configuration**: Change settings in one place -3. **Portable**: Copy folder and update one config file -4. **Secure**: Configuration separate from code -5. **Maintainable**: Easy to update and manage - -## Support - -For issues or questions about the configuration, please refer to: -- `includes/README.md` - Detailed information about includes directory -- Main project documentation -- Panel configuration at `/includes/config.inc.php` diff --git a/Panel/modules/billing/_archived/FEATURES.md b/Panel/modules/billing/_archived/FEATURES.md deleted file mode 100644 index 99671da0..00000000 --- a/Panel/modules/billing/_archived/FEATURES.md +++ /dev/null @@ -1,383 +0,0 @@ -# Website Features Documentation - -This document describes the new features added to the GameServers.World website (_website folder). - -## Table of Contents - -1. [Password Reset System](#password-reset-system) -2. [My Servers Dashboard](#my-servers-dashboard) -3. [Server Status Page](#server-status-page) -4. [UI Improvements](#ui-improvements) -5. [Apache Configuration](#apache-configuration) - ---- - -## Password Reset System - -A complete password reset workflow has been implemented to allow users to recover their accounts. - -### Files Created - -- **forgot_password.php** - Request password reset -- **reset_password.php** - Reset password with token - -### How It Works - -1. User visits the login page and clicks "Forgot Password?" -2. User enters their username or email on `forgot_password.php` -3. System generates a secure token and stores it in `ogp_password_reset_tokens` table -4. Email is sent with reset link (falls back to displaying link if email fails) -5. User clicks link and is taken to `reset_password.php?token=XXX` -6. User enters new password (min 8 characters) -7. Password is updated using both MD5 (panel compatibility) and modern hash (if shadow column exists) -8. Token is marked as used - -### Database Table - -The system automatically creates this table if it doesn't exist: - -```sql -CREATE TABLE ogp_password_reset_tokens ( - id INT AUTO_INCREMENT PRIMARY KEY, - user_id INT NOT NULL, - token VARCHAR(64) NOT NULL, - expires DATETIME NOT NULL, - used TINYINT(1) DEFAULT 0, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - INDEX idx_token (token), - INDEX idx_user_id (user_id) -) -``` - -### Security Features - -- Tokens expire after 1 hour -- Tokens can only be used once -- Secure random token generation (64 hex characters) -- Password requirements enforced (min 8 chars) -- Passwords hashed with both MD5 (panel) and bcrypt (modern) -- User enumeration protection (doesn't reveal if account exists) - -### Email Configuration - -The system uses PHP's `mail()` function. For production: - -1. Configure your server's mail system (sendmail, postfix, etc.) -2. Or integrate with an email service (SendGrid, Mailgun, etc.) -3. Update the email headers in `forgot_password.php` as needed - ---- - -## My Servers Dashboard - -A user dashboard showing all active game servers with renewal options. - -### File Created - -- **my_servers.php** - User's server management dashboard -- **renew_server.php** - Server renewal page - -### Features - -- **Server List**: Shows all servers owned by logged-in user -- **Server Details**: Name, game type, location, status -- **Expiration Tracking**: Shows expiration date for each server -- **Status Indicators**: Active, Inactive, Expired -- **Renewal Links**: Quick access to renew each server -- **Empty State**: Helpful message when user has no servers - -### Access - -- Menu link "My Servers" appears when user is logged in -- Requires authentication via `login_required.php` - -### Database Query - -Joins multiple tables: -- `ogp_home` - Server instances -- `ogp_remote_servers` - Server locations -- `ogp_game_configs` - Game information -- `ogp_billing_orders` - Order/expiration data -- `ogp_billing_services` - Service pricing - ---- - -## Server Status Page - -Public page showing real-time status of all game server infrastructure. - -### File Created - -- **server_status.php** - Server infrastructure status - -### Features - -- **Real-time Status**: Online, Offline, Maintenance, Unknown -- **Resource Usage**: CPU, Memory, Disk usage percentages -- **Uptime Display**: How long each server has been running -- **Last Updated**: Time since last status update -- **Color-coded Badges**: Visual status indicators -- **Notes Support**: Display maintenance or status messages - -### Database Table - -Automatically creates table if it doesn't exist: - -```sql -CREATE TABLE ogp_server_status ( - status_id INT AUTO_INCREMENT PRIMARY KEY, - remote_server_id INT NOT NULL, - server_name VARCHAR(255) NOT NULL, - ip_address VARCHAR(45), - status ENUM('online', 'offline', 'maintenance') DEFAULT 'offline', - cpu_usage DECIMAL(5,2), - memory_usage DECIMAL(5,2), - disk_usage DECIMAL(5,2), - uptime VARCHAR(50), - last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - notes TEXT, - INDEX idx_remote_server (remote_server_id), - UNIQUE KEY unique_server (remote_server_id) -) -``` - -### Server Updates - -The page displays data from `ogp_server_status`. Servers should update this table: - -```php -// Example server update code (run on each server periodically) -$stmt = $db->prepare("INSERT INTO ogp_server_status - (remote_server_id, server_name, ip_address, status, cpu_usage, memory_usage, disk_usage, uptime, notes) - VALUES (?, ?, ?, 'online', ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE - status = VALUES(status), - cpu_usage = VALUES(cpu_usage), - memory_usage = VALUES(memory_usage), - disk_usage = VALUES(disk_usage), - uptime = VALUES(uptime), - notes = VALUES(notes), - last_updated = NOW()"); -``` - -### Access - -- Link in footer: "Server Status" -- Public page (no login required) - ---- - -## UI Improvements - -### Server List Page - -**Before**: "Order Server" was a plain link -**After**: Styled as a button with gradient background - -```html - - Order Now - -``` - -### Order Page - -**Fixed**: Game images now display correctly -- Changed from `src=""` -- To `src="../"` -- Assumes images are stored relative to panel root - -### Login Page - -**Added**: "Forgot Password?" link next to Register link - -### Navigation Menu - -**Added**: "My Servers" link for logged-in users -- Only visible when user is authenticated -- Positioned between "Game Servers" and "Cart" - -### Footer - -**Added**: "Server Status" link -- Public access to infrastructure status -- Positioned in footer with other utility links - ---- - -## Apache Configuration - -Three Apache virtual host configuration files have been created in the GSP root directory. - -### Files Created - -- **panel.conf** - Panel dashboard configuration -- **website.conf** - Storefront website configuration -- **fileserver.conf** - File server configuration -- **APACHE_SETUP.md** - Detailed installation guide - -### panel.conf - -Main Open Game Panel dashboard: -- Domain: panel.yourdomain.com -- Document Root: /var/www/GSP -- PHP settings optimized for panel operations -- Security headers enabled - -### website.conf - -GameServers.World storefront: -- Domain: gameservers.world -- Document Root: /var/www/GSP/_website -- Protected includes and data directories -- Static asset caching -- Compression enabled -- Separate session handling - -### fileserver.conf - -Game file distribution: -- Domain: files.yourdomain.com -- Document Root: /var/www/fileserver -- Directory browsing enabled -- Large file support -- Script execution disabled in uploads -- Bandwidth limiting support (optional) - -### Installation - -See `APACHE_SETUP.md` for complete installation instructions including: -- Copying configuration files -- Enabling sites and modules -- SSL/HTTPS setup with Let's Encrypt -- DNS configuration -- Firewall rules -- Troubleshooting - ---- - -## Testing - -### Password Reset - -1. Visit `login.php` -2. Click "Forgot Password?" -3. Enter username or email -4. Check email or view on-screen link (development mode) -5. Click reset link -6. Enter new password (min 8 chars) -7. Confirm password matches -8. Submit and verify redirect to login - -### My Servers - -1. Login as a user with servers -2. Click "My Servers" in navigation -3. Verify all servers are listed -4. Check expiration dates -5. Click "Renew" on a server -6. Verify renewal page displays correctly - -### Server Status - -1. Visit footer link "Server Status" -2. Verify all remote servers are displayed -3. Check status badges (color coding) -4. Verify "Last Updated" formatting -5. Confirm public access (no login required) - -### UI Changes - -1. Visit `serverlist.php` -2. Verify "Order Now" displays as styled button -3. Click button to go to `order.php` -4. Verify game images display correctly -5. Check footer has "Server Status" link -6. Login and verify "My Servers" appears in menu - ---- - -## Security Considerations - -### Password Reset - -- ✅ Tokens expire after 1 hour -- ✅ One-time use tokens -- ✅ Secure random generation -- ✅ User enumeration protection -- ✅ Password strength requirements -- ⚠️ Email delivery depends on server mail config - -### My Servers - -- ✅ Login required -- ✅ User can only see own servers -- ✅ SQL injection prevention with prepared statements -- ✅ XSS prevention with htmlspecialchars() - -### Server Status - -- ✅ Read-only public page -- ✅ No sensitive information exposed -- ✅ SQL injection prevention -- ℹ️ Server updates should be authenticated (implement separately) - -### Apache Configs - -- ✅ Security headers enabled -- ✅ Sensitive directories protected -- ✅ Directory listing disabled (except fileserver) -- ✅ HTTPS configurations ready -- ⚠️ Update domain names before deployment -- ⚠️ Configure SSL certificates for production - ---- - -## Future Enhancements - -### Password Reset -- Email template customization -- Integration with email service provider -- Rate limiting for reset requests -- SMS/2FA backup recovery - -### My Servers -- Server control buttons (start/stop/restart) -- Real-time server metrics -- Configuration editor -- File manager integration -- Console access -- Backup/restore functionality - -### Server Status -- Automated server monitoring agent -- Alert notifications -- Historical uptime graphs -- Incident history -- Scheduled maintenance display -- Status API for external monitoring - -### General -- User profile management -- Invoice history -- Support ticket system -- Knowledge base integration -- Multi-language support -- Dark/light theme toggle - ---- - -## Support - -For issues or questions: - -1. Check the main GSP documentation -2. Review Apache configuration in `APACHE_SETUP.md` -3. Check PHP error logs -4. Verify database connectivity -5. Ensure proper file permissions - -## License - -All new features follow the same license as the main Open Game Panel project. diff --git a/Panel/modules/billing/_archived/IMPLEMENTATION_SUMMARY.md b/Panel/modules/billing/_archived/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index d498e1f3..00000000 --- a/Panel/modules/billing/_archived/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,181 +0,0 @@ -# Website Login Implementation - Summary - -## Task Completed -Successfully implemented login functionality for the website (_website/) that authenticates users against the panel database (ogp_users table) while maintaining separate sessions. - -## Files Created - -### 1. `_website/login.php` (NEW - 223 lines) -Full-featured login page with: -- Modern, responsive UI design -- Authentication against panel DB using MD5 (panel-compatible) -- Separate website session: `opengamepanel_web` -- Input validation and sanitization -- Error and success message display -- Automatic redirect after successful login -- Login attempt logging -- Already-logged-in detection and redirect - -**Key Features:** -- SQL injection prevention via `mysqli_real_escape_string()` -- XSS prevention via `htmlspecialchars()` in output -- Password verification using MD5 (matching panel's method) -- Clean separation from panel session -- Responsive design that works on mobile and desktop - -### 2. `_website/logout.php` (NEW - 23 lines) -Clean logout functionality: -- Destroys website session properly -- Clears session cookies -- Logs logout events -- Redirects to homepage - -### 3. `_website/index.php` (MODIFIED) -Updated homepage with: -- Session management initialization -- Header with login status display -- "Welcome, [username]!" message when logged in -- Login/Logout button in header -- Maintains original design with minimal changes - -**Changes Made:** -- Added session initialization at top (4 lines) -- Added proper HTML structure (DOCTYPE, html, head tags) -- Added header section with login/logout UI (19 lines) -- Converted from heredoc to regular HTML output -- All styling preserved with additions for header - -### 4. `_website/README_LOGIN.md` (NEW - Documentation) -Comprehensive documentation covering: -- Overview of implementation -- File descriptions -- Session management details -- Security features -- Database requirements -- Usage instructions for users and developers -- Future enhancement suggestions -- Alignment with project guidelines - -### 5. `_website/test_db_connection.php` (NEW - Test Script) -Database testing utility that checks: -- Database connection status -- ogp_users table existence -- Table structure verification -- User count -- Required columns presence -- MD5 hashing functionality -- Session functionality - -**⚠️ Warning in file:** Must be deleted before production deployment - -## Technical Details - -### Session Management -- **Website Session Name:** `opengamepanel_web` -- **Panel Session Name:** `opengamepanel_web` (unchanged) -- **Complete separation:** Users can be logged into one without the other - -### Session Variables Set on Login -```php -$_SESSION['website_user_id'] // User ID from ogp_users -$_SESSION['website_username'] // Username -$_SESSION['website_user_role'] // User role (admin, user, etc.) -$_SESSION['website_user_email'] // User email -$_SESSION['website_login_time'] // Timestamp of login -``` - -### Database Requirements -- Access to `ogp_users` table -- Required fields: `user_id`, `users_login`, `users_passwd`, `users_role`, `users_email` -- Uses existing `db.php` connection - -### Security Measures Implemented -1. **SQL Injection Prevention:** `mysqli_real_escape_string()` on all user input -2. **XSS Prevention:** `htmlspecialchars()` on all output -3. **Session Isolation:** Separate session name prevents conflicts -4. **Password Compatibility:** MD5 hashing matches panel's method -5. **Logging:** All login/logout events logged via `logger()` function -6. **Input Validation:** Empty field checking -7. **Already-Logged-In Check:** Prevents duplicate sessions - -### Code Quality -- All files pass PHP syntax validation (`php -l`) -- Follows existing code conventions -- Minimal changes to existing files -- Clean, readable code with comments -- Responsive design - -## Testing Performed - -### Automated Testing -✅ PHP syntax validation on all files -✅ File structure verification -✅ Git commit verification - -### Manual Testing Required -⚠️ Requires live database connection: -- Login with valid credentials -- Login with invalid credentials -- Already-logged-in redirect -- Logout functionality -- Session persistence across page loads -- Use `test_db_connection.php` to verify database setup - -## Alignment with Project Guidelines - -From `.github/copilot-instructions.md`: - -✅ **Website ↔ Panel on same host:** Uses panel DB for authentication -✅ **Sessions remain separate:** Different session names -✅ **Auth compatibility:** MD5 hashing matches panel -✅ **No-Code Planning:** Documented approach before implementation -✅ **Repository-first:** Reused existing `db.php`, `logger()` function -✅ **Minimal changes:** Surgical modifications to index.php only -✅ **Security considerations:** SQL injection, XSS prevention - -## File Size Summary -- `login.php`: 7,282 bytes (223 lines) -- `logout.php`: 567 bytes (23 lines) -- `index.php`: Modified from 3,961 to 5,381 bytes (+1,420 bytes, +37 lines) -- `README_LOGIN.md`: 4,041 bytes (documentation) -- `test_db_connection.php`: 4,970 bytes (test utility) -- `IMPLEMENTATION_SUMMARY.md`: This file (documentation) - -**Total New Code:** ~17,000 bytes across 3 new PHP files - -## Next Steps - -### For Testing -1. Run `test_db_connection.php` to verify database connectivity -2. Test login with valid panel credentials -3. Verify session persistence -4. Test logout functionality -5. **Delete `test_db_connection.php` after testing** - -### For Production -1. Remove or restrict access to `test_db_connection.php` -2. Consider adding rate limiting for failed login attempts -3. Optional: Add CSRF token protection -4. Optional: Implement modern password hashing with transparent upgrade -5. Monitor `logfile.txt` for login activity - -### Future Enhancements (Optional) -- Password hashing upgrade (bcrypt/argon2) -- CSRF protection -- Rate limiting (IP-based, like panel's ban_list) -- "Remember Me" functionality -- Two-factor authentication -- Password reset flow integration -- Session timeout management - -## Conclusion - -The implementation successfully provides a clean, secure login system for the website that authenticates against the panel database while maintaining complete session separation. The code follows best practices, includes comprehensive documentation, and is ready for testing with a live database connection. - -All requirements from the problem statement have been met: -✅ Clone index page structure -✅ Create login page -✅ Authenticate against panel DB -✅ Create separate login session -✅ Maintain panel compatibility - diff --git a/Panel/modules/billing/_archived/README_LOGIN.md b/Panel/modules/billing/_archived/README_LOGIN.md deleted file mode 100644 index 1392d37c..00000000 --- a/Panel/modules/billing/_archived/README_LOGIN.md +++ /dev/null @@ -1,110 +0,0 @@ -# Website Login Implementation - -## Overview -This implementation adds login functionality to the website that authenticates users against the panel's database (ogp_users table) while maintaining separate sessions for the website and panel. - -## Files Created/Modified - -### 1. `_website/login.php` (NEW) -- Full-featured login page with modern UI -- Authenticates against panel DB using MD5 password hashing (panel-compatible) -- Creates separate website session using `opengamepanel_web` session name -- Logs all login attempts via logger() function -- Session variables set: - - `$_SESSION['website_user_id']` - User ID from ogp_users - - `$_SESSION['website_username']` - Username - - `$_SESSION['website_user_role']` - User role (admin, user, etc.) - - `$_SESSION['website_user_email']` - User email - - `$_SESSION['website_login_time']` - Timestamp of login - -### 2. `_website/logout.php` (NEW) -- Cleanly destroys website session -- Logs logout events -- Redirects to homepage after logout -- Properly clears session cookies - -### 3. `_website/index.php` (MODIFIED) -- Added session management at the top -- Added header with Login/Logout button and user greeting -- Shows "Welcome, [username]!" when logged in -- Maintains same visual design with added header - -## Session Management - -### Separate Sessions -- **Website Session**: `opengamepanel_web` (this implementation) -- **Panel Session**: `opengamepanel_web` (existing panel) - -These sessions are completely separate - users can be logged into one without being logged into the other. - -## Security Features - -1. **SQL Injection Prevention**: Uses `mysqli_real_escape_string()` for input sanitization -2. **Password Hashing**: Compatible with panel's MD5 hashing (legacy but matches panel) -3. **Session Isolation**: Separate session name prevents conflicts with panel -4. **XSS Prevention**: Uses `htmlspecialchars()` for output escaping -5. **Logging**: All login/logout events are logged via logger() function - -## Database Requirements - -Requires connection to panel database with access to: -- `ogp_users` table (fields: user_id, users_login, users_passwd, users_role, users_email) -- Connection configured in `db.php` - -## Usage - -### For Users: -1. Visit `_website/login.php` to login -2. Enter panel credentials (username/password) -3. After successful login, redirected to homepage with session active -4. Click "Logout" button to end session - -### For Developers: -Check if user is logged in: -```php -session_name("opengamepanel_web"); -session_start(); - -if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) { - // User is logged in - $username = $_SESSION['website_username']; - $user_id = $_SESSION['website_user_id']; - $user_role = $_SESSION['website_user_role']; -} -``` - -## Future Enhancements (Optional) - -1. **Password Hashing Upgrade**: Implement modern bcrypt/argon2 with transparent upgrade on login -2. **CSRF Protection**: Add CSRF tokens to login form -3. **Rate Limiting**: Add IP-based login attempt limiting (similar to panel's ban_list) -4. **Remember Me**: Add persistent login cookie option -5. **Password Reset**: Integrate with panel's password reset flow -6. **Two-Factor Auth**: Optional 2FA for enhanced security - -## Testing - -All files pass PHP syntax validation: -```bash -php -l _website/index.php -php -l _website/login.php -php -l _website/logout.php -``` - -## Alignment with Copilot Instructions - -This implementation follows the no-code planning guidelines from `.github/copilot-instructions.md`: - -✅ Website uses panel DB for authentication -✅ Sessions remain separate (website ≠ panel) -✅ Auth compatibility maintained (MD5 hash for panel users) -✅ Minimal changes to existing code -✅ Repository-first approach (reused existing db.php, logger function) -✅ Security considerations (SQL injection prevention, session isolation) - -## Notes - -- Login credentials are the same as panel login (same user table) -- Website session does not grant access to panel - separate login required -- Logger function from db.php creates logfile.txt for audit trail - diff --git a/Panel/modules/billing/_archived/VISUAL_GUIDE.md b/Panel/modules/billing/_archived/VISUAL_GUIDE.md deleted file mode 100644 index 2c0c40a8..00000000 --- a/Panel/modules/billing/_archived/VISUAL_GUIDE.md +++ /dev/null @@ -1,317 +0,0 @@ -# Visual Guide - New Website Features - -This document provides a visual description of the new features and UI changes. - -## 1. Login Page Updates - -### Before -``` -┌─────────────────────────────────────┐ -│ Welcome Back │ -│ Sign in to your GameServers account│ -│ │ -│ Username: [____________] │ -│ Password: [____________] │ -│ │ -│ [ Sign In ] │ -│ │ -│ Register │ -│ ─── or ─── │ -│ Back to Home | Panel Login │ -└─────────────────────────────────────┘ -``` - -### After -``` -┌─────────────────────────────────────┐ -│ Welcome Back │ -│ Sign in to your GameServers account│ -│ │ -│ Username: [____________] │ -│ Password: [____________] │ -│ │ -│ [ Sign In ] │ -│ │ -│ Register | Forgot Password? ←NEW │ -│ ─── or ─── │ -│ Back to Home | Panel Login │ -└─────────────────────────────────────┘ -``` - -## 2. Forgot Password Page (NEW) - -``` -┌─────────────────────────────────────┐ -│ Forgot Password │ -│ Enter your username or email to │ -│ reset your password │ -│ │ -│ Username or Email: │ -│ [_____________________________] │ -│ │ -│ [ Request Password Reset ] │ -│ │ -│ Back to Login | Home │ -└─────────────────────────────────────┘ -``` - -After submission (success): -``` -┌─────────────────────────────────────┐ -│ ✓ Password reset instructions have │ -│ been sent to your email address. │ -└─────────────────────────────────────┘ -``` - -## 3. Reset Password Page (NEW) - -``` -┌─────────────────────────────────────┐ -│ Reset Password │ -│ Enter your new password │ -│ │ -│ New Password: │ -│ [_____________________________] │ -│ Must be at least 8 characters long │ -│ │ -│ Confirm Password: │ -│ [_____________________________] │ -│ │ -│ [ Reset Password ] │ -│ │ -│ Back to Login | Home │ -└─────────────────────────────────────┘ -``` - -## 4. Navigation Menu Updates - -### Before (Not Logged In) -``` -┌──────────────────────────────────────────────────────────┐ -│ GameServers.World [Login] │ -│ Home | Game Servers | Cart │ -└──────────────────────────────────────────────────────────┘ -``` - -### After (Logged In) -``` -┌──────────────────────────────────────────────────────────┐ -│ GameServers.World Welcome, username! [Logout] │ -│ Home | Game Servers | My Servers ←NEW | Cart │ -└──────────────────────────────────────────────────────────┘ -``` - -## 5. Server List Page - -### Before -``` -┌────────────────────────────┐ -│ [Game Image] │ -│ Counter-Strike 2 │ -│ $15.99 Monthly │ -│ │ -│ Order Server (link) │ -└────────────────────────────┘ -``` - -### After -``` -┌────────────────────────────┐ -│ [Game Image] │ -│ Counter-Strike 2 │ -│ $15.99 Monthly │ -│ │ -│ ┌────────────┐ │ -│ │ Order Now │ ←BUTTON │ -│ └────────────┘ │ -└────────────────────────────┘ -``` - -Button styling: -- Gradient background (purple/blue) -- Rounded corners -- Hover effect (lift up) -- Better visibility - -## 6. My Servers Page (NEW) - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ My Game Servers │ -├────────────────────────────────────────────────────────────────────────┤ -│ Server Name │ Game │ Location │ Status │ Expires │ Price │ Action│ -├──────────────┼─────────┼──────────┼────────┼────────────┼───────┼───────┤ -│ My CS2 Srv │ CS2 │ US East │ Active │ Nov 22,2025│ $15.99│[Renew]│ -│ Rust Server │ Rust │ US West │ Active │ Dec 5, 2025│ $19.99│[Renew]│ -│ Minecraft │ MC │ EU │ Expired│ Oct 1, 2025│ $12.99│[Renew]│ -└──────────────┴─────────┴──────────┴────────┴────────────┴───────┴───────┘ - -Status indicators: -- Active: Green badge -- Inactive: Red badge -- Expired: Red badge -``` - -Empty state (no servers): -``` -┌────────────────────────────────────┐ -│ My Game Servers │ -├────────────────────────────────────┤ -│ │ -│ You don't have any game servers │ -│ yet. │ -│ │ -│ ┌──────────────────────┐ │ -│ │ Browse Game Servers │ │ -│ └──────────────────────┘ │ -└────────────────────────────────────┘ -``` - -## 7. Renew Server Page (NEW) - -``` -┌─────────────────────────────────────┐ -│ Renew Server │ -├─────────────────────────────────────┤ -│ Counter-Strike 2 Server │ -│ │ -│ ○ 1 Month - $15.99 │ -│ ○ 1 Year - $159.99 │ -│ │ -│ ┌──────────────────────┐ Cancel │ -│ │ Proceed to Payment │ │ -│ └──────────────────────┘ │ -└─────────────────────────────────────┘ -``` - -## 8. Server Status Page (NEW) - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ Server Status │ -│ Real-time status of our game server infrastructure │ -├────────────────────────────────────────────────────────────────────────────┤ -│ Server │Location/IP │Status │CPU │Memory│Disk │Uptime │Updated│ -├─────────────┼─────────────┼────────────┼──────┼──────┼──────┼───────┼───────┤ -│ US-East-1 │192.168.1.10 │ [Online] │45.2% │72.1% │38.5% │30 days│2m ago │ -│ US-West-1 │192.168.1.11 │ [Online] │32.8% │65.3% │42.1% │15 days│1m ago │ -│ EU-Central-1│192.168.1.12 │[Maintenance]│N/A │N/A │N/A │N/A │Never │ -│ Asia-1 │192.168.1.13 │ [Offline] │N/A │N/A │N/A │N/A │2h ago │ -└─────────────┴─────────────┴────────────┴──────┴──────┴──────┴───────┴───────┘ - -Server status is updated automatically every 5 minutes. -If you experience any issues, please contact support. -``` - -Status badge colors: -- Online: Green -- Offline: Red -- Maintenance: Orange -- Unknown: Gray - -## 9. Footer Updates - -### Before -``` -┌────────────────────────────────────────────────┐ -│ Privacy | TOS | Worlddomination.dev │ -└────────────────────────────────────────────────┘ -``` - -### After -``` -┌────────────────────────────────────────────────────────┐ -│ Privacy | TOS | Server Status ←NEW | Worlddomination.dev│ -└────────────────────────────────────────────────────────┘ -``` - -## 10. Order Page Image Fix - -### Before (Broken) -``` -┌────────────────────────────┐ -│ [X] Image not found │ -│ Counter-Strike 2 │ -│ Description... │ -└────────────────────────────┘ -``` - -### After (Fixed) -``` -┌────────────────────────────┐ -│ [✓] ┌──────────┐ │ -│ │ CS2 Image│ │ -│ └──────────┘ │ -│ Counter-Strike 2 │ -│ Description... │ -└────────────────────────────┘ -``` - -Image path changed from `images/game.png` to `../images/game.png` - -## Color Scheme - -All pages use consistent styling: - -### Primary Colors -- Purple/Blue Gradient: `#667eea` to `#764ba2` -- White backgrounds: `#ffffff` -- Dark backgrounds: `#0b1020` - -### Status Colors -- Success/Active: `#10b981` (Green) -- Error/Expired: `#ef4444` (Red) -- Warning/Maintenance: `#f59e0b` (Orange) -- Info/Unknown: `#6b7280` (Gray) - -### Typography -- Font: System fonts (-apple-system, Segoe UI, Roboto, Arial) -- Headings: Bold, 1.8rem -- Body: 1rem -- Small text: 0.9rem - -### Buttons -- Primary: Gradient purple/blue -- Hover: Lift effect (translateY -2px) -- Border radius: 8px -- Padding: 12px 24px - -## Responsive Design - -All pages are mobile-responsive: - -### Desktop (> 768px) -- Full navigation menu -- Side-by-side layouts -- Larger form fields - -### Mobile (< 768px) -- Stacked navigation -- Single column layouts -- Touch-friendly buttons -- Larger tap targets - -## Accessibility Features - -- Semantic HTML elements -- Proper form labels -- Keyboard navigation support -- Focus indicators -- Alt text for images -- ARIA labels where needed - -## Browser Compatibility - -Tested and compatible with: -- Chrome/Edge (latest) -- Firefox (latest) -- Safari (latest) -- Mobile browsers (iOS Safari, Chrome Mobile) - -## Performance - -- Compressed CSS/JS -- Optimized images -- Cached static assets -- Minimal database queries -- Prepared statements for security and speed diff --git a/Panel/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt b/Panel/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt deleted file mode 100644 index 4e128727..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-142000/ARCHIVE_README.txt +++ /dev/null @@ -1,16 +0,0 @@ -Archived files from _website on 2025-10-23 14:20:00 - -This folder contains a snapshot of removed documentation and test artifacts moved from the active `_website/` tree. - -Files moved here (original paths): -- VISUAL_GUIDE.md -- README_LOGIN.md -- FEATURES.md -- IMPLEMENTATION_SUMMARY.md -- CONFIGURATION.md -- test_db_connection.php -- tools/simulate_webhook.php -- ai.php -- data/SIMULATED-WEBHOOK-20251022-101500.json - -If you need to restore any of these, copy them back to the original paths. diff --git a/Panel/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md b/Panel/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md deleted file mode 100644 index 6c2c5ff2..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-142000/MOVED_DOCS.md +++ /dev/null @@ -1,3 +0,0 @@ -The detailed game docs under `_website/docs/games/` were intentionally left in place (they are product-facing). - -Top-level documentation (VISUAL_GUIDE.md, FEATURES.md, IMPLEMENTATION_SUMMARY.md, CONFIGURATION.md, README_LOGIN.md) were archived here and removed from the active site to reduce clutter. diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json b/Panel/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json deleted file mode 100644 index ea411e67..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/MOVED_FILES.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "moved_at": "2025-10-23T20:25:00Z", - "kept": { - "logs": "_website/logs/", - "docs": "_website/docs/" - }, - "files": [ - { - "original": "_website/ai.php", - "archived": "_website/_archived/removed-20251023-202500/ai.php", - "size_bytes": null, - "note": "archived sample and tools; size omitted" - }, - { - "original": "_website/test_db_connection.php", - "archived": "_website/_archived/removed-20251023-202500/test_db_connection.php", - "size_bytes": null - }, - { - "original": "_website/tools/simulate_webhook.php", - "archived": "_website/_archived/removed-20251023-202500/tools/simulate_webhook.php", - "size_bytes": null - }, - { - "original": "_website/tools/check_db_user.php", - "archived": "_website/_archived/removed-20251023-202500/tools/check_db_user.php", - "size_bytes": null - }, - { - "original": "_website/tools/check_invoices_redirect.php", - "archived": "_website/_archived/removed-20251023-202500/tools/check_invoices_redirect.php", - "size_bytes": null - }, - { - "original": "_website/tools/debug_invoices_redirect.php", - "archived": "_website/_archived/removed-20251023-202500/tools/debug_invoices_redirect.php", - "size_bytes": null - }, - { - "original": "_website/tools/check_logout_redirect.php", - "archived": "_website/_archived/removed-20251023-202500/tools/check_logout_redirect.php", - "size_bytes": null - }, - { - "original": "_website/data/SIMULATED-WEBHOOK-20251022-101500.json", - "archived": "_website/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json", - "size_bytes": null - }, - { - "original": "_website/data/NO-INVOICE.json", - "archived": "_website/_archived/removed-20251023-202500/data/NO-INVOICE.json", - "size_bytes": null - }, - { - "original": "_website/data/INV-20250825-174311-0a7993.json", - "archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json", - "size_bytes": null - }, - { - "original": "_website/data/INV-20250825-170438-e37518.json", - "archived": "_website/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json", - "size_bytes": null - }, - { - "original": "_website/data/FREE-549-1761246925.json", - "archived": "_website/_archived/removed-20251023-202500/data/FREE-549-1761246925.json", - "size_bytes": null - }, - { - "original": "_website/data/FREE-548-1761171178.json", - "archived": "_website/_archived/removed-20251023-202500/data/FREE-548-1761171178.json", - "size_bytes": null - } - ] -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/ai.php b/Panel/modules/billing/_archived/removed-20251023-202500/ai.php deleted file mode 100644 index 27f1f451..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/ai.php +++ /dev/null @@ -1,325 +0,0 @@ -= 400) { - $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error'; - throw new RuntimeException("OpenAI API error ({$code}): {$msg}"); - } - return is_array($data) ? $data : []; -} - -/** Create or reuse a per-visitor thread */ -function ensure_thread_id() { - if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id']; - $created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]); - $tid = $created['id'] ?? null; - if (!$tid) throw new RuntimeException('Failed to create thread.'); - $_SESSION['thread_id'] = $tid; - return $tid; -} - -/** Add a user message */ -function add_user_message($thread_id, $text) { - openai_request('POST', "/threads/{$thread_id}/messages", [ - 'role' => 'user', - 'content' => $text, - ]); -} - -/** Start a run */ -function start_run($thread_id, $assistant_id) { - $run = openai_request('POST', "/threads/{$thread_id}/runs", [ - 'assistant_id' => $assistant_id, - ]); - $run_id = $run['id'] ?? null; - if (!$run_id) throw new RuntimeException('Failed to start run.'); - return $run_id; -} - -/** Wait for completion (or fail/timeout) */ -function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) { - $terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired']; - for ($i = 0; $i < $max_tries; $i++) { - usleep($delay_us); - $run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}"); - $status = $run['status'] ?? ''; - if (in_array($status, $terminal, true)) return $run; - } - return ['status' => 'timeout']; -} - -/** Cache of file_id => filename (per request) */ -$_FILE_NAME_CACHE = []; - -/** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */ -function get_file_name_by_id($file_id) { - global $_FILE_NAME_CACHE; - if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id]; - $file = openai_request('GET', "/files/{$file_id}"); - $name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id)); - $_FILE_NAME_CACHE[$file_id] = $name; - return $name; -} - -/** - * Extract message text + citations (filename + page if available). - * Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]] - */ -function normalize_messages($messages) { - $out = []; - if (empty($messages['data']) || !is_array($messages['data'])) return $out; - - // The API returns newest first by default if not specifying; we request 'asc' in fetch. - foreach ((array)$messages['data'] as $m) { - $role = $m['role'] ?? ''; - if (!in_array($role, ['user', 'assistant', 'system'], true)) continue; - - if (empty($m['content']) || !is_array($m['content'])) continue; - - $all_text = []; - $refs = []; - foreach ((array)$m['content'] as $part) { - if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) { - $all_text[] = $part['text']['value']; - - // Parse annotations for citations (file_citation) - $anns = $part['text']['annotations'] ?? []; - if (is_array($anns)) { - foreach ((array)$anns as $ann) { - if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) { - $fid = $ann['file_citation']['file_id']; - $page = null; - - // Page can appear under different shapes depending on backend. Try common keys: - if (isset($ann['file_citation']['page'])) { - $page = $ann['file_citation']['page']; - } elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) { - // Example: ['start' => 5, 'end' => 6] - $start = $ann['file_citation']['page_range']['start'] ?? null; - $end = $ann['file_citation']['page_range']['end'] ?? null; - if ($start && $end && $start !== $end) $page = "{$start}-{$end}"; - elseif ($start) $page = (string)$start; - } - // Fetch filename - try { - $filename = get_file_name_by_id($fid); - } catch (Throwable $e) { - $filename = $fid; - } - $refs[] = [ - 'file_id' => $fid, - 'filename' => $filename, - 'page' => $page ?? 'n/a', - ]; - } - } - } - } - } - - if (!empty($all_text)) { - $out[] = [ - 'role' => $role, - 'text' => implode("\n", $all_text), - 'refs' => $refs, - ]; - } - } - return $out; -} - -/** Fetch conversation (ascending) */ -function fetch_history($thread_id) { - $messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]); - return normalize_messages($messages); -} - -/* ------------------- HANDLE POST ------------------- */ -$error = null; -$history = []; - -try { - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!empty($_POST['reset_thread'])) { - $_SESSION['thread_id'] = null; - } elseif (isset($_POST['user_input'])) { - $user_text = trim((string)$_POST['user_input']); - if ($user_text !== '') { - $thread_id = ensure_thread_id(); - add_user_message($thread_id, $user_text); - $run_id = start_run($thread_id, $ASSISTANT_ID); - $run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $RUN_POLL_DELAY); - - if (($run['status'] ?? '') === 'failed') { - $error = 'Assistant run failed.'; - } elseif (($run['status'] ?? '') === 'requires_action') { - // If you later support tool calls, handle them here then submit outputs. - } elseif (($run['status'] ?? '') === 'timeout') { - $error = 'Assistant timed out. Please try again.'; - } - } - } - } - - if (!empty($_SESSION['thread_id'])) { - $history = fetch_history($_SESSION['thread_id']); - } -} catch (Throwable $e) { - $error = $e->getMessage(); -} -?> - - -
-

Site Assistant

-

Type a question below. Press Enter to send, Shift+Enter for a new line.

- - -
- Error: -
- - - -
Thread:
- - -
- -
- - -
-
- - -
- Question, assistant => Answer, system => (optional) - $role = $msg['role'] ?? 'assistant'; - if ($role === 'user') $label = 'Question'; - elseif ($role === 'assistant') $label = 'Answer'; - else $label = ucfirst($role); // e.g., System - $text = str_replace("\r\n", "\n", $msg['text'] ?? ''); - $refs = $msg['refs'] ?? []; - ?> -
-
-
- - -
- References: - -
- -
- -
- -
No messages yet.
- - -
- Conversation persists until you click “Reset Conversation”. -
-
- - - diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json deleted file mode 100644 index 12661d0a..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-548-1761171178.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "event_type": "PAYMENT.CAPTURE.COMPLETED", - "status": "PAID", - "amount": 0, - "currency": "USD", - "payer": "iaretechnician@gmail.com", - "invoice": "FREE-548-1761171178", - "custom": "admin_free_create_order_548", - "resource_id": "FREE-439c594e1e65", - "items": [], - "ts": "2025-10-23T00:12:58+02:00" -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json deleted file mode 100644 index 764d6832..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/FREE-549-1761246925.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "event_type": "PAYMENT.CAPTURE.COMPLETED", - "status": "PAID", - "amount": 0, - "currency": "USD", - "payer": "iaretechnician@gmail.com", - "invoice": "FREE-549-1761246925", - "custom": "admin_free_create_order_549", - "resource_id": "FREE-439c594e1e65", - "items": [], - "ts": "2025-10-23T00:12:58+02:00" -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json deleted file mode 100644 index 071c7e79..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-170438-e37518.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "event_type": "PAYMENT.CAPTURE.COMPLETED", - "status": "PAID", - "amount": "19.99", - "currency": "USD", - "payer": null, - "invoice": "INV-20250825-170438-e37518", - "custom": "user_1234_order_5678", - "resource_id": "2V315801FX904340P", - "ts": "2025-08-25T17:05:27-04:00" -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json deleted file mode 100644 index f14c95a7..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/INV-20250825-174311-0a7993.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "event_type": "PAYMENT.CAPTURE.COMPLETED", - "status": "PAID", - "amount": "19.99", - "currency": "USD", - "payer": null, - "invoice": "INV-20250825-174311-0a7993", - "custom": "user_1234_order_5678", - "resource_id": "2V315801FX904340P", - "ts": "2025-08-25T17:05:27-04:00" -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json deleted file mode 100644 index 338554da..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/NO-INVOICE.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "event_type": "PAYMENT.SALE.COMPLETED", - "status": "PAID", - "amount": "0.48", - "currency": "USD", - "payer": null, - "invoice": null, - "custom": null, - "ts": "2025-08-25T16:46:11-04:00" -} diff --git a/Panel/modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json b/Panel/modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json deleted file mode 100644 index 0fd9df67..00000000 --- a/Panel/modules/billing/_archived/removed-20251023-202500/data/SIMULATED-WEBHOOK-20251022-101500.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "event_type": "PAYMENT.CAPTURE.COMPLETED", - "status": "PAID", - "amount": "9.99", - "currency": "USD", - "invoice": "INV-20251022-101500-TEST", - "resource_id": "SIMULATED12345", - "ts": "2025-10-22T10:15:00-04:00", - "note": "Simulated webhook write for testing" -} diff --git a/Panel/modules/billing/add_override_price_column.sql b/Panel/modules/billing/add_override_price_column.sql deleted file mode 100644 index 565cc6d1..00000000 --- a/Panel/modules/billing/add_override_price_column.sql +++ /dev/null @@ -1,25 +0,0 @@ --- DEPRECATED: This file is no longer needed. --- --- The gsp_billing_service_remote_servers mapping table has been removed. --- Server availability per game/service is now stored in gsp_billing_services.remote_server_id --- as a comma-separated list of numeric server IDs (e.g. "1,3,7"). --- The module migration (db_version 4) drops the mapping table automatically. --- --- The original content of this file is kept below for historical reference only. --- Do NOT run this script on new installations. --- --- Migration: add override_price to billing_service_remote_servers --- Run once on existing installs that already have the mapping table (db_version 2) --- but are missing the override_price column (added in db_version 3 / module v3.1). --- --- Replace 'gsp_' with your actual table prefix if it differs. --- --- This statement is safe to run multiple times only if your MySQL version supports --- ADD COLUMN IF NOT EXISTS (MySQL 8.0.3+). On older versions, check first: --- SHOW COLUMNS FROM gsp_billing_service_remote_servers LIKE 'override_price'; - -ALTER TABLE `gsp_billing_service_remote_servers` - ADD COLUMN IF NOT EXISTS `override_price` DECIMAL(10,2) NULL AFTER `enabled`; - --- If your MySQL is older than 8.0.3, use the conditional form instead: --- ALTER TABLE `gsp_billing_service_remote_servers` ADD COLUMN `override_price` DECIMAL(10,2) NULL AFTER `enabled`; diff --git a/Panel/modules/billing/add_paypal_data_column.sql b/Panel/modules/billing/add_paypal_data_column.sql deleted file mode 100644 index 0438b914..00000000 --- a/Panel/modules/billing/add_paypal_data_column.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Add paypal_data column to billing_orders table --- This stores the full PayPal response JSON for admin/refund tracking --- Table prefix is hardcoded to gsp_ for standalone billing module - -ALTER TABLE `gsp_billing_orders` -ADD COLUMN `paypal_data` TEXT NULL AFTER `payment_txid`; - --- Update comment -ALTER TABLE `gsp_billing_orders` -MODIFY COLUMN `paypal_data` TEXT NULL COMMENT 'Full PayPal API response JSON for tracking/refunds'; diff --git a/Panel/modules/billing/add_remote_server_enabled_column.sql b/Panel/modules/billing/add_remote_server_enabled_column.sql deleted file mode 100644 index b4726014..00000000 --- a/Panel/modules/billing/add_remote_server_enabled_column.sql +++ /dev/null @@ -1,41 +0,0 @@ --- DEPRECATED: This file is no longer needed. --- --- The billing module no longer references an `enabled` column on gsp_remote_servers. --- gsp_remote_servers is the server inventory table only. --- Server availability per game/service is stored in gsp_billing_services.remote_server_id --- as a comma-separated list of numeric server IDs (e.g. "1,3,7"). --- --- The original content of this file is kept below for historical reference only. --- Do NOT run this script on new installations. --- --- Migration: add `enabled` column to gsp_remote_servers --- --- The original panel schema (panel.sql / ogp_remote_servers) includes an `enabled` --- INT(11) column. Installations that were created from an older schema, or whose --- table was renamed without carrying the column forward, may be missing it. --- --- Run this once against your panel database (replace `gsp_` with your prefix if --- different). Safe to skip if the column already exists — just check with: --- SHOW COLUMNS FROM `gsp_remote_servers` LIKE 'enabled'; --- --- Usage: --- mysql -u -p < modules/billing/add_remote_server_enabled_column.sql - -SET @table_name = 'gsp_remote_servers'; -SET @col_name = 'enabled'; - -SET @sql = IF( - ( - SELECT COUNT(*) - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = @table_name - AND COLUMN_NAME = @col_name - ) = 0, - CONCAT('ALTER TABLE `', @table_name, '` ADD COLUMN `enabled` INT(11) NOT NULL DEFAULT 1'), - 'SELECT "Column already exists — nothing to do" AS note' -); - -PREPARE stmt FROM @sql; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; diff --git a/Panel/modules/billing/add_service_id_column.sql b/Panel/modules/billing/add_service_id_column.sql deleted file mode 100644 index 3c21e272..00000000 --- a/Panel/modules/billing/add_service_id_column.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Add missing service_id column to gsp_billing_invoices table --- This column is required to track which service/game plan was purchased --- Table prefix is hardcoded to gsp_ for standalone billing module - -ALTER TABLE `gsp_billing_invoices` -ADD COLUMN `service_id` INT(11) NOT NULL AFTER `user_id`; - --- Add index for better query performance -ALTER TABLE `gsp_billing_invoices` -ADD KEY `service_id` (`service_id`); diff --git a/Panel/modules/billing/add_to_cart.php b/Panel/modules/billing/add_to_cart.php deleted file mode 100644 index cfea7b12..00000000 --- a/Panel/modules/billing/add_to_cart.php +++ /dev/null @@ -1,468 +0,0 @@ - 'month', 'rate_type' => 'monthly', 'days' => 31]; -} - -function billing_money_to_cents(float $amount): int -{ - return (int) round($amount * 100); -} - -function billing_cents_to_money(int $cents): float -{ - return $cents / 100; -} - -function billing_rate_from_service(mysqli $db, string $table_prefix, int $service_id, string $rate_type): float -{ - if ($service_id <= 0) { - return 0.0; - } - - $stmt = $db->prepare("SELECT price_monthly FROM {$table_prefix}billing_services WHERE service_id = ? LIMIT 1"); - if (!$stmt) { - return 0.0; - } - - $stmt->bind_param('i', $service_id); - $stmt->execute(); - $stmt->bind_result($price_monthly); - $rate = 0.0; - if ($stmt->fetch()) { - $rate = floatval($price_monthly); - } - $stmt->close(); - - return $rate; -} - -function billing_detect_service_os(string $cfgFile, string $gameKey): string -{ - $haystack = strtolower(trim($cfgFile !== '' ? $cfgFile : $gameKey)); - if ($haystack === '') { - return 'any'; - } - if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) { - return 'windows'; - } - if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) { - return 'linux'; - } - return 'any'; -} - -function billing_normalize_node_os(string $serverOs): string -{ - $value = strtolower(trim($serverOs)); - if ($value === '' || $value === 'any') { - return 'any'; - } - if (str_starts_with($value, 'win')) { - return 'windows'; - } - if (str_starts_with($value, 'lin')) { - return 'linux'; - } - return $value; -} - -function billing_fail_add_to_cart(string $message, array $context = [], ?string $redirect = null): void -{ - site_log_error('add_to_cart_failed', array_merge(['message' => $message], $context)); - $target = $redirect ?? '/cart.php?error=add_to_cart'; - header('Location: ' . $target); - exit; -} - -// Immediate request tracing log (helps confirm the script is hit) -@mkdir(__DIR__ . '/logs', 0775, true); -$trace_file = __DIR__ . '/logs/add_to_cart_requests.log'; -file_put_contents($trace_file, date('c') . " - REQUEST_METHOD=" . ($_SERVER['REQUEST_METHOD'] ?? '') . " URI=" . ($_SERVER['REQUEST_URI'] ?? '') . "\n", FILE_APPEND); - -// Prefer website session id if set (login.php sets website_user_id in debug mode) -$user_id = 0; -if (isset($_SESSION['website_user_id']) && !empty($_SESSION['website_user_id'])) { - $user_id = intval($_SESSION['website_user_id']); -} elseif (isset($_SESSION['user_id']) && !empty($_SESSION['user_id'])) { - $user_id = intval($_SESSION['user_id']); -} -// If we don't have a numeric user_id but have a username, try to resolve it from the panel DB -if ($user_id <= 0 && isset($_SESSION['website_username']) && !empty($_SESSION['website_username'])) { - $uname = trim((string)$_SESSION['website_username']); - // attempt to lookup in DB (if connection available later we will set session after connecting) - // We'll set a temporary flag to resolve after DB connection is established below - $resolve_username_for_user_id = $uname; -} else { - $resolve_username_for_user_id = null; -} -/* -if ($user_id <= 0) { - // Not logged in - redirect to login with return - $return = urlencode('/' . trim(str_replace('\\', '/', $_SERVER['REQUEST_URI']), '/')); - header('Location: ' . billing_url('login.php') . '?return_to=' . $return); - exit; -}*/ - -// Basic validation and normalization -$service_id = isset($_POST['service_id']) ? intval($_POST['service_id']) : 0; -$home_name = isset($_POST['home_name']) ? trim($_POST['home_name']) : ''; -$ip_id = isset($_POST['ip_id']) ? intval($_POST['ip_id']) : 0; -$max_players = isset($_POST['max_players']) ? intval($_POST['max_players']) : 0; -$qty = isset($_POST['qty']) ? intval($_POST['qty']) : 1; -$invoice_duration = isset($_POST['invoice_duration']) ? $_POST['invoice_duration'] : 'month'; -$display_service_id = isset($_POST['display_service_id']) ? intval($_POST['display_service_id']) : 0; -$display_rate = isset($_POST['display_rate']) ? floatval($_POST['display_rate']) : 0.0; -$posted_total = isset($_POST['calculated_total']) ? floatval($_POST['calculated_total']) : 0.0; -$remote_control_password = isset($_POST['remote_control_password']) ? trim((string)$_POST['remote_control_password']) : ''; -$ftp_password = isset($_POST['ftp_password']) ? trim((string)$_POST['ftp_password']) : ''; - -// Price lookup: try to find service price_monthly -$db = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null); -if (!$db) { - // Log connection error and return user to cart with a friendly error flag - @mkdir(__DIR__ . '/logs', 0775, true); - $trace = __DIR__ . '/logs/add_to_cart.log'; - file_put_contents($trace, date('c') . " - mysqli_connect failed: " . mysqli_connect_error() . "\n", FILE_APPEND); - billing_fail_add_to_cart('DB connection failed'); -} else { - mysqli_set_charset($db, 'utf8mb4'); - // Log that config was loaded (mask password) - @mkdir(__DIR__ . '/logs', 0775, true); - $trace = __DIR__ . '/logs/add_to_cart.log'; - $masked_pass = strlen($db_pass) ? '***' : ''; - file_put_contents($trace, date('c') . " - DB connected host={$db_host} user={$db_user} pass={$masked_pass} db={$db_name}\n", FILE_APPEND); -} - -// If we deferred resolving username to user_id, do it now with the DB connection -if (!empty($resolve_username_for_user_id) && $db) { - $safe_uname = mysqli_real_escape_string($db, $resolve_username_for_user_id); - // users_login is the correct column name in this schema - $q = mysqli_query($db, "SELECT user_id FROM {$table_prefix}users WHERE users_login = '$safe_uname' LIMIT 1"); - if ($q && mysqli_num_rows($q) === 1) { - $r = mysqli_fetch_assoc($q); - $user_id = intval($r['user_id'] ?? 0); - // persist into session for subsequent requests - if ($user_id > 0) { - $_SESSION['website_user_id'] = $user_id; - site_log_info('resolved_user_id_from_username', ['username'=>$resolve_username_for_user_id,'user_id'=>$user_id]); - // Also resolve and persist the user's role so menus and admin checks are consistent - $role_q = mysqli_query($db, "SELECT users_role FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1"); - if ($role_q && mysqli_num_rows($role_q) === 1) { - $role_row = mysqli_fetch_assoc($role_q); - $_SESSION['website_user_role'] = $role_row['users_role'] ?? ''; - } - } - } else { - site_log_warn('resolve_user_failed', ['username'=>$resolve_username_for_user_id]); - } -} - -$service_name = ''; -$base_rate = 0.0; -$slot_min_qty = 1; -$slot_max_qty = 1; -$service_home_cfg_id = 0; -$service_remote_server_csv = ''; -$service_cfg_file = ''; -$service_game_key = ''; -$durationInfo = billing_normalize_duration($invoice_duration); -if ($service_id > 0) { - $stmt = $db->prepare("SELECT bs.service_name, bs.price_monthly, bs.slot_min_qty, bs.slot_max_qty, bs.home_cfg_id, bs.remote_server_id, ch.home_cfg_file, ch.game_key - FROM {$table_prefix}billing_services bs - LEFT JOIN {$table_prefix}config_homes ch ON ch.home_cfg_id = bs.home_cfg_id - WHERE bs.service_id = ? AND bs.enabled = 1 - LIMIT 1"); - if ($stmt) { - $stmt->bind_param('i', $service_id); - $stmt->execute(); - $stmt->bind_result($service_name, $price_monthly, $slot_min_qty, $slot_max_qty, $service_home_cfg_id, $service_remote_server_csv, $service_cfg_file, $service_game_key); - if ($stmt->fetch()) { - $base_rate = floatval($price_monthly); - // constrain slots - if ($max_players < $slot_min_qty) $max_players = $slot_min_qty; - if ($max_players > $slot_max_qty) $max_players = $slot_max_qty; - } - $stmt->close(); - } -} - -if ($service_id <= 0 || $base_rate < 0) { - billing_fail_add_to_cart('Invalid service selection', ['service_id' => $service_id]); -} - -if ($service_name === '') { - billing_fail_add_to_cart('Selected service is not available', ['service_id' => $service_id], '/serverlist.php'); -} - -if ($ip_id <= 0) { - billing_fail_add_to_cart('No location selected', ['service_id' => $service_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Please select a server location.')); -} - -$allowedServerIds = []; -foreach (explode(',', (string)$service_remote_server_csv) as $part) { - $part = trim($part); - if ($part !== '' && ctype_digit($part)) { - $allowedServerIds[(int)$part] = true; - } -} -if (!isset($allowedServerIds[$ip_id])) { - billing_fail_add_to_cart('Selected location is not allowed for this service', [ - 'service_id' => $service_id, - 'ip_id' => $ip_id, - 'remote_server_csv' => $service_remote_server_csv, - ], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected location is not available for this service.')); -} - -$hasServerOsColumn = false; -$osColCheck = mysqli_query($db, "SHOW COLUMNS FROM {$table_prefix}remote_servers LIKE 'server_os'"); -if ($osColCheck && mysqli_num_rows($osColCheck) > 0) { - $hasServerOsColumn = true; -} - -if ($hasServerOsColumn) { - $rsQuery = mysqli_query($db, "SELECT remote_server_id, server_os FROM {$table_prefix}remote_servers WHERE remote_server_id = " . intval($ip_id) . " LIMIT 1"); - if ($rsQuery && mysqli_num_rows($rsQuery) === 1) { - $rsRow = mysqli_fetch_assoc($rsQuery); - $serviceOs = billing_detect_service_os((string)$service_cfg_file, (string)$service_game_key); - $nodeOs = billing_normalize_node_os((string)($rsRow['server_os'] ?? 'any')); - if ($serviceOs !== 'any' && $nodeOs !== 'any' && $serviceOs !== $nodeOs) { - $message = $serviceOs === 'windows' - ? 'This service requires a Windows server location.' - : 'This service requires a Linux server location.'; - billing_fail_add_to_cart('Service and node OS mismatch', [ - 'service_id' => $service_id, - 'home_cfg_id' => $service_home_cfg_id, - 'cfg_file' => $service_cfg_file, - 'node_os' => $nodeOs, - ], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode($message)); - } - } else { - billing_fail_add_to_cart('Selected remote server not found', ['service_id' => $service_id, 'ip_id' => $ip_id], '/order.php?service_id=' . intval($service_id) . '&error_message=' . rawurlencode('Selected server location no longer exists.')); - } -} - -if ($base_rate <= 0 && $display_service_id > 0) { - $fallback_rate = billing_rate_from_service($db, $table_prefix, $display_service_id, $durationInfo['rate_type']); - if ($fallback_rate > 0) { - $base_rate = $fallback_rate; - } -} - -if ($base_rate <= 0 && $display_rate > 0) { - $base_rate = $display_rate; -} - -if ($remote_control_password === '' || strcasecmp($remote_control_password, 'ChangeMe') === 0) { - $remote_control_password = billing_generate_password(); -} -if ($ftp_password === '' || strcasecmp($ftp_password, 'ChangeMe') === 0) { - $ftp_password = billing_generate_password(); -} - -// Insert into {table_prefix}billing_invoices (NOT orders - invoice created first) -$now = date('Y-m-d H:i:s'); -$status = 'due'; // Invoice status: due (unpaid), paid -$payment_status = 'unpaid'; -$qty = max(1, $qty); -$max_players = max(1, $max_players); -$rate_per_player_cents = max(0, billing_money_to_cents($base_rate)); -$subtotal_cents = $rate_per_player_cents * $max_players * $qty; -$posted_total_cents = max(0, billing_money_to_cents($posted_total)); -if ($subtotal_cents <= 0 && $posted_total_cents > 0 && $base_rate > 0) { - $subtotal_cents = $posted_total_cents; -} -$subtotal = billing_cents_to_money($subtotal_cents); -$amount = $subtotal; -$period_end = date('Y-m-d H:i:s', strtotime('+' . ($durationInfo['days'] * $qty) . ' days')); - -// Normal flow: process POST immediately. If debug=1 is passed, we'll still log SQL and show results in logs. -$debug = (isset($_GET['debug']) && $_GET['debug'] == '1') || (isset($_POST['debug']) && $_POST['debug'] == '1'); - -// Build and execute the INSERT with prepared statements -@mkdir(__DIR__ . '/logs', 0775, true); -$logfile = __DIR__ . '/logs/add_to_cart.log'; -site_log_info('add_to_cart_invoked', ['user_id'=>$user_id, 'service_id'=>$service_id]); - -// Get customer name and email from {table_prefix}users -$customer_name = ''; -$customer_email = ''; -$user_q = mysqli_query($db, "SELECT users_fname, users_lname, users_email FROM {$table_prefix}users WHERE user_id = " . intval($user_id) . " LIMIT 1"); -if ($user_q && mysqli_num_rows($user_q) === 1) { - $user_row = mysqli_fetch_assoc($user_q); - $customer_name = trim(($user_row['users_fname'] ?? '') . ' ' . ($user_row['users_lname'] ?? '')); - $customer_email = $user_row['users_email'] ?? ''; -} - -// Compute due_date = now + 3 days -$due_dt = new DateTime('now'); -$due_dt->modify('+3 days'); -$due_date = $due_dt->format('Y-m-d H:i:s'); - -// Escape values -$esc_user_id = intval($user_id); -$esc_service_id = intval($service_id); -$esc_ip_id = intval($ip_id); -$esc_max_players = intval($max_players); -$esc_qty = intval($qty); -$description = trim(($service_name !== '' ? $service_name : 'Game Server') . ': ' . $home_name); -$invoiceTable = $table_prefix . 'billing_invoices'; -$invoiceColumns = []; -$columnsResult = mysqli_query($db, "SHOW COLUMNS FROM `{$invoiceTable}`"); -if (!$columnsResult) { - billing_fail_add_to_cart('Could not inspect billing invoice schema', ['table' => $invoiceTable, 'error' => mysqli_error($db)]); -} -while ($col = mysqli_fetch_assoc($columnsResult)) { - $invoiceColumns[$col['Field']] = true; -} -mysqli_free_result($columnsResult); - -$invoice_duration = $durationInfo['invoice_duration']; -$rate_type = $durationInfo['rate_type']; -$rowData = [ - 'order_id' => 0, - 'user_id' => $esc_user_id, - 'service_id' => $esc_service_id, - 'home_id' => 0, - 'home_name' => $home_name, - 'ip' => $esc_ip_id, - 'max_players' => $esc_max_players, - 'remote_control_password' => $remote_control_password, - 'ftp_password' => $ftp_password, - 'customer_name' => $customer_name, - 'customer_email' => $customer_email, - 'amount' => $amount, - 'discount_amount' => 0.00, - 'currency' => 'USD', - 'status' => $status, - 'billing_status' => $status, - 'invoice_date' => $now, - 'due_date' => $due_date, - 'description' => $description, - 'invoice_duration' => $invoice_duration, - 'rate_type' => $rate_type, - 'rate_per_player' => (float)$base_rate, - 'players' => $max_players, - 'period_start' => $now, - 'period_end' => $period_end, - 'subtotal' => $subtotal, - 'total_due' => $amount, - 'payment_status' => $payment_status, - 'qty' => $esc_qty, - 'coupon_id' => 0, -]; - -$insertColumns = []; -$placeholders = []; -$bindTypes = ''; -$bindValues = []; -foreach ($rowData as $column => $value) { - if (!isset($invoiceColumns[$column])) { - continue; - } - $insertColumns[] = "`{$column}`"; - $placeholders[] = '?'; - if (is_int($value)) { - $bindTypes .= 'i'; - } elseif (is_float($value)) { - $bindTypes .= 'd'; - } else { - $bindTypes .= 's'; - } - $bindValues[] = $value; -} - -if (empty($insertColumns)) { - billing_fail_add_to_cart('No compatible invoice columns were found for insert', ['table' => $invoiceTable]); -} - -$sql = "INSERT INTO `{$invoiceTable}` (" . implode(', ', $insertColumns) . ") - VALUES (" . implode(', ', $placeholders) . ")"; - -$stmt = $db->prepare($sql); -$res = false; -$err_no = 0; -$err = ''; -if ($stmt) { - $stmt->bind_param($bindTypes, ...$bindValues); - $res = @$stmt->execute(); - $err_no = mysqli_errno($db); - $err = mysqli_error($db); -} else { - $err_no = mysqli_errno($db); - $err = mysqli_error($db); -} - -site_log_info('add_to_cart_invoice', [ - 'user_id' => $user_id, - 'service_id' => $service_id, - 'home_name' => $home_name, - 'remote_server_id' => $ip_id, - 'players' => $max_players, - 'qty' => $qty, - 'invoice_duration' => $invoice_duration, - 'subtotal' => $subtotal, - 'total_due' => $amount, -]); -file_put_contents($logfile, date('c') . " - Creating invoice (not order): status=due total_due={$amount}\n", FILE_APPEND); - -if (!$res || $err_no > 0) { - site_log_error('mysqli_query_failed', ['errno'=>$err_no, 'error'=>$err, 'sql'=>$sql]); - file_put_contents($logfile, date('c') . " - ERROR: " . $err . " (errno: {$err_no})\n", FILE_APPEND); - // Log table existence check - $tbl_check = mysqli_query($db, "SHOW TABLES LIKE '{$table_prefix}billing_invoices'"); - $tbl_exists = ($tbl_check && mysqli_num_rows($tbl_check) > 0) ? 'yes' : 'no'; - site_log_warn('billing_invoices_exists', ['exists'=>$tbl_exists]); - file_put_contents($logfile, date('c') . " - Table exists check: {$tbl_exists}\n", FILE_APPEND); - - billing_fail_add_to_cart('Invoice insert failed', ['errno' => $err_no, 'error' => $err]); -} else { - $insert_id = mysqli_insert_id($db); - $affected = mysqli_affected_rows($db); - site_log_info('add_to_cart_insert', ['invoice_id'=>$insert_id, 'affected_rows'=>$affected]); - file_put_contents($logfile, date('c') . " - Invoice created: invoice_id={$insert_id}\n", FILE_APPEND); -} - -if ($stmt instanceof mysqli_stmt) { - $stmt->close(); -} - -// Redirect to cart page -header('Location: cart.php'); -exit; - -?> diff --git a/Panel/modules/billing/admin.php b/Panel/modules/billing/admin.php deleted file mode 100644 index b2a0241b..00000000 --- a/Panel/modules/billing/admin.php +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - Admin — Dashboard - - - - -
-

Admin Dashboard

-

Welcome to the admin area. From here you can manage servers, payments, and site settings.

- - - -
-

Quick usage notes

-
    -
  • The Manage Servers & Services page allows enabling/disabling nodes and editing service rows.
  • -
  • The Invoice History page reads JSON payment records from .
  • -
  • The Edit Site Config page edits _website/includes/config.inc.php. Edits create a timestamped backup before saving.
  • -
- -

Sandbox account (testing)

-

Use PayPal sandbox credentials when testing payments. Set your sandbox client_id and client_secret in modules/billing/includes/config.inc.php (the $paypal_client_id and $paypal_client_secret variables). Set $paypal_sandbox = false for live payments.

-
    -
  • Create a sandbox business account at PayPal Developer and obtain a sandbox client ID/secret.
  • -
  • Update the payment handler config and restart the webserver if required.
  • -
  • Run a checkout using the PayPal JS button on the checkout page — after payment completes, the webhook will record a JSON file into .
  • -
  • If you need to simulate a webhook locally, drop a JSON file with the same schema into the data/ folder (we added a sample: SIMULATED-WEBHOOK-*.json).
  • -
- -

Payments: high-level program flow

-
    -
  1. User adds an item and proceeds to checkout (_website/cart.php).
  2. -
  3. The checkout page renders the PayPal JS SDK and calls server-side endpoints (create_order/capture_order).
  4. -
  5. After a successful capture, PayPal sends a webhook event to _website/webhook.php (or the equivalent handler under _website/api/).
  6. -
  7. The webhook verifies the signature, fetches any missing order details, and writes a JSON record to the data/ directory (this powers invoices.php and return.php).
  8. -
  9. On successful payment we mark the order as PAID in the JSON and the site UI (invoices/returns) reads those JSONs to render receipts.
  10. -
  11. Admin pages can view invoices at ./invoices.php and reconcile or trigger further provisioning via internal panel APIs.
  12. -
- -

Environment

- - - - - -
Site Base URL
Data directory
PHP SAPI
Writable?
- -
- - - diff --git a/Panel/modules/billing/admin_config.php b/Panel/modules/billing/admin_config.php deleted file mode 100644 index a7683526..00000000 --- a/Panel/modules/billing/admin_config.php +++ /dev/null @@ -1,896 +0,0 @@ - filemtime($b); - }); - $toDelete = count($files) - $retention; - for ($i = 0; $i < $toDelete; $i++) { - @unlink($files[$i]); - } -} - -// --------------------------------------------------------------------------- -// Helper: create a backup of the config file; returns backup filename or ''. -// --------------------------------------------------------------------------- -function billing_admin_create_backup(string $cfgPath, string $bakDir): string -{ - @mkdir($bakDir, 0775, true); - $bakName = $bakDir . '/config.inc.php.' . date('Ymd-His') . '.' . bin2hex(random_bytes(4)) . '.bak'; - if (!copy($cfgPath, $bakName)) { - return ''; - } - return $bakName; -} - -// --------------------------------------------------------------------------- -// Helper: run php -l on a file and return [ok, output]. -// --------------------------------------------------------------------------- -function billing_admin_lint(string $filePath): array -{ - $phpExec = PHP_BINARY ?: null; - if (!$phpExec) { - return [true, 'PHP executable not found; skipping syntax check.']; - } - $cmd = escapeshellarg($phpExec) . ' -l ' . escapeshellarg($filePath); - $out = []; - $rc = 0; - @exec($cmd . ' 2>&1', $out, $rc); - return [$rc === 0, implode("\n", $out)]; -} - -// --------------------------------------------------------------------------- -// Helper: generate canonical config.inc.php content from an array of values. -// DB settings are preserved from the existing file; only billing fields change. -// --------------------------------------------------------------------------- -function billing_admin_build_config(string $existingContent, array $vals): string -{ - // Extract current DB settings from existing file content so we never lose them. - $dbLines = []; - foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) { - if (preg_match('/^\s*\$' . preg_quote($var, '/') . '\s*=.*$/m', $existingContent, $m)) { - $dbLines[$var] = rtrim($m[0]); - } - } - - $q = static function (string $v): string { - return '"' . addslashes($v) . '"'; - }; - - $mode = (strtolower($vals['paypal_mode'] ?? 'sandbox') === 'live') ? 'live' : 'sandbox'; - $retention = max(1, min(10, (int)($vals['backup_retention'] ?? 5))); - $baseUrl = rtrim(trim($vals['SITE_BASE_URL'] ?? ''), '/'); - $bg = trim($vals['SITE_BACKGROUND'] ?? 'images/dark.jpg'); - $dataDir = trim($vals['SITE_DATA_DIR'] ?? ''); - $wh_path = '/' . ltrim(trim($vals['paypal_webhook_path'] ?? '/paypal/webhook.php'), '/'); - - // Sandbox credentials — never erase existing secret if field was left blank - $sb_id = trim($vals['paypal_sandbox_client_id'] ?? ''); - $sb_sec = trim($vals['paypal_sandbox_client_secret'] ?? ''); - $sb_wh = trim($vals['paypal_sandbox_webhook_id'] ?? ''); - - // Live credentials — never erase existing secret if field was left blank - $lv_id = trim($vals['paypal_live_client_id'] ?? ''); - $lv_sec = trim($vals['paypal_live_client_secret'] ?? ''); - $lv_wh = trim($vals['paypal_live_webhook_id'] ?? ''); - - $dbBlock = ''; - foreach (['db_host', 'db_port', 'db_user', 'db_pass', 'db_name', 'table_prefix', 'db_type'] as $var) { - if (isset($dbLines[$var])) { - $dbBlock .= $dbLines[$var] . "\n"; - } - } - - $dataDirLine = ($dataDir !== '' && $dataDir !== 'auto') - ? '$SITE_DATA_DIR = ' . $q($dataDir) . ';' - : "\$SITE_DATA_DIR = realpath(__DIR__ . '/..') . DIRECTORY_SEPARATOR . 'data';"; - - return ' Edit Config.' . "\n" - . '###############################################' . "\n" - . $dbBlock - . "\n" - . '// Optional: base URL without trailing slash (e.g. https://gameservers.world).' . "\n" - . '// Leave empty to use relative paths.' . "\n" - . '$SITE_BASE_URL = ' . $q($baseUrl) . ';' . "\n" - . '$SITE_BASE_URL = rtrim(trim((string)$SITE_BASE_URL), \'/\');' . "\n" - . "\n" - . '// Site-wide background image (relative to site root).' . "\n" - . '$SITE_BACKGROUND = ' . $q($bg) . ';' . "\n" - . '$SITE_BACKGROUND = trim((string)$SITE_BACKGROUND);' . "\n" - . "\n" - . '// Data directory for persisted payment webhook JSON files.' . "\n" - . $dataDirLine . "\n" - . "\n" - . '// ---------------------------------------------------------------------------' . "\n" - . '// PayPal configuration' . "\n" - . '// ---------------------------------------------------------------------------' . "\n" - . '$paypal_mode = ' . $q($mode) . '; // \'sandbox\' or \'live\'' . "\n" - . "\n" - . '// Sandbox credentials (PayPal Developer Dashboard → sandbox app)' . "\n" - . '$paypal_sandbox_client_id = ' . $q($sb_id) . ';' . "\n" - . '$paypal_sandbox_client_secret = ' . $q($sb_sec) . ';' . "\n" - . '$paypal_sandbox_webhook_id = ' . $q($sb_wh) . ';' . "\n" - . "\n" - . '// Live credentials (leave blank until ready for production)' . "\n" - . '$paypal_live_client_id = ' . $q($lv_id) . ';' . "\n" - . '$paypal_live_client_secret = ' . $q($lv_sec) . ';' . "\n" - . '$paypal_live_webhook_id = ' . $q($lv_wh) . ';' . "\n" - . "\n" - . '// Webhook path (relative to billing site root, must start with /)' . "\n" - . '// Full public URL = $SITE_BASE_URL + $paypal_webhook_path' . "\n" - . '$paypal_webhook_path = ' . $q($wh_path) . ';' . "\n" - . "\n" - . '// Admin config backup retention: how many backups to keep (1–10). Default 5.' . "\n" - . '$SITE_CONFIG_BACKUP_RETENTION = ' . $retention . ';' . "\n" - . '?>' . "\n"; -} - -// --------------------------------------------------------------------------- -// Read current values from config (already loaded by config_loader above). -// --------------------------------------------------------------------------- -$cfgVals = [ - 'SITE_BASE_URL' => $SITE_BASE_URL ?? '', - 'SITE_BACKGROUND' => $SITE_BACKGROUND ?? 'images/dark.jpg', - 'SITE_DATA_DIR' => $SITE_DATA_DIR ?? '', - 'paypal_mode' => $paypal_mode ?? 'sandbox', - 'paypal_sandbox_client_id' => $paypal_sandbox_client_id ?? '', - 'paypal_sandbox_client_secret' => $paypal_sandbox_client_secret ?? '', - 'paypal_sandbox_webhook_id' => $paypal_sandbox_webhook_id ?? '', - 'paypal_live_client_id' => $paypal_live_client_id ?? '', - 'paypal_live_client_secret' => $paypal_live_client_secret ?? '', - 'paypal_live_webhook_id' => $paypal_live_webhook_id ?? '', - 'paypal_webhook_path' => $paypal_webhook_path ?? '/paypal/webhook.php', - 'backup_retention' => $SITE_CONFIG_BACKUP_RETENTION ?? 5, -]; - -// Computed full webhook URL for display -$computedWebhookUrl = function_exists('gsp_paypal_get_full_webhook_url') - ? gsp_paypal_get_full_webhook_url() - : rtrim($cfgVals['SITE_BASE_URL'], '/') . $cfgVals['paypal_webhook_path']; - -// Detect panel-mode (DB settings are managed by the panel) -$panelMode = defined('BILLING_PANEL_CONFIG_PATH'); -$panelCfgPath = $panelMode ? BILLING_PANEL_CONFIG_PATH : null; - -$status = ''; -$statusType = 'info'; // 'success' | 'error' | 'info' - -// --------------------------------------------------------------------------- -// POST: Save interactive form -// --------------------------------------------------------------------------- -if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_form') { - $token = $_POST['csrf'] ?? ''; - if (!hash_equals($csrf, (string)$token)) { - $status = 'Invalid CSRF token.'; - $statusType = 'error'; - } elseif (!is_writable($cfgPath)) { - $status = 'Config file is not writable: ' . h($cfgPath); - $statusType = 'error'; - } else { - // Collect and validate form values - $formVals = [ - 'SITE_BASE_URL' => trim($_POST['SITE_BASE_URL'] ?? ''), - 'SITE_BACKGROUND' => trim($_POST['SITE_BACKGROUND'] ?? 'images/dark.jpg'), - 'SITE_DATA_DIR' => trim($_POST['SITE_DATA_DIR'] ?? ''), - 'paypal_mode' => (strtolower(trim($_POST['paypal_mode'] ?? 'sandbox')) === 'live') ? 'live' : 'sandbox', - 'paypal_sandbox_client_id' => trim($_POST['paypal_sandbox_client_id'] ?? ''), - 'paypal_live_client_id' => trim($_POST['paypal_live_client_id'] ?? ''), - 'paypal_sandbox_webhook_id' => trim($_POST['paypal_sandbox_webhook_id'] ?? ''), - 'paypal_live_webhook_id' => trim($_POST['paypal_live_webhook_id'] ?? ''), - 'paypal_webhook_path' => trim($_POST['paypal_webhook_path'] ?? '/paypal/webhook.php'), - 'backup_retention' => (int)($_POST['backup_retention'] ?? 5), - ]; - - // Client secrets: only update if a non-blank value was submitted (never erase existing). - $sbSecPost = trim($_POST['paypal_sandbox_client_secret'] ?? ''); - $formVals['paypal_sandbox_client_secret'] = ($sbSecPost !== '') ? $sbSecPost : ($cfgVals['paypal_sandbox_client_secret'] ?? ''); - - $lvSecPost = trim($_POST['paypal_live_client_secret'] ?? ''); - $formVals['paypal_live_client_secret'] = ($lvSecPost !== '') ? $lvSecPost : ($cfgVals['paypal_live_client_secret'] ?? ''); - - // Validate - $validationError = ''; - if ($formVals['backup_retention'] < 1 || $formVals['backup_retention'] > 10) { - $validationError = 'Backup retention must be a number between 1 and 10.'; - } - - if ($validationError) { - $status = $validationError; - $statusType = 'error'; - } else { - $existingContent = (string)file_get_contents($cfgPath); - $newContent = billing_admin_build_config($existingContent, $formVals); - - // Backup before write. - // Note: the backup copy and subsequent file_put_contents are not covered by a - // single atomic lock. This is acceptable for an admin-only operation where - // concurrent writes are not expected. - $bakName = billing_admin_create_backup($cfgPath, $bakDir); - if (!$bakName) { - $status = 'Failed to create backup. Aborting save.'; - $statusType = 'error'; - } else { - if (file_put_contents($cfgPath, $newContent, LOCK_EX) === false) { - $status = 'Failed to write config file.'; - $statusType = 'error'; - } else { - // Syntax check - [$lintOk, $lintOut] = billing_admin_lint($cfgPath); - if (!$lintOk) { - @copy($bakName, $cfgPath); // rollback - $status = 'Syntax error in generated config; rolled back. Lint: ' . h($lintOut); - $statusType = 'error'; - } else { - // Apply backup retention - $retention = max(1, min(10, $formVals['backup_retention'])); - billing_admin_apply_retention($bakDir, $retention); - - $cfgVals = $formVals; // update displayed values - $computedWebhookUrl = rtrim($formVals['SITE_BASE_URL'], '/') . ('/' . ltrim($formVals['paypal_webhook_path'] ?? '/paypal/webhook.php', '/')); - $status = 'Config saved successfully. Backup: ' . basename($bakName); - $statusType = 'success'; - } - } - } - } - } -} - -// --------------------------------------------------------------------------- -// POST: Save raw editor -// --------------------------------------------------------------------------- -if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_raw') { - $token = $_POST['csrf'] ?? ''; - if (!hash_equals($csrf, (string)$token)) { - $status = 'Invalid CSRF token.'; - $statusType = 'error'; - } elseif (!is_writable($cfgPath)) { - $status = 'Config file is not writable: ' . h($cfgPath); - $statusType = 'error'; - } else { - $newRaw = $_POST['config_text'] ?? ''; - if (strpos(trim($newRaw), ' - - - - - Admin — Edit Config - - - - - -
-

Edit Site Config

- - -
- - - -
⚠️
- - - -
-

Site Settings

- - -
- ℹ️ Panel-integrated mode. - Database settings are managed by the panel and synced automatically from - . - They are shown below for reference only. -
- - -
- - - - - -
- -
Managed by the panel config. Edit the panel's includes/config.inc.php to change.
- -
-
- - -
-
- - -
- - - -
- -
- Full base URL without trailing slash (e.g. https://gameservers.world). - Leave empty to use relative paths. Used to compute the full public PayPal webhook URL. -
- -
- - -
- -
- Path to background image relative to the billing site root (e.g. images/dark.jpg). -
- -
- - -
- -
- Absolute path where payment webhook JSON files are stored. - Leave empty to use the default: modules/billing/data/. -
- -
- -
-

PayPal Configuration

- - -
- Currently active PayPal mode: -
- - -
- -
- Sandbox uses test credentials and the PayPal sandbox API — safe for development. - Live processes real payments. Switch only after configuring live credentials. -
- -
- - -

Sandbox Credentials

-
- -
Found in PayPal Developer Dashboard → sandbox app. Safe to expose in browser JS.
- -
-
- -
Server-side only — never sent to the browser. Leave blank to keep existing value.
-
- - -
-
-
- -
- Webhook ID from your PayPal sandbox app (for signature verification). - Leave empty to skip verification in sandbox mode (OK for initial setup). -
- -
- - -

Live Credentials

-
- -
From your PayPal live app. Leave blank until ready for production.
- -
-
- -
Server-side only. Leave blank to keep existing value.
-
- - -
-
-
- -
Webhook ID from your PayPal live app (for signature verification).
- -
- - -

Webhook Endpoint

-
- PayPal requires a full public HTTPS URL to deliver webhook events. - Set your Site Base URL above, then copy the computed URL below into your PayPal app's webhook configuration. -
-
- -
Path relative to the billing site root (must start with /). Default: /paypal/webhook.php
- -
-
- -
- This is the URL PayPal will POST webhook events to. - It must be publicly accessible over HTTPS before enabling live mode. -
- - -
- - -
-

Backup Settings

- - -
- -
- Number of config backups to keep (1–10). The oldest backup beyond this limit is - deleted after each save. Backups are stored in - . -
- -
- -
- -
-
-
- - - ' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . ''; - } - - // Last webhook events + recent PayPal errors - $diag_recent_events = []; - $diag_recent_errors = []; - $diag_errors_warning = ''; - try { - $port_int = intval($db_port ?? 3306) ?: 3306; - $diag_db = @mysqli_connect($db_host ?? 'localhost', $db_user ?? '', $db_pass ?? '', $db_name ?? '', $port_int); - if ($diag_db) { - $pfx_diag = $table_prefix ?? 'gsp_'; - mysqli_set_charset($diag_db, 'utf8mb4'); - - $res = @mysqli_query($diag_db, "SELECT paypal_event_id, event_type, processing_status, created_at FROM `{$pfx_diag}billing_paypal_webhook_events` ORDER BY id DESC LIMIT 5"); - if ($res) { - while ($row = mysqli_fetch_assoc($res)) { - $diag_recent_events[] = $row; - } - } - - // Recent PayPal errors — use BillingRepository for safe table creation - require_once __DIR__ . '/classes/BillingRepository.php'; - $diag_repo = new BillingRepository($diag_db, $pfx_diag); - if ($diag_repo->ensureBillingPaypalErrorsTable()) { - $diag_recent_errors = $diag_repo->getRecentPaypalErrors(10); - } else { - $diag_errors_warning = 'Could not create billing_paypal_errors table. Check DB permissions.'; - } - - mysqli_close($diag_db); - } - } catch (Throwable $e) { - $diag_errors_warning = 'Diagnostics DB query failed: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); - } - ?> - -
-

PayPal Diagnostics

- - -
- - - -
- -
- Self-Check Results:
- • Mode:
- • Active Client ID:
- • Active Client Secret:
- • Active Webhook ID:
- • Webhook file:
- • Logs directory:
- • Data directory:
- • Config file:
-
- - -
-
-
Current mode
-
- - - test - - live - -
-
- -
- -
-
Active Client ID
-
-
-
-
Active Client Secret
-
-
-
-
Active Webhook ID
-
-
- -
- -
-
Sandbox Client ID
-
-
-
-
Sandbox Client Secret
-
-
-
-
Sandbox Webhook ID
-
-
- -
- -
-
Live Client ID
-
-
-
-
Live Client Secret
-
-
-
-
Live Webhook ID
-
-
- -
- -
-
Webhook path
-
-
-
-
Full public webhook URL
-
- -
-
-
-
Webhook file on disk
-
- -
-
-
-
- - -

Recent Webhook Events

-
- - - - - - - - - - - - - - - - - -
PayPal Event IDTypeStatusReceived
-
- -

No webhook events recorded yet. Events will appear here after PayPal delivers the first webhook to .

- - -

Recent PayPal Errors

- -
- -

No PayPal errors logged yet.

- -
- - - - - - - - - - - - - - - - - - -
TimeContextError CodeMessageDebug IDOrder IDUser
-
- -
- - -
-

Advanced: Raw Config Editor

-
- ⚠️ Warning: Manually editing the raw PHP file can break the billing - website if you introduce a syntax error or remove required variables. - A backup is created automatically before saving, and a syntax check runs after. - The file is rolled back if a parse error is detected. -
- -
- - -
- -
-
- -

- Backup directory: - -
- backup(s) stored. - Most recent: - - -
No backups yet. - -

-
- -
- - - diff --git a/Panel/modules/billing/admin_coupons.php b/Panel/modules/billing/admin_coupons.php deleted file mode 100644 index fb161aca..00000000 --- a/Panel/modules/billing/admin_coupons.php +++ /dev/null @@ -1,537 +0,0 @@ -getMessage()); - $db = false; -} -if (!$db) { - $error = 'Database connection failed. Please check your configuration.'; - error_log('[admin_coupons] DB connect failed for host=' . ($db_host ?? 'unknown') . ' user=' . ($db_user ?? 'unknown') . ' db=' . ($db_name ?? 'unknown') . ' - ' . mysqli_connect_error()); -} - -$status = ''; -$error = ''; - -// Handle form submissions -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $token = $_POST['csrf'] ?? ''; - if (!hash_equals($csrf, (string)$token)) { - $error = 'Invalid CSRF token.'; - } else { - // Add new coupon - if (isset($_POST['add_coupon'])) { - $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'; - - // Validate code is unique - $check = mysqli_query($db, "SELECT coupon_id FROM {$table_prefix}billing_coupons WHERE code = '$code'"); - if (mysqli_num_rows($check) > 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 ((array)$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): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CodeNameDiscountTypeGame FilterUsesExpiresStatusActions
% - - - - - - All Games - - specific games - - - - / - - (unlimited) - - - - - - - -
- - - -
-
- -

No coupons found. Add your first coupon above.

- - -
- - - - - diff --git a/Panel/modules/billing/admin_invoices.php b/Panel/modules/billing/admin_invoices.php deleted file mode 100644 index 1d1abcd2..00000000 --- a/Panel/modules/billing/admin_invoices.php +++ /dev/null @@ -1,173 +0,0 @@ -prepare("SELECT * FROM `{$prefix}billing_invoices` WHERE invoice_id = ? LIMIT 1"); - if ($stmt) { - $stmt->bind_param('i', $invId); - $stmt->execute(); - $invRow = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - } - - if (!$invRow) { - $message = "Invoice #{$invId} not found."; - $msgType = 'error'; - } elseif ($action === 'mark_paid') { - $gateway = GatewayFactory::make('manual'); - $captureResult = $gateway->handleCallback([ - // total_due is the new schema field; amount is the legacy column during migration - 'amount' => $invRow['total_due'] ?? $invRow['amount'] ?? 0, - 'currency' => $invRow['currency'] ?? 'USD', - ]); - $captureResult['payment_method'] = 'manual'; - $homeId = intval($invRow['home_id'] ?? 0); - $result = $svc->processPaymentSuccess($captureResult, $invId, intval($invRow['user_id']), $homeId, $invRow); - $message = $result['success'] ? "Invoice #{$invId} marked as paid (manual)." : "Failed to mark invoice #{$invId} as paid."; - if (!$result['success']) $msgType = 'error'; - } elseif ($action === 'cancel') { - $stmt = $db->prepare("UPDATE `{$prefix}billing_invoices` SET payment_status='cancelled' WHERE invoice_id=? LIMIT 1"); - if ($stmt) { $stmt->bind_param('i', $invId); $stmt->execute(); $stmt->close(); } - $message = "Invoice #{$invId} cancelled."; - } elseif ($action === 'refund') { - $stmt = $db->prepare("UPDATE `{$prefix}billing_invoices` SET payment_status='refunded' WHERE invoice_id=? LIMIT 1"); - if ($stmt) { $stmt->bind_param('i', $invId); $stmt->execute(); $stmt->close(); } - $message = "Invoice #{$invId} marked as refunded."; - } - - if (!headers_sent()) { - header('Location: admin_invoices.php?msg=' . urlencode($message) . '&type=' . $msgType); - mysqli_close($db); - $db = null; - exit; - } -} - -// Fetch invoices -$invoices = []; -$res = $db->query( - "SELECT i.*, u.users_login, u.users_email - FROM `{$prefix}billing_invoices` i - LEFT JOIN `{$prefix}users` u ON u.user_id = i.user_id - ORDER BY i.invoice_id DESC - LIMIT 500" -); -if ($res) $invoices = $res->fetch_all(MYSQLI_ASSOC); -mysqli_close($db); -$db = null; - -if (isset($_GET['msg'])) $message = $_GET['msg']; -if (isset($_GET['type'])) $msgType = $_GET['type']; -?> - - - - - Admin — Invoices - - - - - - -
-

Admin — All Invoices

- -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#UserServerServiceRatePlayersPeriodTotalStatusMethodTxn IDActions
No invoices found.
- -
- - - -
- - -
- - - -
- - -
- - - -
- -
-
- - - diff --git a/Panel/modules/billing/admin_orders.php b/Panel/modules/billing/admin_orders.php deleted file mode 100644 index a47623ec..00000000 --- a/Panel/modules/billing/admin_orders.php +++ /dev/null @@ -1,241 +0,0 @@ -isAdmin($user_id); - - if (!$isAdmin) { - echo "

Access Denied: Admin privileges required.

"; - return; - } - - // Handle bulk actions - if (isset($_POST['bulk_action']) && isset($_POST['selected_orders'])) { - $action = $_POST['bulk_action']; - $selected = $_POST['selected_orders']; - - foreach ((array)$selected as $order_id) { - $order_id = $db->realEscapeSingle($order_id); - - switch ($action) { - case 'provision': - // Redirect to provision page for each order - header("Location: home.php?m=billing&p=provision_servers&order_id=".$order_id); - exit; - break; - case 'expire': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Expired' WHERE order_id=".$order_id); - break; - case 'activate': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Active' WHERE order_id=".$order_id); - break; - case 'invoice': - $db->query("UPDATE OGP_DB_PREFIXbilling_orders SET status='Invoiced' WHERE order_id=".$order_id); - break; - } - } - - echo "

Bulk action completed for ".count((array)$selected)." order(s).

"; - } - - // Get filter parameters - $status_filter = isset($_GET['status']) ? $_GET['status'] : 'all'; - $search = isset($_GET['search']) ? $_GET['search'] : ''; - - echo "

Manage All Orders (Admin)

"; - - // Filter form - echo "
"; - echo ""; - echo ""; - echo "Status: "; - echo "Search: "; - echo ""; - echo "
"; - - // Build query - $query = "SELECT o.*, s.service_name, u.users_login, u.users_email - FROM OGP_DB_PREFIXbilling_orders o - LEFT JOIN OGP_DB_PREFIXbilling_services s ON o.service_id = s.service_id - LEFT JOIN OGP_DB_PREFIXusers u ON o.user_id = u.user_id - WHERE 1=1"; - - if ($status_filter != 'all') { - $query .= " AND o.status = '".$db->realEscapeSingle($status_filter)."'"; - } - - if (!empty($search)) { - $search_escaped = $db->realEscapeSingle($search); - $query .= " AND (o.order_id LIKE '%".$search_escaped."%' - OR o.home_name LIKE '%".$search_escaped."%' - OR u.users_login LIKE '%".$search_escaped."%' - OR u.users_email LIKE '%".$search_escaped."%')"; - } - - $query .= " ORDER BY o.order_date DESC"; - - $orders = $db->resultQuery($query); - - if (empty($orders)) { - echo "

No orders found matching your filters.

"; - return; - } - - echo "
"; - echo "
"; - echo "With selected: "; - echo " "; - echo ""; - echo "
"; - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - - foreach ((array)$orders as $order) { - $status_class = ''; - switch ($order['status']) { - case 'Active': $status_class = 'label-success'; break; - case 'Invoiced': $status_class = 'label-warning'; break; - case 'Expired': $status_class = 'label-danger'; break; - default: $status_class = 'label-info'; - } - - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - - echo "
Order IDUsernameServer NameGame ServicePlayersPriceDurationStatusOrder DateEnd DateHome IDActions
".$order['order_id']."".$order['users_login']."
".$order['users_email']."
".$order['home_name']."".$order['service_name']."".$order['max_players']."$".number_format($order['price'], 2)."".$order['qty']." ".$order['invoice_duration']."(s)".$order['status']."".date('Y-m-d H:i', strtotime($order['order_date']))."".($order['end_date'] ? date('Y-m-d', strtotime($order['end_date'])) : 'N/A')."".($order['home_id'] ? $order['home_id'] : 'N/A').""; - - if ($order['status'] == 'Active' && !$order['home_id']) { - echo "Provision "; - } - - if ($order['status'] == 'Active' && $order['home_id']) { - echo "View Server "; - } - - echo "Details"; - echo "
"; - echo "
"; - - // JavaScript for checkbox toggle - echo ""; - - // Summary stats - $stats = $db->resultQuery("SELECT status, COUNT(*) as count, SUM(price) as total - FROM OGP_DB_PREFIXbilling_orders - GROUP BY status"); - - echo "
"; - echo "

Order Statistics

"; - echo ""; - echo ""; - - foreach ((array)$stats as $stat) { - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - - echo "
StatusCountTotal Value
".$stat['status']."".$stat['count']."$".number_format($stat['total'], 2)."
"; - echo "
"; - - // Orphaned home_id diagnostics ————————————————————————————————————————— - // Find billing_orders rows where home_id != 0 but no matching gsp_server_homes - // record exists. These indicate provisioning failures or stale data, and they - // are the reason the game monitor may show "No expiration date found". - $orphans = $db->resultQuery( - "SELECT o.order_id, o.user_id, o.home_name, o.home_id, o.status, o.end_date - FROM OGP_DB_PREFIXbilling_orders o - LEFT JOIN OGP_DB_PREFIXserver_homes sh ON sh.home_id = o.home_id - WHERE o.home_id != '0' - AND o.home_id != '' - AND sh.home_id IS NULL - ORDER BY o.order_id ASC" - ); - - echo "
"; - echo "

Orphaned home_id Diagnostics

"; - echo "

Billing orders that reference a home_id which no longer exists in gsp_server_homes. "; - echo "These orders will not show an expiration date on the game monitor. "; - echo "Reset home_id to 0 or re-provision these orders to fix them. "; - echo "Run normalize_billing_order_status.sql to standardize any legacy status values.

"; - - if (empty($orphans)) { - echo "

✓ No orphaned billing orders found.

"; - } else { - echo ""; - echo ""; - foreach ($orphans as $row) { - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - } - echo "
Order IDUser IDServer Namehome_id (missing)StatusEnd Date
".intval($row['order_id'])."".intval($row['user_id'])."".htmlspecialchars($row['home_name'] ?? '')."".htmlspecialchars($row['home_id'] ?? '')."".htmlspecialchars($row['status'] ?? '')."".htmlspecialchars($row['end_date'] ?? 'NULL')."
"; - } - echo "
"; -} -?> diff --git a/Panel/modules/billing/admin_payments.php b/Panel/modules/billing/admin_payments.php deleted file mode 100644 index bfb0495a..00000000 --- a/Panel/modules/billing/admin_payments.php +++ /dev/null @@ -1,97 +0,0 @@ -getTransactions($filter, 200, 0); - } catch (Throwable $e) { - $errorMsg = 'Could not load transactions: ' . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8'); - } - mysqli_close($db); - $db = null; -} -?> - - - - - Admin — Payment Transactions - - - - - -
-

Payment Transaction Log

-
- -
- - - - - Clear -
- - -

No transactions found.

- - - - - - - - - - - - - - - - - - - - - - - -
#InvoiceUserServerMethodTxn IDAmountStatusDate
- -
- - - diff --git a/Panel/modules/billing/admin_xml_editor.php b/Panel/modules/billing/admin_xml_editor.php deleted file mode 100644 index 41054fb7..00000000 --- a/Panel/modules/billing/admin_xml_editor.php +++ /dev/null @@ -1,161 +0,0 @@ -isFile() && strtolower($fileInfo->getExtension()) === 'xml') { - $availableFiles[] = $fileInfo->getFilename(); - } -} -sort($availableFiles, SORT_NATURAL | SORT_FLAG_CASE); - -$selectedFile = ''; -$fileContents = ''; - -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $postedFile = $_POST['file'] ?? ''; - $postedFile = basename(trim((string)$postedFile)); - if ($postedFile === '' || !in_array($postedFile, $availableFiles, true)) { - $errors[] = 'Invalid file selected.'; - } else { - $fullPath = $serverConfigDir . DIRECTORY_SEPARATOR . $postedFile; - if (!is_file($fullPath) || !is_readable($fullPath)) { - $errors[] = 'Selected file is missing or unreadable.'; - } elseif (!is_writable($fullPath)) { - $errors[] = 'Selected file is not writable.'; - } else { - $newContents = $_POST['xml_contents'] ?? ''; - $backupDir = $serverConfigDir . DIRECTORY_SEPARATOR . '_backups'; - if (!is_dir($backupDir)) { - @mkdir($backupDir, 0775, true); - } - $timestamp = date('Ymd-His'); - $backupPath = $backupDir . DIRECTORY_SEPARATOR . $postedFile . '.' . $timestamp . '.bak'; - $original = file_get_contents($fullPath); - if ($original === false) { - $errors[] = 'Unable to read original file for backup.'; - } elseif (@file_put_contents($backupPath, $original) === false) { - $errors[] = 'Failed to create backup copy before saving.'; - } elseif (@file_put_contents($fullPath, $newContents) === false) { - $errors[] = 'Failed to write new XML contents.'; - } else { - $messages[] = 'Saved changes to ' . htmlspecialchars($postedFile, ENT_QUOTES, 'UTF-8') . ' (backup: ' . basename($backupPath) . ').'; - $selectedFile = $postedFile; - $fileContents = $newContents; - } - } - } -} - -if ($selectedFile === '') { - $queryFile = $_GET['file'] ?? ''; - $queryFile = basename(trim((string)$queryFile)); - if ($queryFile !== '' && in_array($queryFile, $availableFiles, true)) { - $selectedFile = $queryFile; - } -} - -if ($selectedFile !== '' && $fileContents === '') { - $fullPath = $serverConfigDir . DIRECTORY_SEPARATOR . $selectedFile; - if (is_file($fullPath) && is_readable($fullPath)) { - $fileContents = file_get_contents($fullPath); - if ($fileContents === false) { - $errors[] = 'Unable to read the selected file.'; - $fileContents = ''; - } - } else { - $errors[] = 'Selected file is missing or unreadable.'; - $selectedFile = ''; - } -} - -function billing_render_flash(array $items, string $cssClass): void { - if (!$items) { - return; - } - echo '
'; - foreach ((array)$items as $item) { - echo '
' . $item . '
'; - } - echo '
'; -} - -?> - - - - - Admin — XML Config Editor - - - - - -
-

XML Config Editor

-

Editing files in . Each save creates a backup under _backups/.

- - - - -
-
-

Server Config XML Files

- -

No XML files found.

- - - - - - -
-
- -

Select an XML file from the list to begin editing.

- -
- - -
- - Backup created before each save. -
-
- -
-
-
- - - diff --git a/Panel/modules/billing/adminserverlist.php b/Panel/modules/billing/adminserverlist.php deleted file mode 100644 index 53aeab5c..00000000 --- a/Panel/modules/billing/adminserverlist.php +++ /dev/null @@ -1,718 +0,0 @@ - - - - - - Admin Service Configuration - GSP - - - - $imgFile) { - if (str_starts_with($normImgKey, $key) || str_starts_with($key, $normImgKey)) { - return $imgFile; - } - } - } - - return ''; -} - -$db = billing_get_db(); -if (!($db instanceof mysqli)) { - die("Database connection failed."); -} - -include(__DIR__ . '/includes/top.php'); -include(__DIR__ . '/includes/menu.php'); - -/* ----------------------------------------------------------------------- - Auto-sync: keep billing_services in step with config_homes - Source: one row per config_homes entry, keyed by home_cfg_id. - Runs on every page load; INSERT and soft-disable only — never hard-delete. ------------------------------------------------------------------------ */ -function sync_billing_services(mysqli $db, string $prefix): array -{ - $messages = []; - $tableName = $prefix . 'billing_services'; - - // Schema auto-repair: ensure all expected columns exist. - // col_exists() is provided by bootstrap.php. - $autoRepairCols = [ - 'home_cfg_id' => "ADD COLUMN `home_cfg_id` INT(11) NOT NULL DEFAULT 0", - 'description' => "ADD COLUMN `description` VARCHAR(1000) NOT NULL DEFAULT ''", - 'img_url' => "ADD COLUMN `img_url` VARCHAR(255) NOT NULL DEFAULT ''", - 'slot_min_qty' => "ADD COLUMN `slot_min_qty` INT(11) NOT NULL DEFAULT 1", - 'slot_max_qty' => "ADD COLUMN `slot_max_qty` INT(11) NOT NULL DEFAULT 100", - 'price_daily' => "ADD COLUMN `price_daily` FLOAT(15,4) NOT NULL DEFAULT 0", - 'price_monthly' => "ADD COLUMN `price_monthly` FLOAT(15,4) NOT NULL DEFAULT 0", - 'price_year' => "ADD COLUMN `price_year` FLOAT(15,4) NOT NULL DEFAULT 0", - 'remote_server_id' => "ADD COLUMN `remote_server_id` VARCHAR(255) NOT NULL DEFAULT ''", - ]; - - foreach ($autoRepairCols as $col => $alterFragment) { - if (!col_exists($db, $tableName, $col)) { - if ($db->query("ALTER TABLE `{$tableName}` {$alterFragment}")) { - $messages[] = "✔ Auto-repaired: added column '{$col}' to {$tableName}."; - } else { - $messages[] = "✖ Could not add column '{$col}' to {$tableName}: " . $db->error; - } - } - } - - // If critical columns are still absent after repair, abort to avoid SQL errors. - foreach (['service_name', 'home_cfg_id', 'enabled'] as $critical) { - if (!col_exists($db, $tableName, $critical)) { - $messages[] = "⚠ Critical column '{$critical}' missing from {$tableName}; skipping sync."; - return $messages; - } - } - - // Load all game configs from config_homes — one entry per game XML. - $configHomes = []; - $res = $db->query( - "SELECT home_cfg_id, game_name, home_cfg_file - FROM `{$prefix}config_homes` - ORDER BY game_name" - ); - if ($res) { - while ($row = $res->fetch_assoc()) { - $configHomes[(int)$row['home_cfg_id']] = $row; - } - } - - if (empty($configHomes)) { - // config_homes is empty or the table does not exist yet — nothing to sync. - return $messages; - } - - // Load existing billing_services indexed by home_cfg_id. - $existing = []; - $svcRes = $db->query( - "SELECT service_id, home_cfg_id, enabled - FROM `{$tableName}`" - ); - if ($svcRes) { - while ($row = $svcRes->fetch_assoc()) { - $hid = (int)$row['home_cfg_id']; - if ($hid > 0) { - $existing[$hid] = $row; - } - } - } - - // Insert a new row for every config_homes entry not yet in billing_services. - // Admin-editable fields (prices, slots, enabled, etc.) get safe defaults so - // the service is visible to the admin but not yet live in the store. - $availableImages = list_game_images(); - foreach ($configHomes as $homeCfgId => $ch) { - if (isset($existing[$homeCfgId])) { - continue; - } - $svcName = $db->real_escape_string($ch['game_name']); - $guessedImg = $db->real_escape_string( - guess_game_image((string)$ch['game_name'], (string)($ch['home_cfg_file'] ?? ''), $availableImages) - ); - $db->query( - "INSERT INTO `{$tableName}` - (home_cfg_id, mod_cfg_id, service_name, description, - remote_server_id, enabled, - price_daily, price_monthly, price_year, - slot_min_qty, slot_max_qty, - img_url, ftp, install_method, manual_url, access_rights) - VALUES - ({$homeCfgId}, 0, '{$svcName}', '{$svcName}', - '', 0, - 0.00, 0.00, 0.00, - 1, 100, - '{$guessedImg}', '', 'steamcmd', '', '')" - ); - $msg = "Added new service: " . $ch['game_name']; - if ($guessedImg !== '') { - $msg .= " (image auto-set: {$guessedImg})"; - } - $messages[] = $msg; - } - - // Soft-disable billing_services whose home_cfg_id no longer appears in config_homes. - foreach ($existing as $homeCfgId => $svcRow) { - if (!isset($configHomes[$homeCfgId])) { - $sid = (int)$svcRow['service_id']; - $db->query( - "UPDATE `{$tableName}` - SET enabled = 0 - WHERE service_id = {$sid} AND enabled = 1" - ); - if ($db->affected_rows > 0) { - $messages[] = "Service ID {$sid} disabled — game config no longer in config_homes."; - } - } - } - - return $messages; -} - -$syncMessages = sync_billing_services($db, $table_prefix); - -$flash = []; -$flashType = 'ok'; -$sort = strtolower((string)($_GET['sort'] ?? $_POST['sort'] ?? 'game')); -$dir = strtolower((string)($_GET['dir'] ?? $_POST['dir'] ?? 'asc')) === 'desc' ? 'desc' : 'asc'; -$gameMode = strtolower((string)($_GET['game_mode'] ?? $_POST['game_mode'] ?? 'name')); -if (!in_array($sort, ['game', 'config', 'enabled', 'month', 'servers'], true)) { - $sort = 'game'; -} -if (!in_array($gameMode, ['name', 'enabled'], true)) { - $gameMode = 'name'; -} -$sortQuery = http_build_query([ - 'sort' => $sort, - 'dir' => $dir, - 'game_mode' => $gameMode, -]); - -function sort_link_params(string $column, string $sort, string $dir, string $gameMode): array -{ - $nextDir = ($sort === $column && $dir === 'asc') ? 'desc' : 'asc'; - $nextGameMode = $gameMode; - if ($column === 'game' && $sort === 'game' && $gameMode === 'name') { - $nextGameMode = 'enabled'; - $nextDir = 'asc'; - } elseif ($column === 'game' && $sort === 'game' && $gameMode === 'enabled') { - $nextGameMode = 'name'; - $nextDir = 'asc'; - } elseif ($column !== 'game') { - $nextGameMode = 'name'; - } - return [ - 'sort' => $column, - 'dir' => $nextDir, - 'game_mode' => $nextGameMode, - ]; -} - -/* ----------------------------------------------------------------------- - SAVE: service configuration form submitted - Only admin-editable fields are updated; service_name and home_cfg_id - are never overwritten here. ------------------------------------------------------------------------ */ -if (isset($_POST['save_services']) || isset($_POST['save_row'])) { - // Load valid remote server IDs for validation - $validServerIds = []; - $rsRes = $db->query("SELECT remote_server_id FROM `{$table_prefix}remote_servers`"); - while ($rsRes && ($rsRow = $rsRes->fetch_assoc())) { - $validServerIds[] = (int)$rsRow['remote_server_id']; - } - $validSet = array_flip($validServerIds); - - $postedServices = $_POST['svc'] ?? []; - $postedServers = $_POST['servers'] ?? []; - $rowOnlyServiceId = isset($_POST['save_row']) ? (int)$_POST['save_row'] : 0; - $updatedCount = 0; - - foreach ((array)$postedServices as $sid => $svcData) { - $sid = (int)$sid; - if ($rowOnlyServiceId > 0 && $sid !== $rowOnlyServiceId) { - continue; - } - $enabled = isset($svcData['enabled']) ? 1 : 0; - $priceMonthly = number_format((float)($svcData['price_monthly'] ?? 0), 2, '.', ''); - $slotMin = max(1, (int)($svcData['slot_min_qty'] ?? 1)); - $slotMax = max(1, (int)($svcData['slot_max_qty'] ?? 1)); - if ($slotMax < $slotMin) { $slotMax = $slotMin; } - $description = $db->real_escape_string(substr((string)($svcData['description'] ?? ''), 0, 1000)); - // Merge dropdown and fallback text input: - // - dropdown value "__other__" means use the text fallback field - // - otherwise use the dropdown value (bare filename or '') - $rawImgUrl = (string)($svcData['img_url'] ?? ''); - if ($rawImgUrl === '__other__') { - $rawImgUrl = (string)($svcData['img_url_other'] ?? ''); - } - $imgUrl = $db->real_escape_string(substr($rawImgUrl, 0, 255)); - - // Build comma-separated remote_server_id from checkboxes, validating each ID - $checkedIds = []; - foreach ((array)($postedServers[$sid] ?? []) as $rawId) { - $rid = (int)$rawId; - if (isset($validSet[$rid])) { - $checkedIds[] = $rid; - } - } - $remoteServerIdStr = $db->real_escape_string(implode(',', $checkedIds)); - - $ok = $db->query( - "UPDATE `{$table_prefix}billing_services` - SET enabled = {$enabled}, - price_monthly = '{$priceMonthly}', - slot_min_qty = {$slotMin}, - slot_max_qty = {$slotMax}, - description = '{$description}', - img_url = '{$imgUrl}', - remote_server_id = '{$remoteServerIdStr}' - WHERE service_id = {$sid}" - ); - if ($ok) { - $updatedCount++; - } - } - - if ($updatedCount > 0) { - if ($rowOnlyServiceId > 0) { - $flash[] = "Service row #{$rowOnlyServiceId} saved."; - } else { - $flash[] = "{$updatedCount} service row(s) saved."; - } - } else { - $flashType = 'err'; - if ($rowOnlyServiceId > 0) { - $flash[] = "No changes were saved for service row #{$rowOnlyServiceId}."; - } else { - $flash[] = "No service rows were updated."; - } - } - $_SESSION['billing_adminserverlist_flash'] = ['type' => $flashType, 'messages' => $flash]; - header("Location: /adminserverlist.php?{$sortQuery}"); - exit; -} - -if (!empty($_SESSION['billing_adminserverlist_flash'])) { - $flashData = $_SESSION['billing_adminserverlist_flash']; - unset($_SESSION['billing_adminserverlist_flash']); - $flashType = ($flashData['type'] ?? 'ok') === 'err' ? 'err' : 'ok'; - $flash = array_values(array_filter((array)($flashData['messages'] ?? []), 'is_string')); -} - -/* ----------------------------------------------------------------------- - Load data for display — join config_homes to show the config XML filename ------------------------------------------------------------------------ */ -$remoteServers = []; -$rsRes = $db->query( - "SELECT remote_server_id, remote_server_name - FROM `{$table_prefix}remote_servers` - ORDER BY remote_server_name" -); -while ($rsRes && ($row = $rsRes->fetch_assoc())) { - $remoteServers[] = $row; -} - -$services = []; -$svcRes = $db->query( - "SELECT bs.service_id, bs.service_name, bs.enabled, - bs.price_monthly, - bs.slot_min_qty, bs.slot_max_qty, - bs.remote_server_id, bs.description, bs.img_url, - ch.home_cfg_file - FROM `{$table_prefix}billing_services` bs - LEFT JOIN `{$table_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id - ORDER BY bs.service_name" -); -while ($svcRes && ($row = $svcRes->fetch_assoc())) { - $services[] = $row; -} -if (!empty($services)) { - usort($services, function (array $a, array $b) use ($sort, $dir, $gameMode): int { - $cmp = 0; - switch ($sort) { - case 'config': - $cmp = strcasecmp((string)($a['home_cfg_file'] ?? ''), (string)($b['home_cfg_file'] ?? '')); - break; - case 'enabled': - $cmp = ((int)($a['enabled'] ?? 0)) <=> ((int)($b['enabled'] ?? 0)); - break; - case 'month': - $cmp = ((float)($a['price_monthly'] ?? 0)) <=> ((float)($b['price_monthly'] ?? 0)); - break; - case 'servers': - $countA = trim((string)($a['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$a['remote_server_id']), 'strlen')); - $countB = trim((string)($b['remote_server_id'] ?? '')) === '' ? 0 : count(array_filter(explode(',', (string)$b['remote_server_id']), 'strlen')); - $cmp = $countA <=> $countB; - break; - case 'game': - default: - if ($gameMode === 'enabled') { - $cmp = ((int)($b['enabled'] ?? 0)) <=> ((int)($a['enabled'] ?? 0)); - if ($cmp === 0) { - $cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? '')); - } - } else { - $cmp = strcasecmp((string)($a['service_name'] ?? ''), (string)($b['service_name'] ?? '')); - } - break; - } - if ($cmp === 0) { - $cmp = ((int)($a['service_id'] ?? 0)) <=> ((int)($b['service_id'] ?? 0)); - } - return $dir === 'desc' ? -$cmp : $cmp; - }); -} -?> - - -
- - -

Service Configuration

-

- Enable services, configure pricing and slot ranges, and select which remote servers - each game can be installed on. The service list is automatically kept in sync with - the panel game configuration (config_homes). Check one or more servers - to make a game available for purchase; leaving all servers unchecked prevents the - game from appearing in the store. -

- - -

No billing services found. Ensure game configs are loaded in the panel (Home → Games configuration).

- - -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - Game Name - - - Config XML - - - Enabled - Min SlotsMax Slots - - Price / Month ($) - DescriptionImage - - Available Servers - Action
- -
ID:
-
- —'; ?> - - - > - - - - - - - - - - - - - - - - No remote servers configured - - - - - - - -
-
- -
- -
-
- - - -
-

Notes:

-
    -
  • A service will only appear in the store when Enabled is checked - and at least one server is selected.
  • -
  • Price / Month ($) is the canonical billing price used by cart, checkout, and provisioning.
  • -
  • The Game Name and Config XML columns are sourced - from and are read-only - here. To change them, update the game XML config in the panel.
  • -
  • Available servers are stored as a comma-separated list of server IDs in - .
  • -
  • The service list is automatically synced with the panel game configuration on - every page load. New games are added with Enabled = off so they do not - appear in the store until you configure and enable them.
  • -
  • Games removed from the panel configuration are disabled automatically; they are - never deleted while orders may reference them.
  • -
-
- - - - - - diff --git a/Panel/modules/billing/ai.php b/Panel/modules/billing/ai.php deleted file mode 100644 index a32eb683..00000000 --- a/Panel/modules/billing/ai.php +++ /dev/null @@ -1,326 +0,0 @@ -= 400) { - $msg = isset($data['error']['message']) ? $data['error']['message'] : 'Unknown API error'; - throw new RuntimeException("OpenAI API error ({$code}): {$msg}"); - } - return is_array($data) ? $data : []; -} - -/** Create or reuse a per-visitor thread */ -function ensure_thread_id() { - if (!empty($_SESSION['thread_id'])) return $_SESSION['thread_id']; - $created = openai_request('POST', '/threads', ['metadata' => ['site' => $_SERVER['HTTP_HOST'] ?? 'unknown']]); - $tid = $created['id'] ?? null; - if (!$tid) throw new RuntimeException('Failed to create thread.'); - $_SESSION['thread_id'] = $tid; - return $tid; -} - -/** Add a user message */ -function add_user_message($thread_id, $text) { - openai_request('POST', "/threads/{$thread_id}/messages", [ - 'role' => 'user', - 'content' => $text, - ]); -} - -/** Start a run */ -function start_run($thread_id, $assistant_id) { - $run = openai_request('POST', "/threads/{$thread_id}/runs", [ - 'assistant_id' => $assistant_id, - ]); - $run_id = $run['id'] ?? null; - if (!$run_id) throw new RuntimeException('Failed to start run.'); - return $run_id; -} - -/** Wait for completion (or fail/timeout) */ -function wait_for_run($thread_id, $run_id, $max_tries, $delay_us) { - $terminal = ['completed', 'failed', 'requires_action', 'cancelled', 'expired']; - for ($i = 0; $i < $max_tries; $i++) { - usleep($delay_us); - $run = openai_request('GET', "/threads/{$thread_id}/runs/{$run_id}"); - $status = $run['status'] ?? ''; - if (in_array($status, $terminal, true)) return $run; - } - return ['status' => 'timeout']; -} - -/** Cache of file_id => filename (per request) */ -$_FILE_NAME_CACHE = []; - -/** Resolve file name from file_id (API returns "filename" or sometimes "display_name") */ -function get_file_name_by_id($file_id) { - global $_FILE_NAME_CACHE; - if (isset($_FILE_NAME_CACHE[$file_id])) return $_FILE_NAME_CACHE[$file_id]; - $file = openai_request('GET', "/files/{$file_id}"); - $name = $file['filename'] ?? ($file['display_name'] ?? ($file['name'] ?? $file_id)); - $_FILE_NAME_CACHE[$file_id] = $name; - return $name; -} - -/** - * Extract message text + citations (filename + page if available). - * Returns an array of entries: ['role' => 'user|assistant', 'text' => '...', 'refs' => [['filename'=>'','page'=>'','file_id'=>'']]] - */ -function normalize_messages($messages) { - $out = []; - if (empty($messages['data']) || !is_array($messages['data'])) return $out; - - // The API returns newest first by default if not specifying; we request 'asc' in fetch. - foreach ((array)$messages['data'] as $m) { - $role = $m['role'] ?? ''; - if (!in_array($role, ['user', 'assistant', 'system'], true)) continue; - - if (empty($m['content']) || !is_array($m['content'])) continue; - - $all_text = []; - $refs = []; - foreach ((array)$m['content'] as $part) { - if (($part['type'] ?? '') === 'text' && !empty($part['text']['value'])) { - $all_text[] = $part['text']['value']; - - // Parse annotations for citations (file_citation) - $anns = $part['text']['annotations'] ?? []; - if (is_array($anns)) { - foreach ((array)$anns as $ann) { - if (($ann['type'] ?? '') === 'file_citation' && !empty($ann['file_citation']['file_id'])) { - $fid = $ann['file_citation']['file_id']; - $page = null; - - // Page can appear under different shapes depending on backend. Try common keys: - if (isset($ann['file_citation']['page'])) { - $page = $ann['file_citation']['page']; - } elseif (isset($ann['file_citation']['page_range']) && is_array($ann['file_citation']['page_range'])) { - // Example: ['start' => 5, 'end' => 6] - $start = $ann['file_citation']['page_range']['start'] ?? null; - $end = $ann['file_citation']['page_range']['end'] ?? null; - if ($start && $end && $start !== $end) $page = "{$start}-{$end}"; - elseif ($start) $page = (string)$start; - } - // Fetch filename - try { - $filename = get_file_name_by_id($fid); - } catch (Throwable $e) { - $filename = $fid; - } - $refs[] = [ - 'file_id' => $fid, - 'filename' => $filename, - 'page' => $page ?? 'n/a', - ]; - } - } - } - } - } - - if (!empty($all_text)) { - $out[] = [ - 'role' => $role, - 'text' => implode("\n", $all_text), - 'refs' => $refs, - ]; - } - } - return $out; -} - -/** Fetch conversation (ascending) */ -function fetch_history($thread_id) { - $messages = openai_request('GET', "/threads/{$thread_id}/messages", null, ['order' => 'asc', 'limit' => 50]); - return normalize_messages($messages); -} - -/* ------------------- HANDLE POST ------------------- */ -$error = null; -$history = []; - -try { - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!empty($_POST['reset_thread'])) { - $_SESSION['thread_id'] = null; - } elseif (isset($_POST['user_input'])) { - $user_text = trim((string)$_POST['user_input']); - if ($user_text !== '') { - $thread_id = ensure_thread_id(); - add_user_message($thread_id, $user_text); - $run_id = start_run($thread_id, $ASSISTANT_ID); - $run = wait_for_run($thread_id, $run_id, $POLL_MAX_TRIES, $POLL_DELAY_US); - - if (($run['status'] ?? '') === 'failed') { - $error = 'Assistant run failed.'; - } elseif (($run['status'] ?? '') === 'requires_action') { - // If you later support tool calls, handle them here then submit outputs. - } elseif (($run['status'] ?? '') === 'timeout') { - $error = 'Assistant timed out. Please try again.'; - } - } - } - } - - if (!empty($_SESSION['thread_id'])) { - $history = fetch_history($_SESSION['thread_id']); - } -} catch (Throwable $e) { - $error = $e->getMessage(); -} -?> - - -
-

Site Assistant

-

Type a question below. Press Enter to send, Shift+Enter for a new line.

- - -
- Error: -
- - - -
Thread:
- - -
- -
- - -
-
- - -
- Question, assistant => Answer, system => (optional) - $role = $msg['role'] ?? 'assistant'; - if ($role === 'user') $label = 'Question'; - elseif ($role === 'assistant') $label = 'Answer'; - else $label = ucfirst($role); // e.g., System - $text = str_replace("\r\n", "\n", $msg['text'] ?? ''); - $refs = $msg['refs'] ?? []; - ?> -
-
-
- - -
- References: - -
- -
- -
- -
No messages yet.
- - -
- Conversation persists until you click “Reset Conversation”. -
-
- - - - diff --git a/Panel/modules/billing/api/capture_order.php b/Panel/modules/billing/api/capture_order.php deleted file mode 100644 index 1f1b0538..00000000 --- a/Panel/modules/billing/api/capture_order.php +++ /dev/null @@ -1,433 +0,0 @@ - array_keys($_SESSION)]); - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'no_user_session', - 'message' => 'You must be logged in to complete payment.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -// Parse input -$rawInput = file_get_contents('php://input'); -$input = json_decode($rawInput, true); -if (json_last_error() !== JSON_ERROR_NONE) { - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'invalid_json', - 'message' => 'Invalid JSON in request body.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -$paypalOrderId = $input['order_id'] ?? null; -if (!$paypalOrderId) { - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'missing_order_id', - 'message' => 'Missing PayPal order ID.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -cap_log('REQUEST', ['order_id' => $paypalOrderId, 'user_id' => $userId]); - -// DB connection -$port = intval($db_port ?? 3306) ?: 3306; -$mysqli = @mysqli_connect($db_host, $db_user, $db_pass, $db_name, $port); -if (!$mysqli) { - cap_log('DB_FAILED', mysqli_connect_error()); - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'db_connection_failed', - 'message' => 'Database connection failed.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} -mysqli_set_charset($mysqli, 'utf8mb4'); - -$prefix = $table_prefix ?? 'gsp_'; -$repo = new BillingRepository($mysqli, $prefix); - -function cap_invoice_ids_from_custom_id(?string $customId): array { - if (!is_string($customId) || $customId === '') { - return []; - } - if (ctype_digit($customId)) { - return [intval($customId)]; - } - if (stripos($customId, 'cart:') !== 0) { - return []; - } - $parts = explode(',', substr($customId, 5)); - $invoiceIds = []; - foreach ($parts as $part) { - $part = trim($part); - if ($part !== '' && ctype_digit($part)) { - $invoiceIds[] = intval($part); - } - } - return array_values(array_unique($invoiceIds)); -} - -function cap_get_duration_metadata(array $invoice): array { - return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; -} - -function cap_get_end_date(array $invoice, ?string $fromDate = null): string { - $meta = cap_get_duration_metadata($invoice); - $qty = max(1, intval($invoice['qty'] ?? 1)); - $baseTs = time(); - if (!empty($fromDate)) { - $fromTs = strtotime($fromDate); - if ($fromTs !== false && $fromTs > time()) { - $baseTs = $fromTs; - } - } - return date('Y-m-d H:i:s', $baseTs + ($meta['days'] * $qty * 86400)); -} - -function cap_discount_map(array $invoices, float $paidAmount): array { - $baseTotals = []; - $baseAmount = 0.0; - foreach ($invoices as $invoice) { - $invoiceId = intval($invoice['invoice_id'] ?? 0); - $lineBase = round((float)($invoice['subtotal'] ?? $invoice['total_due'] ?? $invoice['amount'] ?? 0), 2); - $baseTotals[$invoiceId] = $lineBase; - $baseAmount += $lineBase; - } - - $discountTotal = round(max(0, $baseAmount - $paidAmount), 2); - if ($discountTotal <= 0 || $baseAmount <= 0) { - return array_fill_keys(array_keys($baseTotals), 0.0); - } - - $discounts = []; - $remaining = $discountTotal; - $lastInvoiceId = array_key_last($baseTotals); - foreach ($baseTotals as $invoiceId => $lineBase) { - if ($invoiceId === $lastInvoiceId) { - $lineDiscount = $remaining; - } else { - $lineDiscount = round($discountTotal * ($lineBase / $baseAmount), 2); - $remaining = round($remaining - $lineDiscount, 2); - } - $discounts[$invoiceId] = min($lineBase, max(0, $lineDiscount)); - } - - return $discounts; -} - -// Capture payment via PayPal gateway -try { - $gateway = GatewayFactory::make('paypal'); -} catch (Exception $e) { - cap_log('GATEWAY_ERROR', $e->getMessage()); - $repo->logPaypalError([ - 'context' => 'gateway_init', - 'error_code' => 'gateway_init_failed', - 'message' => $e->getMessage(), - 'order_id' => $paypalOrderId, - 'user_id' => $userId, - ]); - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'gateway_init_failed', - 'message' => 'Payment gateway initialisation failed.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - mysqli_close($mysqli); - exit; -} - -$capture = $gateway->handleCallback(['order_id' => $paypalOrderId]); -cap_log('CAPTURE_RESULT', ['success' => $capture['success'], 'txid' => $capture['transaction_id'] ?? null]); - -if (!$capture['success']) { - cap_log('CAPTURE_FAILED', $capture); - // Sanitize raw capture data before logging — never store secrets - $captureForLog = $capture; - foreach (['client_secret', 'access_token', 'refresh_token'] as $_sk) { - unset($captureForLog[$_sk]); - } - $repo->logPaypalError([ - 'context' => 'capture_order', - 'error_code' => $capture['error'] ?? 'capture_failed', - 'message' => $capture['message'] ?? 'PayPal order capture failed.', - 'paypal_debug_id' => $capture['debug_id'] ?? null, - 'order_id' => $paypalOrderId, - 'user_id' => $userId, - 'raw_json' => $captureForLog, - ]); - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => $capture['error'] ?? 'capture_failed', - 'message' => $capture['message'] ?? 'PayPal order capture failed. Please try again.', - 'debug_id' => $capture['debug_id'] ?? null, - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - mysqli_close($mysqli); - exit; -} - -$txid = $capture['transaction_id'] ?? ''; -$paidAmount = round((float)($capture['amount'] ?? 0), 2); -$capture['payment_method'] = 'paypal'; -$invoiceIds = cap_invoice_ids_from_custom_id($capture['custom_id'] ?? null); -$invoices = !empty($invoiceIds) - ? $repo->getInvoicesForUserByIds($userId, $invoiceIds, true) - : $repo->getUnpaidInvoicesForUser($userId); -$invoicesPaid = 0; -$ordersCreated = 0; -$newOrderIds = []; -$now = date('Y-m-d H:i:s'); -$couponId = intval($_SESSION['cart_coupon_id'] ?? 0); -$discountMap = cap_discount_map($invoices, $paidAmount); -$couponCode = trim((string)($_SESSION['cart_coupon_code'] ?? '')); - -if ($couponId <= 0 && $couponCode !== '') { - $coupon = $repo->getCouponByCode($couponCode); - $couponId = intval($coupon['coupon_id'] ?? 0); -} - -if (empty($invoices)) { - cap_log('NO_INVOICES', ['user_id' => $userId, 'custom_id' => $capture['custom_id'] ?? null]); - ob_clean(); - echo json_encode([ - 'success' => false, - 'error_code' => 'no_matching_invoices', - 'message' => 'No matching unpaid invoices were found for this payment.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - mysqli_close($mysqli); - exit; -} - -foreach ($invoices as $inv) { - $invoiceId = intval($inv['invoice_id']); - $homeId = intval($inv['home_id'] ?? 0); - $invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2); - $lineDiscount = round((float)($discountMap[$invoiceId] ?? 0), 2); - $lineTotal = round(max(0, $invoiceBase - $lineDiscount), 2); - $durationMeta = cap_get_duration_metadata($inv); - - $invoiceUpdate = [ - 'coupon_id' => $couponId, - 'discount_amount' => $lineDiscount, - 'subtotal' => $invoiceBase, - 'amount' => $lineTotal, - 'total_due' => $lineTotal, - 'status' => 'paid', - 'billing_status' => 'Active', - 'payment_status' => 'paid', - 'payment_txid' => $txid, - 'payment_method' => 'paypal', - 'paid_date' => $now, - 'invoice_duration' => $durationMeta['invoice_duration'], - 'rate_type' => $durationMeta['rate_type'], - ]; - - if (!$repo->updateInvoiceFields($invoiceId, $invoiceUpdate)) { - cap_log('INVOICE_PAY_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]); - continue; - } - - $invoicesPaid++; - cap_log('INVOICE_PAID', ['invoice_id' => $invoiceId, 'txid' => $txid, 'amount' => $lineTotal]); - - $rawCapture = $capture['raw_response'] ?? []; - if (is_array($rawCapture)) { - unset($rawCapture['client_secret'], $rawCapture['access_token']); // never log secrets - } - - // Resolve (or create) the billing_orders row for this invoice so the provisioner can run. - // billing_orders.status='Active' is what create_servers.php queries. - $orderId = intval($inv['order_id'] ?? 0); - $currentHomeId = $homeId; - - if ($orderId > 0) { - // Existing order linked to this invoice — extend it and mark Active. - $order = $repo->getOrder($orderId); - if ($order) { - $newEnd = cap_get_end_date($inv, $order['end_date'] ?? null); - $currentHomeId = intval($order['home_id'] ?? 0); - $repo->updateOrderFields($orderId, [ - 'status' => 'Active', - 'end_date' => $newEnd, - 'payment_txid' => $txid, - 'paid_ts' => $now, - 'price' => $lineTotal, - 'discount_amount' => $lineDiscount, - 'coupon_id' => $couponId, - ]); - if ($currentHomeId > 0) { - $repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]); - } - $ordersCreated++; - if (!in_array($orderId, $newOrderIds, true)) { - $newOrderIds[] = $orderId; - } - cap_log('ORDER_QUEUED_PROVISION', ['order_id' => $orderId, 'home_id' => $currentHomeId]); - } - } else { - // No billing_orders row yet — create one now so the provisioner can run. - $newEnd = cap_get_end_date($inv, null); - $newOrderId = $repo->createOrder([ - 'user_id' => intval($inv['user_id']), - 'service_id' => intval($inv['service_id']), - 'home_name' => $inv['home_name'] ?? '', - 'ip' => (string)($inv['ip'] ?? '0'), - 'qty' => intval($inv['qty'] ?? 1), - 'invoice_duration' => $durationMeta['invoice_duration'], - 'max_players' => intval($inv['max_players'] ?? 0), - 'price' => $lineTotal, - 'discount_amount' => $lineDiscount, - 'remote_control_password' => $inv['remote_control_password'] ?? '', - 'ftp_password' => $inv['ftp_password'] ?? '', - 'status' => 'Active', - 'end_date' => $newEnd, - 'payment_txid' => $txid, - 'paid_ts' => $now, - 'coupon_id' => $couponId, - ]); - if ($newOrderId > 0) { - // Link invoice → order so retried captures are idempotent. - $repo->updateInvoiceOrderId($invoiceId, $newOrderId); - $repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]); - if (!in_array($newOrderId, $newOrderIds, true)) { - $newOrderIds[] = $newOrderId; - } - $ordersCreated++; - cap_log('ORDER_CREATED', ['invoice_id' => $invoiceId, 'order_id' => $newOrderId]); - } else { - cap_log('ORDER_CREATE_FAILED', ['invoice_id' => $invoiceId, 'db_error' => $mysqli->error]); - continue; - } - } - - $repo->logTransaction([ - 'invoice_id' => $invoiceId, - 'user_id' => $userId, - 'home_id' => $currentHomeId, - 'payment_method' => 'paypal', - 'transaction_external_id' => $txid, - 'amount' => $lineTotal, - 'currency' => (string)($inv['currency'] ?? 'USD'), - 'status' => 'completed', - 'raw_response' => $rawCapture, - ]); -} - -if ($couponId > 0 && $invoicesPaid > 0) { - $mysqli->query("UPDATE `{$prefix}billing_coupons` - SET current_uses = current_uses + 1 - WHERE coupon_id = " . intval($couponId)); -} - -// Auto-provision new servers (orders without a home_id) -$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0]; -if (!empty($newOrderIds)) { - require_once __DIR__ . '/../includes/panel_bridge.php'; - $panelCtx = billing_panel_bootstrap(); - if ($panelCtx && isset($panelCtx['db'])) { - $GLOBALS['db'] = $panelCtx['db']; - $GLOBALS['settings'] = $panelCtx['settings']; - require_once __DIR__ . '/../create_servers.php'; - $autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]); - if (($autoProvision['failed_count'] ?? 0) > 0) { - cap_log('AUTO_PROVISION_PARTIAL_FAILURE', $autoProvision); - } - } else { - cap_log('AUTO_PROVISION_SKIPPED', 'panel bootstrap failed — orders require manual provisioning: ' . implode(',', $newOrderIds)); - $autoProvision = [ - 'provisioned_count' => 0, - 'failed_count' => count($newOrderIds), - 'details' => [], - 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log', - 'trace_error' => 'Panel bootstrap failed before billing provisioning could start.', - ]; - } -} -if (function_exists('billing_store_provision_session_result')) { - billing_store_provision_session_result($txid, [ - 'source' => 'api/capture_order.php', - 'txid' => $txid, - 'order_ids' => $newOrderIds, - 'result' => $autoProvision, - ]); -} - -unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']); - -mysqli_close($mysqli); - -cap_log('COMPLETE', ['invoices_paid' => $invoicesPaid, 'txid' => $txid, 'orders' => $newOrderIds]); - -ob_clean(); -echo json_encode([ - 'success' => true, - 'status' => 'COMPLETED', - 'txid' => $txid, - 'invoices_paid' => $invoicesPaid, - 'orders_created' => $ordersCreated, - 'provisioned' => $autoProvision['provisioned_count'] ?? 0, - 'request_id' => $requestId, -]); diff --git a/Panel/modules/billing/api/create_order.php b/Panel/modules/billing/api/create_order.php deleted file mode 100644 index 5fd3c418..00000000 --- a/Panel/modules/billing/api/create_order.php +++ /dev/null @@ -1,104 +0,0 @@ - false, - 'error_code' => 'invalid_json', - 'message' => 'Invalid JSON in request body.', - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -co_log('REQUEST', ['amount' => $in['amount'] ?? null, 'invoice_id' => $in['invoice_id'] ?? null]); - -// Resolve portable return/cancel URLs -$returnUrl = $in['return_url'] ?? '/payment_success.php'; -$cancelUrl = $in['cancel_url'] ?? '/payment_cancel.php'; - -// Ensure absolute URLs -if (strpos($returnUrl, 'http') !== 0) { - $returnUrl = billing_abs_url($returnUrl); -} -if (strpos($cancelUrl, 'http') !== 0) { - $cancelUrl = billing_abs_url($cancelUrl); -} - -// Build gateway params -$params = [ - 'amount' => $in['amount'] ?? '0.00', - 'currency' => $in['currency'] ?? 'USD', - 'invoice_id' => $in['invoice_id'] ?? null, - 'custom_id' => $in['custom_id'] ?? $in['invoice_id'] ?? null, - 'description' => $in['description'] ?? 'Game Server Order', - 'return_url' => $returnUrl, - 'cancel_url' => $cancelUrl, - 'items' => $in['items'] ?? null, -]; - -try { - $gateway = GatewayFactory::make('paypal'); - $result = $gateway->createPayment($params); -} catch (Exception $e) { - co_log('EXCEPTION', $e->getMessage()); - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error_code' => 'gateway_error', - 'message' => $e->getMessage(), - 'debug_id' => null, - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -if (!$result['success']) { - co_log('CREATE_FAILED', $result); - http_response_code(500); - echo json_encode([ - 'success' => false, - 'error_code' => $result['error'] ?? 'create_failed', - 'message' => $result['message'] ?? 'Failed to create PayPal order.', - 'debug_id' => $result['debug_id'] ?? null, - 'timestamp' => date('c'), - 'request_id' => $requestId, - ]); - exit; -} - -co_log('CREATE_SUCCESS', ['provider_order_id' => $result['provider_order_id']]); -echo json_encode($result['raw_response']); diff --git a/Panel/modules/billing/api/log_error.php b/Panel/modules/billing/api/log_error.php deleted file mode 100644 index 79d57aeb..00000000 --- a/Panel/modules/billing/api/log_error.php +++ /dev/null @@ -1,44 +0,0 @@ - 'logged']); -} else { - log_client_error(['raw_input' => $rawInput, 'error' => 'Invalid JSON']); - echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']); -} -?> diff --git a/Panel/modules/billing/billing_bootstrap.php b/Panel/modules/billing/billing_bootstrap.php deleted file mode 100644 index 1ca5091d..00000000 --- a/Panel/modules/billing/billing_bootstrap.php +++ /dev/null @@ -1,175 +0,0 @@ -real_escape_string((string)$v); - } - return addslashes((string)$v); - } -} - -if (!function_exists('fetch_all_assoc')) { - function fetch_all_assoc($db, $sql) - { - if (!($db instanceof mysqli)) return []; - $res = $db->query($sql); - return $res ? $res->fetch_all(MYSQLI_ASSOC) : []; - } -} - -if (!function_exists('col_exists')) { - function col_exists($db, $table, $col) - { - if (!($db instanceof mysqli)) return false; - $t = $db->real_escape_string($table); - $c = $db->real_escape_string($col); - $res = $db->query("SHOW COLUMNS FROM `{$t}` LIKE '{$c}'"); - return ($res && $res->num_rows > 0); - } -} - -// expose a convenience variable for scripts that expect $db -// Do not overwrite an existing $db if present -if (!isset($db) || !($db instanceof mysqli)) { - $maybe = billing_get_db(); - if ($maybe instanceof mysqli) { - $db = $maybe; - } -} - -/** - * Resolve a billing_services.img_url value to a browser-safe URL. - * - * Rules: - * - Empty string → '' (caller should skip the tag). - * - Full URL (http:// or https://) → returned as-is. - * - Bare filename (e.g. "dayz.jpg") → "/images/games/{filename}". - * - Anything else treated as a bare filename for safety. - * - * Output is NOT htmlspecialchars'd here; callers must escape for HTML context. - */ -if (!function_exists('billing_image_url')) { - function billing_image_url(string $imgUrl): string - { - $imgUrl = trim($imgUrl); - if ($imgUrl === '') { - return ''; - } - // Keep full external URLs intact - if (str_starts_with($imgUrl, 'http://') || str_starts_with($imgUrl, 'https://')) { - return $imgUrl; - } - // Strip any leading path separators/directories so we always get a bare filename - $filename = basename($imgUrl); - if ($filename === '') { - return ''; - } - return billing_url('images/games/' . $filename); - } -} - -// End bootstrap diff --git a/Panel/modules/billing/cart.php b/Panel/modules/billing/cart.php deleted file mode 100644 index 26be2fb4..00000000 --- a/Panel/modules/billing/cart.php +++ /dev/null @@ -1,956 +0,0 @@ -= intval($coupon['max_uses'])) { - $max_uses_reached = true; - } - } - - if ($expired) { - $error_message = 'This coupon has expired.'; - } elseif ($max_uses_reached) { - $error_message = 'This coupon has reached its maximum usage limit.'; - } else { - // Check game filter - $game_valid = true; - if ($coupon['game_filter_type'] === 'specific_games' && !empty($coupon['game_filter_list'])) { - $allowed_games = json_decode($coupon['game_filter_list'], true); - if (is_array($allowed_games) && count((array)$allowed_games) > 0) { - $has_valid_game = false; - foreach ((array)$invoices as $inv) { - $inv_game_key = isset($inv['game_key']) ? $inv['game_key'] : null; - if ($inv_game_key !== null && in_array($inv_game_key, $allowed_games)) { - $has_valid_game = true; - break; - } - } - if (!$has_valid_game) { - $game_valid = false; - } - } - } - - if (!$game_valid) { - $error_message = 'This coupon is not valid for the items in your cart.'; - } else { - // Apply coupon - $applied_coupon = $coupon; - $coupon_discount_percent = floatval($coupon['discount_percent']); - $_SESSION['cart_coupon_code'] = $coupon_code; - $_SESSION['cart_coupon_id'] = $coupon['coupon_id']; - $success_message = 'Coupon "' . htmlspecialchars($coupon['name']) . '" applied! You save ' . $coupon_discount_percent . '%'; - } - } - mysqli_free_result($coupon_result); - } else { - $error_message = 'Invalid coupon code.'; - } - } - } -} - -// Handle coupon removal -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_coupon'])) { - unset($_SESSION['cart_coupon_code']); - unset($_SESSION['cart_coupon_id']); - $applied_coupon = null; - $coupon_discount_percent = 0; -} - -// Re-validate coupon from session if present -if ($db && empty($applied_coupon) && isset($_SESSION['cart_coupon_code'])) { - $coupon_code = $_SESSION['cart_coupon_code']; - $safe_code = mysqli_real_escape_string($db, $coupon_code); - $coupon_query = "SELECT * FROM {$table_prefix}billing_coupons - WHERE code = '$safe_code' AND is_active = 1"; - $coupon_result = mysqli_query($db, $coupon_query); - - if ($coupon_result && mysqli_num_rows($coupon_result) === 1) { - $applied_coupon = mysqli_fetch_assoc($coupon_result); - $coupon_discount_percent = floatval($applied_coupon['discount_percent']); - mysqli_free_result($coupon_result); - } else { - // Coupon no longer valid, clear from session - unset($_SESSION['cart_coupon_code']); - unset($_SESSION['cart_coupon_id']); - } -} - -// AJAX remove invoice action (hard delete) - returns JSON when remove_invoice_ajax is set -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_invoice_ajax']) && isset($_POST['invoice_id'])) { - header('Content-Type: application/json'); - $remove_id = intval($_POST['invoice_id']); - if ($remove_id <= 0) { - echo json_encode(['success' => false, 'error' => 'Invalid invoice id.']); - exit; - } - - if (!$db) { - echo json_encode(['success' => false, 'error' => 'Database unavailable.']); - exit; - } - - // Verify ownership and that invoice is still unpaid/due - $check_q = "SELECT invoice_id FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1"; - $check_r = mysqli_query($db, $check_q); - if (!($check_r && mysqli_num_rows($check_r) === 1)) { - echo json_encode(['success' => false, 'error' => 'Invoice not found or cannot be removed.']); - exit; - } - - // Hard-delete the invoice row - $del_q = "DELETE FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1"; - $ok = mysqli_query($db, $del_q); - if ($ok && mysqli_affected_rows($db) > 0) { - echo json_encode(['success' => true]); - } else { - echo json_encode(['success' => false, 'error' => 'Failed to delete invoice.']); - } - exit; -} - -// Handle non-AJAX remove invoice action (hard delete + redirect) -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['remove_invoice']) && isset($_POST['invoice_id'])) { - $remove_id = intval($_POST['invoice_id']); - if ($remove_id <= 0) { - $error_message = 'Invalid invoice id.'; - } else { - if (!$db) { - $error_message = 'Unable to remove item: database unavailable.'; - } else { - // Verify ownership and that invoice is still unpaid/due - $check_q = "SELECT invoice_id FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1"; - $check_r = mysqli_query($db, $check_q); - if ($check_r && mysqli_num_rows($check_r) === 1) { - // Hard-delete to remove from cart - $del_q = "DELETE FROM {$table_prefix}billing_invoices WHERE invoice_id = " . intval($remove_id) . " AND user_id = " . intval($user_id) . " AND (status = 'due' OR status = '') AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) LIMIT 1"; - if (mysqli_query($db, $del_q)) { - // Reload to avoid form re-submission and refresh invoice list - header('Location: ' . billing_url('cart.php')); - exit; - } else { - $error_message = 'Failed to remove item from cart.'; - } - } else { - $error_message = 'Invoice not found or cannot be removed.'; - } - } - } -} - -// Calculate discount -if ($applied_coupon && $coupon_discount_percent > 0) { - $discount_amount_cents = (int) round($total_amount_cents * ($coupon_discount_percent / 100)); - $discount_amount_cents = min($discount_amount_cents, $total_amount_cents); -} - -$discount_amount = billing_cart_cents_to_money($discount_amount_cents); -$final_amount_cents = max(0, $total_amount_cents - $discount_amount_cents); -$final_amount = billing_cart_cents_to_money($final_amount_cents); - -// PayPal configuration (from config) -$client_id = function_exists('gsp_paypal_get_client_id') ? gsp_paypal_get_client_id() : ($paypal_client_id ?? ''); -$sandbox = function_exists('gsp_paypal_is_sandbox') ? gsp_paypal_is_sandbox() : ($paypal_sandbox ?? true); - -// Prepare PayPal items -$paypal_items = []; -$paypal_invoice_ids = []; -foreach ((array)$invoices as $inv) { - $game_display = !empty($inv['game_name']) ? $inv['game_name'] : 'Game Server'; - $qty = max(1, intval($inv['qty'])); - $paypal_invoice_ids[] = intval($inv['invoice_id']); - $lineAmountCents = billing_cart_money_to_cents((float)($inv['total_due'] ?? $inv['amount'] ?? 0)); - $lineAmount = billing_cart_cents_to_money($lineAmountCents); - $paypal_items[] = [ - 'name' => $inv['home_name'] . ' (' . $game_display . ')', - 'description' => $inv['description'] ?? '', - 'quantity' => $qty, - 'unit_amount' => [ - 'currency_code' => 'USD', - 'value' => number_format($lineAmount / $qty, 2, '.', '') - ] - ]; -} -$paypal_custom_id = 'cart:' . implode(',', $paypal_invoice_ids); - -// Get site base URL -$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://'; -$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; -$siteBase = $protocol . $host; - -// (Do not close the shared DB connection here; menu and other includes may use it.) -?> - - - - - - Shopping Cart - Game Server Panel - - - - - - - - - - - - - - - -
- -
- Database error: -
- -

🛒 Shopping Cart

- - -
- - - -
- - - -
-

Your cart is empty

-

Browse our game servers and add them to your cart to get started!

- Browse Servers -
- -
- - - - - - - - - - - - - - - - - - - - - - - -
Game ServerDurationQuantityStatusPriceAction
-
-
- -
- -
x - $ - - -
-
- - -
-

Coupon Code

- - -
-
- - -
- -
- -
-
- Coupon Applied: - - (% off) -
-
- -
-
- -
- - -
- 0): ?> -
- Subtotal: - $ -
-
- Discount (%): - -$ -
- -
- Total: - $ -
-
- - - - -
-

🎉 Complete Your Free Order

-

Your coupon covers the full amount. Click below to confirm and automatically provision your server(s).

-
-
- - - -
- -
- -
-

Checkout with PayPal

- -
- Checkout Unavailable: PayPal has not been configured for this site. - Please contact the site administrator or try again later. - 0 && $db) { - $ar = mysqli_query($db, "SELECT users_role FROM {$table_prefix}users WHERE user_id = " . $cart_user_id_check . " LIMIT 1"); - if ($ar && ($arow = mysqli_fetch_assoc($ar))) { - $cart_is_admin = strtolower($arow['users_role'] ?? '') === 'admin'; - } - } - if ($cart_is_admin): - ?> -
Admin: configure PayPal credentials in Site Config. - -
- -

Click the button below to complete your purchase securely through PayPal.

-
-
- - -
- - - - - 0 && !empty($client_id)): ?> - - - - -
- - - diff --git a/Panel/modules/billing/check_table.php b/Panel/modules/billing/check_table.php deleted file mode 100644 index 9982441f..00000000 --- a/Panel/modules/billing/check_table.php +++ /dev/null @@ -1,76 +0,0 @@ -{$table_prefix}billing_invoices Table Structure\n"; - -$result = mysqli_query($db, "DESCRIBE {$table_prefix}billing_invoices"); - -if (!$result) { - die("Table doesn't exist or query failed: " . mysqli_error($db)); -} - -echo "\n"; -echo "\n"; - -while ($row = mysqli_fetch_assoc($result)) { - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo ""; - echo "\n"; -} - -echo "
FieldTypeNullKeyDefaultExtra
{$row['Field']}{$row['Type']}{$row['Null']}{$row['Key']}" . ($row['Default'] ?? 'NULL') . "{$row['Extra']}
\n"; - -// Count existing invoices -$count_result = mysqli_query($db, "SELECT COUNT(*) as cnt FROM {$table_prefix}billing_invoices"); -$count = mysqli_fetch_assoc($count_result); -echo "

Total invoices in table: {$count['cnt']}

\n"; - -// Show last 5 invoices -echo "

Last 5 Invoices

\n"; -$last_result = mysqli_query($db, "SELECT * FROM {$table_prefix}billing_invoices ORDER BY invoice_id DESC LIMIT 5"); - -if (mysqli_num_rows($last_result) > 0) { - echo "\n"; - echo ""; - $first = true; - while ($row = mysqli_fetch_assoc($last_result)) { - if ($first) { - foreach (array_keys($row) as $col) { - echo ""; - } - echo "\n"; - $first = false; - mysqli_data_seek($last_result, 0); - } - } - - while ($row = mysqli_fetch_assoc($last_result)) { - echo ""; - foreach ((array)$row as $val) { - echo ""; - } - echo "\n"; - } - echo "
{$col}
" . htmlspecialchars($val ?? 'NULL') . "
\n"; -} else { - echo "

No invoices found.

\n"; -} - - billing_maybe_close_db($db); -?> diff --git a/Panel/modules/billing/checkout_free.php b/Panel/modules/billing/checkout_free.php deleted file mode 100644 index 54971cee..00000000 --- a/Panel/modules/billing/checkout_free.php +++ /dev/null @@ -1,261 +0,0 @@ -), - * creates billing_orders rows, and triggers automatic server provisioning. - * - * POST params: coupon_id, coupon_code - */ - -if (session_status() === PHP_SESSION_NONE) { - session_name('opengamepanel_web'); - session_start(); -} - -require_once __DIR__ . '/bootstrap.php'; -require_once __DIR__ . '/includes/login_required.php'; - -function billing_free_money_to_cents(float $amount): int -{ - return (int) round($amount * 100); -} - -$userId = intval($_SESSION['website_user_id'] ?? $_SESSION['user_id'] ?? 0); -if ($userId <= 0) { - header('Location: ' . billing_url('login.php')); - exit; -} - -// Only accept POST -if ($_SERVER['REQUEST_METHOD'] !== 'POST') { - header('Location: ' . billing_url('cart.php')); - exit; -} - -// DB connection -$mysqli = mysqli_connect($db_host, $db_user, $db_pass, $db_name, isset($db_port) ? (int)$db_port : null); -if (!$mysqli) { - die('

Database connection failed. Please return to the shop or contact support.

'); -} -mysqli_set_charset($mysqli, 'utf8mb4'); - -// Fetch unpaid invoices for this user (prepared statement) -$invoices = []; -$stmt = mysqli_prepare($mysqli, "SELECT * FROM {$table_prefix}billing_invoices - WHERE user_id = ? - AND (status = 'due' OR status = '') - AND (payment_status IS NULL OR payment_status NOT IN ('paid','cancelled','refunded')) - ORDER BY invoice_id ASC"); -if ($stmt) { - mysqli_stmt_bind_param($stmt, 'i', $userId); - mysqli_stmt_execute($stmt); - $result = mysqli_stmt_get_result($stmt); - while ($row = mysqli_fetch_assoc($result)) { - $invoices[] = $row; - } - mysqli_stmt_close($stmt); -} - -if (empty($invoices)) { - if ($mysqli instanceof mysqli) { - mysqli_close($mysqli); - } - header('Location: ' . billing_url('cart.php') . '?msg=empty'); - exit; -} - -// Validate coupon from POST / session -$couponId = intval($_POST['coupon_id'] ?? $_SESSION['cart_coupon_id'] ?? 0); -$couponCode = trim($_POST['coupon_code'] ?? $_SESSION['cart_coupon_code'] ?? ''); -$discountPct = 0.0; - -if ($couponCode !== '') { - $safe = mysqli_real_escape_string($mysqli, $couponCode); - $cr = mysqli_query($mysqli, "SELECT * FROM {$table_prefix}billing_coupons - WHERE code = '$safe' AND is_active = 1 LIMIT 1"); - if ($cr && mysqli_num_rows($cr) === 1) { - $coupon = mysqli_fetch_assoc($cr); - $discountPct = (float)($coupon['discount_percent'] ?? 0); - mysqli_free_result($cr); - } -} - -// Calculate total and verify it is $0 after discount -$totalAmountCents = 0; -foreach ($invoices as $inv) { - $lineAmount = (float)($inv['total_due'] ?? $inv['amount'] ?? 0); - $totalAmountCents += billing_free_money_to_cents($lineAmount); -} -$discountAmountCents = (int) round($totalAmountCents * ($discountPct / 100.0)); -$discountAmountCents = min($discountAmountCents, $totalAmountCents); -$finalAmountCents = max(0, $totalAmountCents - $discountAmountCents); - -if ($finalAmountCents !== 0) { - // Coupon no longer covers the full amount — redirect to cart - if ($mysqli instanceof mysqli) { - mysqli_close($mysqli); - } - header('Location: ' . billing_url('cart.php') . '?msg=coupon_insufficient'); - exit; -} - -// Process the free checkout -$now = date('Y-m-d H:i:s'); -$txid = 'free-' . time() . '-' . $userId; - -require_once __DIR__ . '/classes/BillingRepository.php'; -require_once __DIR__ . '/classes/BillingService.php'; - -$repo = new BillingRepository($mysqli, $table_prefix); -$newOrderIds = []; -$duration_meta = static function (array $invoice): array { - return ['invoice_duration' => 'month', 'rate_type' => 'monthly', 'days' => 31]; -}; - -foreach ($invoices as $inv) { - $invoiceId = intval($inv['invoice_id']); - $invoiceBase = round((float)($inv['subtotal'] ?? $inv['total_due'] ?? $inv['amount'] ?? 0), 2); - $orderId = intval($inv['order_id'] ?? 0); - $meta = $duration_meta($inv); - - $repo->updateInvoiceFields($invoiceId, [ - 'order_id' => $orderId, - 'coupon_id' => $couponId, - 'discount_amount' => $invoiceBase, - 'subtotal' => $invoiceBase, - 'amount' => 0.00, - 'total_due' => 0.00, - 'status' => 'paid', - 'billing_status' => 'Active', - 'payment_status' => 'paid', - 'payment_txid' => $txid, - 'payment_method' => 'coupon', - 'paid_date' => $now, - 'invoice_duration' => $meta['invoice_duration'], - 'rate_type' => $meta['rate_type'], - ]); - - $repo->logTransaction([ - 'invoice_id' => $invoiceId, - 'user_id' => $userId, - 'home_id' => 0, - 'payment_method' => 'coupon', - 'transaction_external_id' => $txid, - 'amount' => 0.00, - 'currency' => 'USD', - 'status' => 'completed', - 'raw_response' => ['coupon_id' => $couponId, 'discount_pct' => $discountPct, 'original_amount' => (float)($inv['amount'] ?? 0)], - ]); - - $currentHomeId = 0; - $extendFrom = null; - if ($orderId > 0) { - $order = $repo->getOrder($orderId); - if ($order) { - $currentHomeId = intval($order['home_id'] ?? 0); - $extendFrom = $order['end_date'] ?? null; - } - } - - $baseTs = time(); - if (!empty($extendFrom)) { - $extendTs = strtotime($extendFrom); - if ($extendTs !== false && $extendTs > time()) { - $baseTs = $extendTs; - } - } - $endDate = date('Y-m-d H:i:s', $baseTs + ($meta['days'] * max(1, intval($inv['qty'] ?? 1)) * 86400)); - - if ($orderId > 0) { - $repo->updateOrderFields($orderId, [ - 'status' => 'Active', - 'end_date' => $endDate, - 'payment_txid' => $txid, - 'paid_ts' => $now, - 'price' => 0.00, - 'discount_amount' => $invoiceBase, - 'coupon_id' => $couponId, - ]); - if ($currentHomeId > 0) { - $repo->updateInvoiceFields($invoiceId, ['home_id' => $currentHomeId]); - } - if (!in_array($orderId, $newOrderIds, true)) { - $newOrderIds[] = $orderId; - } - } else { - $newOrderId = $repo->createOrder([ - 'user_id' => intval($inv['user_id']), - 'service_id' => intval($inv['service_id']), - 'home_name' => $inv['home_name'] ?? '', - 'ip' => (string)($inv['ip'] ?? '0'), - 'qty' => intval($inv['qty'] ?? 1), - 'invoice_duration' => $meta['invoice_duration'], - 'max_players' => intval($inv['max_players'] ?? 0), - 'price' => 0.00, - 'discount_amount' => $invoiceBase, - 'remote_control_password' => $inv['remote_control_password'] ?? '', - 'ftp_password' => $inv['ftp_password'] ?? '', - 'status' => 'Active', - 'end_date' => $endDate, - 'payment_txid' => $txid, - 'paid_ts' => $now, - 'coupon_id' => $couponId, - ]); - - if ($newOrderId > 0) { - $repo->updateInvoiceOrderId($invoiceId, $newOrderId); - $repo->updateInvoiceFields($invoiceId, ['order_id' => $newOrderId]); - if (!in_array($newOrderId, $newOrderIds, true)) { - $newOrderIds[] = $newOrderId; - } - } - } -} - -if ($couponId > 0 && !empty($invoices)) { - mysqli_query($mysqli, "UPDATE {$table_prefix}billing_coupons - SET current_uses = current_uses + 1 - WHERE coupon_id = " . intval($couponId)); -} - -// Clear coupon from session -unset($_SESSION['cart_coupon_code'], $_SESSION['cart_coupon_id']); - -// Attempt automatic provisioning via panel bridge -$autoProvision = ['provisioned_count' => 0, 'failed_count' => 0, 'details' => [], 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log']; -if (!empty($newOrderIds)) { - require_once __DIR__ . '/includes/panel_bridge.php'; - $panelCtx = billing_panel_bootstrap(); - if ($panelCtx && isset($panelCtx['db'])) { - $GLOBALS['db'] = $panelCtx['db']; - $GLOBALS['settings'] = $panelCtx['settings']; - require_once __DIR__ . '/create_servers.php'; - $autoProvision = billing_invoke_provision(['order_ids' => $newOrderIds, 'user_id' => $userId, 'is_admin' => true]); - } else { - $autoProvision = [ - 'provisioned_count' => 0, - 'failed_count' => count($newOrderIds), - 'details' => [], - 'trace_log_path' => 'modules/billing/logs/provisioning_trace.log', - 'trace_error' => 'Panel bootstrap failed before billing provisioning could start.', - ]; - } - // If panel bootstrap fails the order is Active and admins can provision via the orders panel. -} -if (function_exists('billing_store_provision_session_result')) { - billing_store_provision_session_result($txid, [ - 'source' => 'checkout_free.php', - 'txid' => $txid, - 'order_ids' => $newOrderIds, - 'result' => $autoProvision, - ]); -} - -if ($mysqli instanceof mysqli) { - mysqli_close($mysqli); -} - -header('Location: ' . billing_url('payment_success.php') . '?order_id=' . urlencode($txid) . '&source=free'); -exit; diff --git a/Panel/modules/billing/classes/BillingRepository.php b/Panel/modules/billing/classes/BillingRepository.php deleted file mode 100644 index be19be85..00000000 --- a/Panel/modules/billing/classes/BillingRepository.php +++ /dev/null @@ -1,674 +0,0 @@ -db = $db; - $this->prefix = $prefix; - } - - // --------------------------------------------------------------- - // Invoice helpers - // --------------------------------------------------------------- - - /** Find a single 'unpaid' invoice by ID, owned by $userId. */ - public function getUnpaidInvoice(int $invoiceId, int $userId): ?array - { - $stmt = $this->db->prepare( - "SELECT * FROM `{$this->prefix}billing_invoices` - WHERE invoice_id = ? AND user_id = ? AND payment_status IN ('unpaid','due') LIMIT 1" - ); - if (!$stmt) return null; - $stmt->bind_param('ii', $invoiceId, $userId); - $stmt->execute(); - $row = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - return $row ?: null; - } - - /** Get all unpaid invoices for a user. */ - public function getUnpaidInvoicesForUser(int $userId): array - { - $stmt = $this->db->prepare( - "SELECT * FROM `{$this->prefix}billing_invoices` - WHERE user_id = ? AND payment_status IN ('unpaid','due') - ORDER BY invoice_id ASC" - ); - if (!$stmt) return []; - $stmt->bind_param('i', $userId); - $stmt->execute(); - return $stmt->get_result()->fetch_all(MYSQLI_ASSOC); - } - - /** Get invoice rows for a specific user and invoice id list. */ - public function getInvoicesForUserByIds(int $userId, array $invoiceIds, bool $onlyUnpaid = true): array - { - $invoiceIds = array_values(array_unique(array_filter(array_map('intval', $invoiceIds), static fn($id) => $id > 0))); - if (empty($invoiceIds)) { - return []; - } - - $placeholders = implode(',', array_fill(0, count($invoiceIds), '?')); - $types = str_repeat('i', count($invoiceIds) + 1); - $params = array_merge([$userId], $invoiceIds); - $where = $onlyUnpaid ? " AND payment_status IN ('unpaid','due')" : ''; - $sql = "SELECT * FROM `{$this->prefix}billing_invoices` - WHERE user_id = ? AND invoice_id IN ({$placeholders}){$where} - ORDER BY invoice_id ASC"; - $stmt = $this->db->prepare($sql); - if (!$stmt) { - return []; - } - $stmt->bind_param($types, ...$params); - $stmt->execute(); - $rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC); - $stmt->close(); - return $rows; - } - - /** Mark an invoice as paid. Also sets status='paid' so it disappears from cart queries. */ - public function markInvoicePaid(int $invoiceId, string $txid, string $method, string $paidAt): bool - { - $stmt = $this->db->prepare( - "UPDATE `{$this->prefix}billing_invoices` - SET payment_status='paid', status='paid', payment_txid=?, payment_method=?, paid_date=? - WHERE invoice_id = ? LIMIT 1" - ); - if (!$stmt) return false; - $stmt->bind_param('sssi', $txid, $method, $paidAt, $invoiceId); - $ok = $stmt->execute(); - $stmt->close(); - return $ok; - } - - /** - * Create a billing_orders row from invoice/payment data. - * Returns new order_id (0 on failure). - * - * @param array $data Keys: user_id, service_id, home_name, ip, qty, invoice_duration, - * max_players, price, remote_control_password, ftp_password, - * status, end_date, payment_txid, paid_ts, coupon_id - */ - public function createOrder(array $data): int - { - $now = date('Y-m-d H:i:s'); - $status = (string)($data['status'] ?? 'Active'); - $endDate = $data['end_date'] ?? null; - $txid = (string)($data['payment_txid'] ?? ''); - $paidTs = (string)($data['paid_ts'] ?? $now); - $couponId = intval($data['coupon_id'] ?? 0); - $discount = (float)($data['discount_amount'] ?? 0); - $ip = (string)($data['ip'] ?? '0'); - $qty = intval($data['qty'] ?? 1); - $maxPl = intval($data['max_players'] ?? 0); - $price = (float)($data['price'] ?? 0); - $userId = intval($data['user_id']); - $svcId = intval($data['service_id']); - $homeName = (string)($data['home_name'] ?? ''); - $invDur = (string)($data['invoice_duration'] ?? 'month'); - $rcp = (string)($data['remote_control_password'] ?? ''); - $ftp = (string)($data['ftp_password'] ?? ''); - $fields = [ - 'user_id' => $userId, - 'service_id' => $svcId, - 'home_name' => $homeName, - 'ip' => $ip, - 'qty' => $qty, - 'invoice_duration' => $invDur, - 'max_players' => $maxPl, - 'price' => $price, - 'discount_amount' => $discount, - 'remote_control_password' => $rcp, - 'ftp_password' => $ftp, - 'home_id' => '0', - 'status' => $status, - 'order_date' => $now, - 'end_date' => $endDate, - 'payment_txid' => $txid, - 'paid_ts' => $paidTs, - 'coupon_id' => $couponId, - ]; - if ($this->hasColumn('billing_orders', 'paypal_data')) { - $fields['paypal_data'] = isset($data['paypal_data']) - ? (is_array($data['paypal_data']) ? json_encode($data['paypal_data']) : (string)$data['paypal_data']) - : null; - } - return $this->insertAssoc('billing_orders', $fields); - } - - /** - * Link a billing_invoice row to its corresponding billing_orders row. - * Called after createOrder() so the capture endpoint can be idempotent. - */ - public function updateInvoiceOrderId(int $invoiceId, int $orderId): bool - { - $stmt = $this->db->prepare( - "UPDATE `{$this->prefix}billing_invoices` SET order_id = ? WHERE invoice_id = ? LIMIT 1" - ); - if (!$stmt) return false; - $stmt->bind_param('ii', $orderId, $invoiceId); - $ok = $stmt->execute(); - $stmt->close(); - return $ok; - } - - /** Create a new invoice record. Returns new invoice_id or 0 on failure. */ - public function createInvoice(array $data): int - { - $fields = [ - 'user_id', 'service_id', 'home_id', 'home_name', - 'customer_name', 'customer_email', - 'rate_type', 'rate_per_player', 'players', - 'period_start', 'period_end', - 'subtotal', 'total_due', - 'currency', 'payment_status', 'payment_method', 'description', - ]; - $cols = implode(',', array_map(fn($f) => "`$f`", $fields)); - $places = implode(',', array_fill(0, count($fields), '?')); - $types = 'iiissssssiissssss'; - - $stmt = $this->db->prepare( - "INSERT INTO `{$this->prefix}billing_invoices` ({$cols}) VALUES ({$places})" - ); - if (!$stmt) return 0; - - $vals = []; - foreach ($fields as $f) { - $vals[] = $data[$f] ?? null; - } - $stmt->bind_param($types, ...$vals); - if (!$stmt->execute()) { $stmt->close(); return 0; } - $id = (int)$stmt->insert_id; - $stmt->close(); - return $id; - } - - // --------------------------------------------------------------- - // Safe table-creation helpers (idempotent, check INFORMATION_SCHEMA first) - // --------------------------------------------------------------- - - /** - * Ensure billing_transactions table exists. - * Safe to call on every request; uses INFORMATION_SCHEMA to skip if already present. - */ - public function ensureBillingTransactionsTable(): bool - { - $res = $this->db->query( - "SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = '{$this->prefix}billing_transactions'" - ); - if ($res && (int)$res->fetch_assoc()['cnt'] > 0) { - return true; - } - return (bool)$this->db->query( - "CREATE TABLE IF NOT EXISTS `{$this->prefix}billing_transactions` ( - `transaction_id` INT(11) NOT NULL AUTO_INCREMENT, - `invoice_id` INT(11) NOT NULL DEFAULT 0, - `user_id` INT(11) NOT NULL DEFAULT 0, - `home_id` INT(11) NOT NULL DEFAULT 0, - `payment_method` VARCHAR(50) NOT NULL DEFAULT 'paypal', - `transaction_external_id` VARCHAR(255) NOT NULL DEFAULT '', - `amount` DECIMAL(15,2) NOT NULL DEFAULT 0.00, - `currency` VARCHAR(3) NOT NULL DEFAULT 'USD', - `status` ENUM('pending','completed','failed','refunded') NOT NULL DEFAULT 'pending', - `raw_response` MEDIUMTEXT NULL, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`transaction_id`), - KEY `invoice_id` (`invoice_id`), - KEY `user_id` (`user_id`), - KEY `home_id` (`home_id`), - KEY `status` (`status`), - KEY `payment_method` (`payment_method`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - } - - /** - * Ensure billing_paypal_errors table exists. - * Safe to call on every request; uses INFORMATION_SCHEMA to skip if already present. - */ - public function ensureBillingPaypalErrorsTable(): bool - { - $res = $this->db->query( - "SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = '{$this->prefix}billing_paypal_errors'" - ); - if ($res && (int)$res->fetch_assoc()['cnt'] > 0) { - return true; - } - return (bool)$this->db->query( - "CREATE TABLE IF NOT EXISTS `{$this->prefix}billing_paypal_errors` ( - `id` INT NOT NULL AUTO_INCREMENT, - `context` VARCHAR(64) NOT NULL DEFAULT '', - `error_code` VARCHAR(128) NOT NULL DEFAULT '', - `message` TEXT NULL, - `paypal_debug_id` VARCHAR(128) NULL, - `order_id` VARCHAR(128) NULL, - `capture_id` VARCHAR(128) NULL, - `billing_order_id` INT NULL, - `user_id` INT NULL, - `raw_json` LONGTEXT NULL, - `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - KEY `idx_context` (`context`), - KEY `idx_created_at` (`created_at`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" - ); - } - - // --------------------------------------------------------------- - // Transaction (payment log) helpers - // --------------------------------------------------------------- - - /** Insert a row into billing_transactions. Returns new transaction_id. */ - public function logTransaction(array $data): int - { - $this->ensureBillingTransactionsTable(); - $invoiceId = intval($data['invoice_id'] ?? 0); - $extId = (string)($data['transaction_external_id'] ?? ''); - if ($invoiceId > 0 && $extId !== '') { - $existing = $this->db->prepare( - "SELECT transaction_id FROM `{$this->prefix}billing_transactions` - WHERE invoice_id = ? AND transaction_external_id = ? - LIMIT 1" - ); - if ($existing) { - $existing->bind_param('is', $invoiceId, $extId); - $existing->execute(); - $row = $existing->get_result()->fetch_assoc(); - $existing->close(); - if (!empty($row['transaction_id'])) { - return (int)$row['transaction_id']; - } - } - } - $stmt = $this->db->prepare( - "INSERT INTO `{$this->prefix}billing_transactions` - (invoice_id, user_id, home_id, payment_method, transaction_external_id, - amount, currency, status, raw_response) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" - ); - if (!$stmt) return 0; - $rawJson = is_array($data['raw_response']) ? json_encode($data['raw_response']) : (string)($data['raw_response'] ?? ''); - $userId = intval($data['user_id'] ?? 0); - $homeId = intval($data['home_id'] ?? 0); - $method = (string)($data['payment_method'] ?? 'paypal'); - $amount = (float)($data['amount'] ?? 0); - $currency = (string)($data['currency'] ?? 'USD'); - $status = (string)($data['status'] ?? 'completed'); - $stmt->bind_param( - 'iiissdsss', - $invoiceId, $userId, $homeId, $method, $extId, $amount, $currency, $status, $rawJson - ); - if (!$stmt->execute()) { $stmt->close(); return 0; } - $id = (int)$stmt->insert_id; - $stmt->close(); - return $id; - } - - /** Get all transactions, optionally filtered. Creates the table if missing. */ - public function getTransactions(array $filter = [], int $limit = 100, int $offset = 0): array - { - if (!$this->ensureBillingTransactionsTable()) { - return []; - } - $where = '1=1'; - $params = []; - $types = ''; - if (!empty($filter['user_id'])) { - $where .= ' AND t.user_id = ?'; - $params[] = intval($filter['user_id']); - $types .= 'i'; - } - if (!empty($filter['home_id'])) { - $where .= ' AND t.home_id = ?'; - $params[] = intval($filter['home_id']); - $types .= 'i'; - } - if (!empty($filter['payment_method'])) { - $where .= ' AND t.payment_method = ?'; - $params[] = $filter['payment_method']; - $types .= 's'; - } - $sql = "SELECT t.*, u.users_login, u.users_email - FROM `{$this->prefix}billing_transactions` t - LEFT JOIN `{$this->prefix}users` u ON u.user_id = t.user_id - WHERE {$where} - ORDER BY t.transaction_id DESC - LIMIT ? OFFSET ?"; - $params[] = $limit; - $params[] = $offset; - $types .= 'ii'; - $stmt = $this->db->prepare($sql); - if (!$stmt) return []; - $stmt->bind_param($types, ...$params); - $stmt->execute(); - return $stmt->get_result()->fetch_all(MYSQLI_ASSOC); - } - - // --------------------------------------------------------------- - // PayPal error log helpers - // --------------------------------------------------------------- - - /** - * Insert a row into billing_paypal_errors. Never logs client secrets. - * Returns new error log id (0 on failure). - */ - public function logPaypalError(array $data): int - { - $this->ensureBillingPaypalErrorsTable(); - $stmt = $this->db->prepare( - "INSERT INTO `{$this->prefix}billing_paypal_errors` - (context, error_code, message, paypal_debug_id, order_id, capture_id, - billing_order_id, user_id, raw_json) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" - ); - if (!$stmt) return 0; - $context = substr((string)($data['context'] ?? ''), 0, 64); - $errorCode = substr((string)($data['error_code'] ?? ''), 0, 128); - $message = (string)($data['message'] ?? ''); - $debugId = isset($data['paypal_debug_id']) ? substr((string)$data['paypal_debug_id'], 0, 128) : null; - $orderId = isset($data['order_id']) ? substr((string)$data['order_id'], 0, 128) : null; - $captureId = isset($data['capture_id']) ? substr((string)$data['capture_id'], 0, 128) : null; - $billingOrderId = isset($data['billing_order_id']) ? intval($data['billing_order_id']) : null; - $userId = isset($data['user_id']) ? intval($data['user_id']) : null; - $rawJson = isset($data['raw_json']) - ? (is_array($data['raw_json']) ? json_encode($data['raw_json']) : (string)$data['raw_json']) - : null; - // Truncate large payloads to avoid LONGTEXT bloat - if ($rawJson !== null && strlen($rawJson) > 65536) { - $rawJson = substr($rawJson, 0, 65536) . '…[truncated]'; - } - $stmt->bind_param( - 'ssssssiis', - $context, $errorCode, $message, $debugId, $orderId, $captureId, - $billingOrderId, $userId, $rawJson - ); - if (!$stmt->execute()) { $stmt->close(); return 0; } - $id = (int)$stmt->insert_id; - $stmt->close(); - return $id; - } - - /** - * Return the $limit most recent rows from billing_paypal_errors. - * Returns empty array if the table does not exist. - */ - public function getRecentPaypalErrors(int $limit = 10): array - { - if (!$this->ensureBillingPaypalErrorsTable()) { - return []; - } - $stmt = $this->db->prepare( - "SELECT id, created_at, context, error_code, message, - paypal_debug_id, order_id, capture_id, billing_order_id, user_id - FROM `{$this->prefix}billing_paypal_errors` - ORDER BY id DESC - LIMIT ?" - ); - if (!$stmt) return []; - $stmt->bind_param('i', $limit); - $stmt->execute(); - return $stmt->get_result()->fetch_all(MYSQLI_ASSOC); - } - - // --------------------------------------------------------------- - // Server home (billing state) helpers - // --------------------------------------------------------------- - - /** Get server home billing info by home_id. */ - public function getServerHomeBilling(int $homeId): ?array - { - $stmt = $this->db->prepare( - "SELECT home_id, home_name, user_id_main, billing_status, billing_expires_at, - billing_price, billing_rate_type, billing_players, billing_enabled, - next_invoice_date, server_expiration_date, billing_invoice_sent_at - FROM `{$this->prefix}server_homes` - WHERE home_id = ? LIMIT 1" - ); - if (!$stmt) return null; - $stmt->bind_param('i', $homeId); - $stmt->execute(); - $row = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - return $row ?: null; - } - - /** Update billing state fields on server_homes. */ - public function updateServerHomeBilling(int $homeId, array $data): bool - { - $allowed = [ - 'billing_status', 'billing_expires_at', 'billing_price', - 'billing_rate_type', 'billing_players', 'billing_enabled', - 'next_invoice_date', 'server_expiration_date', 'billing_invoice_sent_at', - ]; - $set = []; - $params = []; - $types = ''; - foreach ($allowed as $col) { - if (array_key_exists($col, $data)) { - $set[] = "`{$col}` = ?"; - $params[] = $data[$col]; - $val = $data[$col]; - if ($val === null) { - $types .= 's'; // NULL binds safely as string in mysqli - } elseif (is_int($val)) { - $types .= 'i'; - } elseif (is_float($val)) { - $types .= 'd'; - } else { - $types .= 's'; - } - } - } - if (empty($set)) return false; - $params[] = $homeId; - $types .= 'i'; - $stmt = $this->db->prepare( - "UPDATE `{$this->prefix}server_homes` SET " . implode(', ', $set) . " WHERE home_id = ? LIMIT 1" - ); - if (!$stmt) return false; - $stmt->bind_param($types, ...$params); - $ok = $stmt->execute(); - $stmt->close(); - return $ok; - } - - // --------------------------------------------------------------- - // Service helpers - // --------------------------------------------------------------- - - /** Get a billing service by ID. Returns null if not found / disabled. */ - public function getService(int $serviceId, bool $mustBeEnabled = true): ?array - { - $extra = $mustBeEnabled ? ' AND enabled = 1' : ''; - $stmt = $this->db->prepare( - "SELECT * FROM `{$this->prefix}billing_services` WHERE service_id = ?{$extra} LIMIT 1" - ); - if (!$stmt) return null; - $stmt->bind_param('i', $serviceId); - $stmt->execute(); - $row = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - return $row ?: null; - } - - /** Get enabled services (for storefront listing). */ - public function getEnabledServices(): array - { - $res = $this->db->query( - "SELECT * FROM `{$this->prefix}billing_services` WHERE enabled = 1 ORDER BY service_name" - ); - return $res ? $res->fetch_all(MYSQLI_ASSOC) : []; - } - - // --------------------------------------------------------------- - // Legacy billing_orders helpers (kept for backward compat during migration) - // --------------------------------------------------------------- - - /** Get an active order by order_id. */ - public function getOrder(int $orderId): ?array - { - $stmt = $this->db->prepare( - "SELECT * FROM `{$this->prefix}billing_orders` WHERE order_id = ? LIMIT 1" - ); - if (!$stmt) return null; - $stmt->bind_param('i', $orderId); - $stmt->execute(); - $row = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - return $row ?: null; - } - - /** Extend an existing order's end_date. */ - public function extendOrder(int $orderId, string $newEndDate, string $txid, string $now): bool - { - $stmt = $this->db->prepare( - "UPDATE `{$this->prefix}billing_orders` - SET status='Active', end_date=?, payment_txid=?, paid_ts=? - WHERE order_id=? LIMIT 1" - ); - if (!$stmt) return false; - $stmt->bind_param('sssi', $newEndDate, $txid, $now, $orderId); - $ok = $stmt->execute(); - $stmt->close(); - return $ok; - } - - public function getCouponByCode(string $couponCode): ?array - { - $stmt = $this->db->prepare( - "SELECT * FROM `{$this->prefix}billing_coupons` - WHERE code = ? AND is_active = 1 - LIMIT 1" - ); - if (!$stmt) { - return null; - } - $stmt->bind_param('s', $couponCode); - $stmt->execute(); - $row = $stmt->get_result()->fetch_assoc(); - $stmt->close(); - return $row ?: null; - } - - public function updateInvoiceFields(int $invoiceId, array $data): bool - { - return $this->updateAssoc('billing_invoices', 'invoice_id', $invoiceId, $data); - } - - public function updateOrderFields(int $orderId, array $data): bool - { - return $this->updateAssoc('billing_orders', 'order_id', $orderId, $data); - } - - private function hasColumn(string $table, string $column): bool - { - $cacheKey = $table . '.' . $column; - if (array_key_exists($cacheKey, $this->columnCache)) { - return $this->columnCache[$cacheKey]; - } - - $tableName = $this->db->real_escape_string($this->prefix . $table); - $columnName = $this->db->real_escape_string($column); - $res = $this->db->query( - "SELECT COUNT(*) AS cnt - FROM INFORMATION_SCHEMA.COLUMNS - WHERE TABLE_SCHEMA = DATABASE() - AND TABLE_NAME = '{$tableName}' - AND COLUMN_NAME = '{$columnName}'" - ); - $exists = $res ? ((int)($res->fetch_assoc()['cnt'] ?? 0) > 0) : false; - $this->columnCache[$cacheKey] = $exists; - return $exists; - } - - private function insertAssoc(string $table, array $data): int - { - if (empty($data)) { - return 0; - } - $columns = array_keys($data); - $placeholders = implode(',', array_fill(0, count($columns), '?')); - $sql = sprintf( - "INSERT INTO `%s%s` (%s) VALUES (%s)", - $this->prefix, - $table, - implode(',', array_map(static fn($field) => "`{$field}`", $columns)), - $placeholders - ); - $stmt = $this->db->prepare($sql); - if (!$stmt) { - return 0; - } - [$types, $values] = $this->prepareBindValues($data); - $stmt->bind_param($types, ...$values); - if (!$stmt->execute()) { - $stmt->close(); - return 0; - } - $id = (int)$stmt->insert_id; - $stmt->close(); - return $id; - } - - private function updateAssoc(string $table, string $idColumn, int $idValue, array $data): bool - { - $data = array_filter($data, static fn($value) => $value !== null); - if (empty($data)) { - return true; - } - $set = []; - foreach (array_keys($data) as $field) { - $set[] = "`{$field}` = ?"; - } - $sql = sprintf( - "UPDATE `%s%s` SET %s WHERE `%s` = ? LIMIT 1", - $this->prefix, - $table, - implode(', ', $set), - $idColumn - ); - $stmt = $this->db->prepare($sql); - if (!$stmt) { - return false; - } - [$types, $values] = $this->prepareBindValues($data); - $types .= 'i'; - $values[] = $idValue; - $stmt->bind_param($types, ...$values); - $ok = $stmt->execute(); - $stmt->close(); - return $ok; - } - - private function prepareBindValues(array $data): array - { - $types = ''; - $values = []; - foreach ($data as $value) { - if (is_int($value)) { - $types .= 'i'; - $values[] = $value; - } elseif (is_float($value)) { - $types .= 'd'; - $values[] = $value; - } else { - $types .= 's'; - $values[] = ($value === null) ? null : (string)$value; - } - } - return [$types, $values]; - } -} diff --git a/Panel/modules/billing/classes/BillingService.php b/Panel/modules/billing/classes/BillingService.php deleted file mode 100644 index bca672ad..00000000 --- a/Panel/modules/billing/classes/BillingService.php +++ /dev/null @@ -1,179 +0,0 @@ -repo = $repo; - } - - /** - * Calculate pricing for a new order. - * - * @param array $service Row from gsp_billing_services - * @param string $rateType 'daily' | 'monthly' | 'yearly' - * @param int $players Number of player slots - * @param int $qty Duration quantity (e.g. 2 = 2 months) - * @return array { rate_per_player, subtotal, total_due, period_days } - */ - public function calculatePrice(array $service, string $rateType, int $players, int $qty = 1): array - { - $qty = max(1, $qty); - $players = max(1, $players); - $rateType = 'monthly'; - $basePrice = (float)($service['price_monthly'] ?? 0); - $periodDays = $qty * 31; - - // price_monthly etc is the per-player per-period rate - $ratePerPlayer = $basePrice; - $subtotal = round($ratePerPlayer * $players * $qty, 2); - $totalDue = $subtotal; - - return [ - 'rate_type' => $rateType, - 'rate_per_player' => $ratePerPlayer, - 'players' => $players, - 'qty' => $qty, - 'subtotal' => $subtotal, - 'total_due' => $totalDue, - 'period_days' => $periodDays, - ]; - } - - /** - * Create a billing invoice row. - * - * @param array $pricing Result from calculatePrice() - * @param array $context { user_id, service_id, home_id, home_name, customer_name, customer_email, description } - * @return int New invoice_id (0 on failure) - */ - public function createInvoice(array $pricing, array $context): int - { - $now = date('Y-m-d H:i:s'); - $periodStart = $now; - $periodEnd = date('Y-m-d H:i:s', strtotime('+' . $pricing['period_days'] . ' days')); - - return $this->repo->createInvoice([ - 'user_id' => intval($context['user_id'] ?? 0), - 'service_id' => intval($context['service_id'] ?? 0), - 'home_id' => intval($context['home_id'] ?? 0), - 'home_name' => $context['home_name'] ?? '', - 'customer_name' => $context['customer_name'] ?? '', - 'customer_email' => $context['customer_email'] ?? '', - 'rate_type' => $pricing['rate_type'], - 'rate_per_player' => $pricing['rate_per_player'], - 'players' => $pricing['players'], - 'period_start' => $periodStart, - 'period_end' => $periodEnd, - 'subtotal' => $pricing['subtotal'], - 'total_due' => $pricing['total_due'], - 'currency' => $context['currency'] ?? 'USD', - 'payment_status' => 'unpaid', - 'payment_method' => '', - 'description' => $context['description'] ?? '', - ]); - } - - /** - * Process a successful payment result from a gateway. - * - * 1. Log the transaction - * 2. Mark invoice paid - * 3. Update server home billing state (extend or reset expiration) - * - * @param array $captureResult Result from PaymentGatewayInterface::handleCallback() - * @param int $invoiceId - * @param int $userId - * @param int $homeId - * @param array $invoiceRow The invoice row (from DB) — needed for period/pricing - * @return array { success: bool, transaction_id: string, error?: string } - */ - public function processPaymentSuccess( - array $captureResult, - int $invoiceId, - int $userId, - int $homeId, - array $invoiceRow - ): array { - $txid = $captureResult['transaction_id'] ?? null; - $method = $captureResult['payment_method'] ?? 'paypal'; - $amount = (float)($captureResult['amount'] ?? $invoiceRow['total_due'] ?? 0); - $currency = $captureResult['currency'] ?? $invoiceRow['currency'] ?? 'USD'; - $now = date('Y-m-d H:i:s'); - - // 1. Log transaction - $this->repo->logTransaction([ - 'invoice_id' => $invoiceId, - 'user_id' => $userId, - 'home_id' => $homeId, - 'payment_method' => $method, - 'transaction_external_id' => $txid ?? '', - 'amount' => $amount, - 'currency' => $currency, - 'status' => 'completed', - 'raw_response' => $captureResult['raw_response'] ?? [], - ]); - - // 2. Mark invoice paid - if ($invoiceId > 0) { - $this->repo->markInvoicePaid($invoiceId, $txid ?? '', $method, $now); - } - - // 3. Update server home billing state - if ($homeId > 0) { - $this->extendServerBilling($homeId, $invoiceRow, $now); - } - - return ['success' => true, 'transaction_id' => $txid]; - } - - /** - * Extend or reset a server's billing expiration based on the invoice period. - */ - public function extendServerBilling(int $homeId, array $invoiceRow, string $now): void - { - $home = $this->repo->getServerHomeBilling($homeId); - $periodEnd = $invoiceRow['period_end'] ?? null; - - if (!$periodEnd) { - $periodEnd = date('Y-m-d H:i:s', strtotime('+31 days')); - } - - // If current expiry is in the future, extend from it; otherwise reset from period_end - $currentExpiry = $home['billing_expires_at'] ?? null; - if ($currentExpiry && strtotime($currentExpiry) > time()) { - // Calculate the period length from the invoice; fall back to rate_type if dates are missing - $periodStart = $invoiceRow['period_start'] ?? null; - $periodEndVal = $invoiceRow['period_end'] ?? null; - if ($periodStart && $periodEndVal) { - $currentPeriodSecs = strtotime($periodEndVal) - strtotime($periodStart); - } else { - $currentPeriodSecs = 31 * 86400; - } - $newExpiry = date('Y-m-d H:i:s', strtotime($currentExpiry) + max(86400, $currentPeriodSecs)); - } else { - $newExpiry = $periodEnd; - } - - $this->repo->updateServerHomeBilling($homeId, [ - 'billing_status' => 'active', - 'billing_expires_at' => $newExpiry, - 'next_invoice_date' => $newExpiry, - 'server_expiration_date' => null, - 'billing_invoice_sent_at' => null, - ]); - } -} diff --git a/Panel/modules/billing/classes/GatewayFactory.php b/Panel/modules/billing/classes/GatewayFactory.php deleted file mode 100644 index d9518ca2..00000000 --- a/Panel/modules/billing/classes/GatewayFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - true, 'provider_order_id' => 'MANUAL-' . uniqid(), 'raw_response' => []]; - } - - public function handleCallback(array $params): array - { - $txid = $params['admin_txid'] ?? ('MANUAL-' . uniqid()); - return [ - 'success' => true, - 'transaction_id' => $txid, - 'amount' => (float)($params['amount'] ?? 0), - 'currency' => $params['currency'] ?? 'USD', - 'status' => 'completed', - 'raw_response' => $params, - ]; - } - - public function verifyPayment(array $payload): bool { return true; } - - public function getTransactionId(array $captureResult): ?string - { - return $captureResult['transaction_id'] ?? null; - } -} diff --git a/Panel/modules/billing/classes/PayPalGateway.php b/Panel/modules/billing/classes/PayPalGateway.php deleted file mode 100644 index fd7b5b26..00000000 --- a/Panel/modules/billing/classes/PayPalGateway.php +++ /dev/null @@ -1,194 +0,0 @@ -clientId = $clientId; - $this->clientSecret = $clientSecret; - $this->sandbox = $sandbox; - $this->apiBase = $sandbox - ? 'https://api-m.sandbox.paypal.com' - : 'https://api-m.paypal.com'; - } - - /** - * Build a PayPalGateway instance from global config variables. - * Prefers the new gsp_paypal_* helper functions; falls back to legacy globals. - */ - public static function fromConfig(): self - { - if (function_exists('gsp_paypal_get_client_id')) { - $clientId = gsp_paypal_get_client_id(); - $clientSecret = gsp_paypal_get_client_secret(); - $sandbox = gsp_paypal_is_sandbox(); - } else { - $clientId = $GLOBALS['paypal_client_id'] ?? ''; - $clientSecret = $GLOBALS['paypal_client_secret'] ?? ''; - $sandbox = (bool)($GLOBALS['paypal_sandbox'] ?? true); - } - return new self($clientId, $clientSecret, $sandbox); - } - - public function getName(): string { return 'paypal'; } - - /** Exchange client credentials for a Bearer token. Returns token or null. */ - private function getAccessToken(): ?string - { - $ch = curl_init("{$this->apiBase}/v1/oauth2/token"); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => 'grant_type=client_credentials', - CURLOPT_HTTPHEADER => ['Accept: application/json'], - CURLOPT_USERPWD => "{$this->clientId}:{$this->clientSecret}", - ]); - $body = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - if ($code !== 200 || !$body) return null; - $data = json_decode($body, true); - return $data['access_token'] ?? null; - } - - public function createPayment(array $params): array - { - $token = $this->getAccessToken(); - if (!$token) { - return ['success' => false, 'error' => 'paypal_oauth_failed']; - } - - $amount = number_format((float)($params['amount'] ?? 0), 2, '.', ''); - $currency = $params['currency'] ?? 'USD'; - $invoiceId = $params['invoice_id'] ?? null; - $description = $params['description'] ?? 'Game Server Order'; - $returnUrl = $params['return_url'] ?? ''; - $cancelUrl = $params['cancel_url'] ?? ''; - $items = $params['items'] ?? null; - - $purchaseUnit = [ - 'amount' => ['currency_code' => $currency, 'value' => $amount], - 'description' => $description, - 'custom_id' => (string)($params['custom_id'] ?? $invoiceId ?? ''), - ]; - if ($invoiceId) { - $purchaseUnit['invoice_id'] = (string)$invoiceId; - } - if ($items) { - $purchaseUnit['items'] = $items; - $purchaseUnit['amount']['breakdown'] = [ - 'item_total' => ['currency_code' => $currency, 'value' => $amount], - ]; - } - - $body = [ - 'intent' => 'CAPTURE', - 'purchase_units' => [$purchaseUnit], - 'application_context' => [ - 'return_url' => $returnUrl, - 'cancel_url' => $cancelUrl, - 'user_action' => 'PAY_NOW', - ], - ]; - - $ch = curl_init("{$this->apiBase}/v2/checkout/orders"); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => json_encode($body), - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/json', - "Authorization: Bearer {$token}", - ], - ]); - $res = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($code !== 201 || !$res) { - return ['success' => false, 'error' => 'paypal_create_order_failed', 'http_code' => $code]; - } - $data = json_decode($res, true); - if (json_last_error() !== JSON_ERROR_NONE) { - return ['success' => false, 'error' => 'paypal_invalid_response']; - } - return [ - 'success' => true, - 'provider_order_id' => $data['id'] ?? '', - 'raw_response' => $data, - ]; - } - - public function handleCallback(array $params): array - { - $providerOrderId = $params['order_id'] ?? null; - if (!$providerOrderId) { - return ['success' => false, 'error' => 'missing_order_id']; - } - - $token = $this->getAccessToken(); - if (!$token) { - return ['success' => false, 'error' => 'paypal_oauth_failed']; - } - - $ch = curl_init("{$this->apiBase}/v2/checkout/orders/{$providerOrderId}/capture"); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_HTTPHEADER => [ - 'Content-Type: application/json', - "Authorization: Bearer {$token}", - ], - ]); - $res = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if (($code !== 200 && $code !== 201) || !$res) { - return ['success' => false, 'error' => 'paypal_capture_failed', 'http_code' => $code]; - } - $data = json_decode($res, true); - if (json_last_error() !== JSON_ERROR_NONE) { - return ['success' => false, 'error' => 'paypal_invalid_capture_response']; - } - - $status = $data['status'] ?? ''; - if ($status !== 'COMPLETED') { - return ['success' => false, 'error' => 'payment_not_completed', 'status' => $status]; - } - - $capture = $data['purchase_units'][0]['payments']['captures'][0] ?? []; - $txid = $capture['id'] ?? null; - $amount = (float)($capture['amount']['value'] ?? 0); - $currency = $capture['amount']['currency_code'] ?? 'USD'; - $customId = $data['purchase_units'][0]['custom_id'] ?? null; - - return [ - 'success' => true, - 'transaction_id' => $txid, - 'amount' => $amount, - 'currency' => $currency, - 'status' => 'completed', - 'custom_id' => $customId, - 'raw_response' => $data, - ]; - } - - public function verifyPayment(array $payload): bool - { - // For REST API flow (JS SDK capture), verification is done by the capture response itself. - // Webhook signature verification would be implemented here for webhook events. - return true; - } - - public function getTransactionId(array $captureResult): ?string - { - return $captureResult['transaction_id'] ?? null; - } -} diff --git a/Panel/modules/billing/classes/PaymentGatewayInterface.php b/Panel/modules/billing/classes/PaymentGatewayInterface.php deleted file mode 100644 index 2a0a424c..00000000 --- a/Panel/modules/billing/classes/PaymentGatewayInterface.php +++ /dev/null @@ -1,40 +0,0 @@ - false, 'error' => 'stripe_not_implemented']; - } - - public function handleCallback(array $params): array - { - return ['success' => false, 'error' => 'stripe_not_implemented']; - } - - public function verifyPayment(array $payload): bool { return false; } - - public function getTransactionId(array $captureResult): ?string { return null; } -} diff --git a/Panel/modules/billing/cleanupDB.sh b/Panel/modules/billing/cleanupDB.sh deleted file mode 100644 index dc2151dd..00000000 --- a/Panel/modules/billing/cleanupDB.sh +++ /dev/null @@ -1,3 +0,0 @@ -$test_id = 1362; - $db->query( "DROP USER 'server_" .$test_id ."'@localhost'"); -mysql -uremoteuser -pDrV75Uyyxr9VFVVt -hmysql.iaregamer.com -e "DROP USER server_'${test_id}'" diff --git a/Panel/modules/billing/create_coupons_table.sql b/Panel/modules/billing/create_coupons_table.sql deleted file mode 100644 index 8a66add7..00000000 --- a/Panel/modules/billing/create_coupons_table.sql +++ /dev/null @@ -1,107 +0,0 @@ --- Enhanced coupon system for billing module --- This creates a flexible coupon system with game filters and usage tracking --- Table prefix is hardcoded to gsp_ for standalone billing module - --- Drop existing table if upgrading from old coupon module -DROP TABLE IF EXISTS `gsp_billing_coupons`; - --- Create enhanced coupons table -CREATE TABLE `gsp_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 = 'gsp_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 `gsp_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 gsp_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 = 'gsp_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 `gsp_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 gsp_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 = 'gsp_billing_invoices' - AND COLUMN_NAME = 'discount_amount' -); - -SET @addColumn = IF(@checkIfColumnExists = 0, - 'ALTER TABLE `gsp_billing_invoices` ADD COLUMN `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `amount`', - 'SELECT "Column discount_amount already exists in gsp_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 = 'gsp_billing_orders' - AND COLUMN_NAME = 'discount_amount' -); - -SET @addColumn = IF(@checkIfColumnExists = 0, - 'ALTER TABLE `gsp_billing_orders` ADD COLUMN `discount_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `price`', - 'SELECT "Column discount_amount already exists in gsp_billing_orders"' -); - -PREPARE stmt FROM @addColumn; -EXECUTE stmt; -DEALLOCATE PREPARE stmt; - --- Sample coupons for testing -INSERT INTO `gsp_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); diff --git a/Panel/modules/billing/create_invoices_table.sql b/Panel/modules/billing/create_invoices_table.sql deleted file mode 100644 index b447cc30..00000000 --- a/Panel/modules/billing/create_invoices_table.sql +++ /dev/null @@ -1,34 +0,0 @@ --- Create billing_invoices table for invoice-first flow --- Run this SQL to enable the new billing system --- Table prefix is hardcoded to gsp_ for standalone billing module - -CREATE TABLE IF NOT EXISTS `gsp_billing_invoices` ( - `invoice_id` INT(11) NOT NULL AUTO_INCREMENT, - `order_id` INT(11) NOT NULL DEFAULT 0, - `user_id` INT(11) NOT NULL, - `service_id` INT(11) NOT NULL, - `home_name` VARCHAR(255) NOT NULL DEFAULT '', - `ip` INT(11) NOT NULL DEFAULT 0, - `max_players` INT(11) NOT NULL DEFAULT 0, - `remote_control_password` VARCHAR(255) NULL, - `ftp_password` VARCHAR(255) NULL, - `customer_name` VARCHAR(255) NOT NULL DEFAULT '', - `customer_email` VARCHAR(255) NOT NULL DEFAULT '', - `amount` FLOAT(15,2) NOT NULL DEFAULT 0, - `currency` VARCHAR(3) NOT NULL DEFAULT 'USD', - `status` VARCHAR(16) NOT NULL DEFAULT 'due', - `invoice_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - `due_date` DATETIME NULL, - `paid_date` DATETIME NULL, - `payment_txid` VARCHAR(255) NULL, - `payment_method` VARCHAR(50) NULL, - `description` VARCHAR(500) NOT NULL DEFAULT '', - `invoice_duration` VARCHAR(16) NOT NULL DEFAULT 'month', - `qty` INT(11) NOT NULL DEFAULT 1, - PRIMARY KEY (`invoice_id`), - KEY `order_id` (`order_id`), - KEY `user_id` (`user_id`), - KEY `status` (`status`), - KEY `due_date` (`due_date`), - KEY `service_id` (`service_id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; diff --git a/Panel/modules/billing/create_servers.php b/Panel/modules/billing/create_servers.php deleted file mode 100644 index 287af776..00000000 --- a/Panel/modules/billing/create_servers.php +++ /dev/null @@ -1,1535 +0,0 @@ - false, 'error' => $error, 'path' => $logFile); - } - if (!is_writable($logDir)) { - $error = "Billing trace log directory is not writable: {$logDir}"; - billing_set_trace_error($error); - return array('ok' => false, 'error' => $error, 'path' => $logFile); - } - if (!file_exists($logFile)) { - $result = @file_put_contents($logFile, '', FILE_APPEND | LOCK_EX); - if ($result === false) { - $error = "Could not create billing trace log file: {$logFile}"; - billing_set_trace_error($error); - return array('ok' => false, 'error' => $error, 'path' => $logFile); - } - } - if (!is_writable($logFile)) { - $error = "Billing trace log file is not writable: {$logFile}"; - billing_set_trace_error($error); - return array('ok' => false, 'error' => $error, 'path' => $logFile); - } - return array('ok' => true, 'path' => $logFile); - } -} - -if (!function_exists('billing_provision_trace')) { - function billing_provision_trace($message, array $context = array()): bool - { - $ready = billing_ensure_trace_log_ready(); - if (empty($ready['ok'])) { - return false; - } - $baseContext = isset($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']) && is_array($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']) - ? $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT'] - : array(); - $merged = array_merge($baseContext, $context); - $line = '[' . date('Y-m-d H:i:s') . '] ' . trim((string)$message); - if (!empty($merged)) { - $parts = array(); - foreach ($merged as $key => $value) { - $parts[] = $key . '=' . billing_format_trace_value($value); - } - $line .= ' | ' . implode(' | ', $parts); - } - $result = @file_put_contents($ready['path'], $line . PHP_EOL, FILE_APPEND | LOCK_EX); - if ($result === false) { - billing_set_trace_error('Failed to append billing trace log: ' . $ready['path']); - return false; - } - return true; - } -} - -if (!function_exists('billing_get_server_home_row')) { - function billing_get_server_home_row($db, string $db_prefix, int $home_id): array - { - if ($home_id <= 0) { - return array(); - } - $row = $db->resultQuery( - "SELECT * FROM `{$db_prefix}server_homes` - WHERE home_id=" . $db->realEscapeSingle($home_id) . " - LIMIT 1" - ); - return !empty($row[0]) ? (array)$row[0] : array(); - } -} - -if (!function_exists('billing_trace_home_info_summary')) { - function billing_trace_home_info_summary(array $home_info): array - { - $mods = array(); - foreach ((array)($home_info['mods'] ?? array()) as $modId => $modInfo) { - $mods[] = array( - 'mod_id' => intval($modId), - 'mod_key' => $modInfo['mod_key'] ?? '', - ); - } - return array( - 'home_id' => intval($home_info['home_id'] ?? 0), - 'home_cfg_id' => intval($home_info['home_cfg_id'] ?? 0), - 'home_cfg_file' => $home_info['home_cfg_file'] ?? '', - 'remote_server_id' => intval($home_info['remote_server_id'] ?? 0), - 'home_path' => $home_info['home_path'] ?? '', - 'agent_ip' => $home_info['agent_ip'] ?? '', - 'agent_port' => $home_info['agent_port'] ?? '', - 'mods' => $mods, - ); - } -} - -if (!function_exists('billing_trace_settings_summary')) { - function billing_trace_settings_summary(array $settings): array - { - return array( - 'panel_name' => $settings['panel_name'] ?? '', - 'steam_user_configured' => !empty($settings['steam_user']), - 'steam_guard_configured' => !empty($settings['steam_guard']), - ); - } -} - -if (!function_exists('billing_detect_install_state')) { - function billing_detect_install_state(array $home_info): array - { - $state = array( - 'complete' => false, - 'remote_status' => 'unknown', - 'update_active' => false, - 'exec_path' => '', - 'exec_exists' => false, - 'reason' => '', - ); - if (empty($home_info['home_id'])) { - $state['reason'] = 'home_info is missing home_id.'; - return $state; - } - if (empty($home_info['home_cfg_file'])) { - $state['reason'] = 'home_cfg_file is missing; install completion cannot be verified.'; - return $state; - } - $xml_cfg_file = $home_info['home_cfg_file'] ?? ''; - $xml_rel = rtrim(SERVER_CONFIG_LOCATION, '/') . '/' . $xml_cfg_file; - $xml_abs = $xml_rel; - if (!is_readable($xml_rel)) { - $panel_root = realpath(__DIR__ . '/../../'); - if ($panel_root !== false) { - $xml_abs = $panel_root . '/' . ltrim($xml_rel, '/'); - } - } - if (function_exists('billing_provision_trace')) { - billing_provision_trace('billing_detect_install_state: XML path resolution.', array( - 'home_id' => intval($home_info['home_id'] ?? 0), - 'home_cfg_file' => $xml_cfg_file, - 'xml_rel_path' => $xml_rel, - 'xml_abs_path' => $xml_abs, - 'cwd' => getcwd(), - 'xml_file_exists' => file_exists($xml_abs), - 'xml_is_readable' => is_readable($xml_abs), - )); - } - $server_xml = read_server_config($xml_abs); - if (!$server_xml) { - $state['reason'] = "Could not read server config XML; install completion cannot be verified. Tried: {$xml_abs}"; - return $state; - } - $server_exec_name = trim((string)($server_xml->server_exec_name ?? '')); - if ($server_exec_name === '') { - $state['reason'] = 'server_exec_name is empty; install completion cannot be verified.'; - return $state; - } - $remote = new OGPRemoteLibrary($home_info['agent_ip'], $home_info['agent_port'], $home_info['encryption_key'], $home_info['timeout']); - $hostStat = $remote->status_chk(); - $state['remote_status'] = ($hostStat === 1) ? 'online' : 'offline'; - if ($hostStat !== 1) { - $state['reason'] = 'Agent is offline; install completion cannot be verified.'; - return $state; - } - $log_txt = ''; - $update_active = $remote->get_log(OGP_SCREEN_TYPE_UPDATE, intval($home_info['home_id']), clean_path($home_info['home_path']), $log_txt); - $state['update_active'] = ($update_active == 1); - $state['update_log'] = $log_txt; - $execFolder = clean_path($home_info['home_path'] . "/" . (string)($server_xml->exe_location ?? '')); - $execPath = clean_path($execFolder . "/" . $server_exec_name); - $state['exec_path'] = $execPath; - $state['exec_exists'] = ($remote->rfile_exists($execPath) === 1); - $state['complete'] = $state['exec_exists']; - if ($state['exec_exists']) { - $state['reason'] = 'Expected executable already exists on the remote server.'; - } elseif (!empty($state['update_active'])) { - $state['reason'] = 'Server installation is in progress.'; - } else { - $state['reason'] = 'Expected executable is missing on the remote server.'; - } - return $state; - } -} - -if (!function_exists('billing_store_provision_session_result')) { - function billing_store_provision_session_result(string $key, array $payload): void - { - if (session_status() === PHP_SESSION_ACTIVE) { - $_SESSION['billing_provision_results'][$key] = $payload; - } - } -} - -if (!function_exists('billing_generate_provision_password')) { - function billing_generate_provision_password() - { - $length = 6; - $alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - $alphabetLen = strlen($alphabet); - $password = ''; - try { - for ($i = 0; $i < $length; $i++) { - $password .= $alphabet[random_int(0, $alphabetLen - 1)]; - } - return $password; - } catch (Throwable $e) { - billing_provision_trace('Password generation fallback after exception.', array('exception' => $e->getMessage())); - for ($i = 0; $i < $length; $i++) { - $password .= $alphabet[mt_rand(0, $alphabetLen - 1)]; - } - return $password; - } - } -} - -if (!function_exists('billing_is_valid_provision_password')) { - function billing_is_valid_provision_password($value): bool - { - return is_string($value) && preg_match('/^[A-Za-z0-9]{6}$/', $value) === 1; - } -} - -if (!function_exists('billing_should_regenerate_provision_password')) { - function billing_should_regenerate_provision_password($value): bool - { - return !billing_is_valid_provision_password($value) || strcasecmp((string)$value, 'ChangeMe') === 0; - } -} - -if (!function_exists('billing_agent_offline_reason')) { - function billing_agent_offline_reason(int $remote_server_id, array $home_info): string - { - return "Agent is offline for remote server #{$remote_server_id} (" . ($home_info['agent_ip'] ?? 'unknown') . ":" . ($home_info['agent_port'] ?? 'unknown') . ")."; - } -} - -if (!function_exists('billing_detect_service_os')) { - function billing_detect_service_os(string $cfg_file, string $game_key): string - { - $haystack = strtolower(trim($cfg_file !== '' ? $cfg_file : $game_key)); - if ($haystack === '') { - return 'any'; - } - if (preg_match('/(?:^|[_\\-])(win|windows)(?:[_\\-]|$)/i', $haystack)) { - return 'windows'; - } - if (preg_match('/(?:^|[_\\-])linux(?:[_\\-]|$)/i', $haystack)) { - return 'linux'; - } - return 'any'; - } -} - -if (!function_exists('billing_normalize_node_os')) { - function billing_normalize_node_os(string $server_os): string - { - $value = strtolower(trim($server_os)); - if ($value === '' || $value === 'any') { - return 'any'; - } - if (str_starts_with($value, 'win')) { - return 'windows'; - } - if (str_starts_with($value, 'lin')) { - return 'linux'; - } - return $value; - } -} - -if (!function_exists('billing_remote_servers_has_os_column')) { - function billing_remote_servers_has_os_column($db, string $db_prefix): bool - { - static $cache = array(); - if (isset($cache[$db_prefix])) { - return $cache[$db_prefix]; - } - $rows = $db->resultQuery("SHOW COLUMNS FROM `{$db_prefix}remote_servers` LIKE 'server_os'"); - $cache[$db_prefix] = !empty($rows); - return $cache[$db_prefix]; - } -} - -if (!function_exists('billing_invoke_provision')) { - function billing_invoke_provision(array $options = array()) - { - if (empty($options['caller_source'])) { - $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); - $caller = $trace[1] ?? array(); - $options['caller_source'] = ($caller['file'] ?? __FILE__) . ':' . intval($caller['line'] ?? 0); - } - unset($GLOBALS['BILLING_PROVISION_TRACE_ERROR'], $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); - $GLOBALS['BILLING_PROVISION_OVERRIDE'] = $options; - ob_start(); - exec_ogp_module(); - $output = ob_get_clean(); - $result = isset($GLOBALS['BILLING_PROVISION_LAST_RESULT']) ? $GLOBALS['BILLING_PROVISION_LAST_RESULT'] : array(); - $result['output'] = $output; - $result['trace_log_path'] = billing_provision_trace_relative_path(); - if (!empty($GLOBALS['BILLING_PROVISION_TRACE_ERROR'])) { - $result['trace_error'] = $GLOBALS['BILLING_PROVISION_TRACE_ERROR']; - } - unset($GLOBALS['BILLING_PROVISION_OVERRIDE'], $GLOBALS['BILLING_PROVISION_LAST_RESULT'], $GLOBALS['BILLING_PROVISION_TRACE_ERROR'], $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); - return $result; - } -} - -if (!function_exists('billing_get_remote_ip_ids')) { - function billing_get_remote_ip_ids($db, string $db_prefix, int $remote_server_id): array - { - $rows = $db->resultQuery( - "SELECT ip_id FROM `{$db_prefix}remote_server_ips` WHERE remote_server_id=" . $db->realEscapeSingle($remote_server_id) . " ORDER BY ip_id ASC" - ); - $ipIds = array(); - foreach ((array)$rows as $row) { - $ipId = intval($row['ip_id'] ?? 0); - if ($ipId > 0) { - $ipIds[] = $ipId; - } - } - return $ipIds; - } -} - -if (!function_exists('billing_allocate_home_port')) { - function billing_allocate_home_port($db, string $db_prefix, int $home_id, int $remote_server_id, int $home_cfg_id): array - { - $ipIds = billing_get_remote_ip_ids($db, $db_prefix, $remote_server_id); - billing_provision_trace('Resolved remote server IP IDs for port allocation.', array( - 'home_id' => $home_id, - 'selected_remote_server_id' => $remote_server_id, - 'selected_home_cfg_id' => $home_cfg_id, - 'ip_ids' => $ipIds, - )); - if (empty($ipIds)) { - billing_provision_trace('Port allocation failed because no remote_server_ips rows were found.', array( - 'home_id' => $home_id, - 'selected_remote_server_id' => $remote_server_id, - 'selected_home_cfg_id' => $home_cfg_id, - )); - return array('ok' => false, 'error' => "No IP addresses are configured for remote server #{$remote_server_id}."); - } - - $ips_with_no_range = array(); - $ips_exhausted = array(); - foreach ($ipIds as $ipId) { - $ranges = $db->resultQuery( - "SELECT start_port, end_port, port_increment - FROM `{$db_prefix}arrange_ports` - WHERE ip_id=" . $db->realEscapeSingle($ipId) . " - AND home_cfg_id=" . $db->realEscapeSingle($home_cfg_id) . " - ORDER BY range_id ASC" - ); - if (empty($ranges)) { - $ranges = $db->resultQuery( - "SELECT start_port, end_port, port_increment - FROM `{$db_prefix}arrange_ports` - WHERE ip_id=" . $db->realEscapeSingle($ipId) . " - AND home_cfg_id=0 - ORDER BY range_id ASC" - ); - } - if (empty($ranges)) { - $ips_with_no_range[] = $ipId; - billing_provision_trace('No port range found for current ip_id.', array( - 'home_id' => $home_id, - 'selected_ip_id' => $ipId, - 'selected_home_cfg_id' => $home_cfg_id, - )); - continue; - } - billing_provision_trace('Loaded port ranges for current ip_id.', array( - 'home_id' => $home_id, - 'selected_ip_id' => $ipId, - 'port_ranges_used' => $ranges, - )); - - $usedRows = $db->resultQuery( - "SELECT port FROM `{$db_prefix}home_ip_ports` WHERE ip_id=" . $db->realEscapeSingle($ipId) - ); - $usedPorts = array(); - foreach ((array)$usedRows as $usedRow) { - $usedPorts[intval($usedRow['port'] ?? 0)] = true; - } - - foreach ((array)$ranges as $range) { - $start = intval($range['start_port'] ?? 0); - $end = intval($range['end_port'] ?? 0); - $increment = max(1, intval($range['port_increment'] ?? 1)); - if ($start <= 0 || $end <= 0 || $start > $end) { - continue; - } - - for ($port = $start; $port <= $end; $port += $increment) { - if (isset($usedPorts[$port])) { - continue; - } - $safeIpId = $db->realEscapeSingle($ipId); - $safePort = $db->realEscapeSingle($port); - $safeHome = $db->realEscapeSingle($home_id); - $insertOk = $db->query( - "INSERT INTO `{$db_prefix}home_ip_ports` (`ip_id`, `port`, `home_id`) - SELECT {$safeIpId}, {$safePort}, {$safeHome} - FROM DUAL - WHERE NOT EXISTS ( - SELECT 1 - FROM `{$db_prefix}home_ip_ports` - WHERE ip_id = {$safeIpId} - AND port = {$safePort} - )" - ); - billing_provision_trace('Attempted home_ip_ports insert.', array( - 'home_id' => $home_id, - 'selected_ip_id' => $ipId, - 'selected_port' => $port, - 'home_ip_ports_insert_succeeded' => (bool)$insertOk, - )); - if (!$insertOk) { - continue; - } - $verify = $db->resultQuery( - "SELECT home_id FROM `{$db_prefix}home_ip_ports` - WHERE ip_id = {$safeIpId} - AND port = {$safePort} - AND home_id = {$safeHome} - LIMIT 1" - ); - if (!empty($verify)) { - billing_provision_trace('Port allocation succeeded.', array( - 'home_id' => $home_id, - 'selected_ip_id' => $ipId, - 'selected_port' => $port, - 'home_ip_ports_insert_succeeded' => true, - )); - return array('ok' => true, 'ip_id' => $ipId, 'port' => intval($port)); - } - } - } - $ips_exhausted[] = $ipId; - } - - if (!empty($ips_with_no_range) && count($ips_with_no_range) === count($ipIds)) { - billing_provision_trace('Port allocation failed because no matching arrange_ports ranges were found.', array( - 'home_id' => $home_id, - 'selected_remote_server_id' => $remote_server_id, - 'ips_with_no_range' => $ips_with_no_range, - )); - return array('ok' => false, 'error' => "No port range found for home_cfg_id #{$home_cfg_id} on ip_id(s) [" . implode(',', $ips_with_no_range) . "] for remote server #{$remote_server_id}."); - } - billing_provision_trace('Port allocation failed because all ranges were exhausted.', array( - 'home_id' => $home_id, - 'selected_remote_server_id' => $remote_server_id, - 'selected_home_cfg_id' => $home_cfg_id, - 'ips_exhausted' => !empty($ips_exhausted) ? $ips_exhausted : $ipIds, - )); - return array('ok' => false, 'error' => "No available port in arrange_ports for remote server #{$remote_server_id}, home_cfg_id #{$home_cfg_id}, ip_id(s) [" . implode(',', !empty($ips_exhausted) ? $ips_exhausted : $ipIds) . "]."); - } -} - -if (!function_exists('billing_resolve_mod_cfg_id')) { - function billing_resolve_mod_cfg_id($db, int $home_cfg_id, int $preferred_mod_cfg_id): array - { - $mods = $db->getCfgMods($home_cfg_id); - billing_provision_trace('Loaded config mods for home_cfg_id.', array( - 'selected_home_cfg_id' => $home_cfg_id, - 'preferred_mod_cfg_id' => $preferred_mod_cfg_id, - 'cfg_mod_rows' => $mods, - )); - if (empty($mods)) { - billing_provision_trace('No config mods found for home_cfg_id.', array( - 'selected_home_cfg_id' => $home_cfg_id, - )); - return array('ok' => false, 'error' => "No config_mods rows found for home_cfg_id #{$home_cfg_id}."); - } - - $first = null; - $available_mod_cfg_ids = array(); - foreach ((array)$mods as $mod) { - $modCfgId = intval($mod['mod_cfg_id'] ?? 0); - if ($modCfgId <= 0) { - continue; - } - $available_mod_cfg_ids[] = $modCfgId; - if ($first === null) { - $first = $modCfgId; - } - if ($preferred_mod_cfg_id > 0 && $modCfgId === $preferred_mod_cfg_id) { - billing_provision_trace('Selected preferred mod_cfg_id for provisioning.', array( - 'selected_home_cfg_id' => $home_cfg_id, - 'selected_mod_cfg_id' => $modCfgId, - )); - return array('ok' => true, 'mod_cfg_id' => $modCfgId); - } - } - - if ($first !== null) { - billing_provision_trace('Selected fallback mod_cfg_id for provisioning.', array( - 'selected_home_cfg_id' => $home_cfg_id, - 'selected_mod_cfg_id' => $first, - )); - return array('ok' => true, 'mod_cfg_id' => $first); - } - - billing_provision_trace('No usable mod_cfg_id was found for provisioning.', array( - 'selected_home_cfg_id' => $home_cfg_id, - 'available_mod_cfg_ids' => $available_mod_cfg_ids, - )); - return array('ok' => false, 'error' => "No usable mod_cfg_id found for home_cfg_id #{$home_cfg_id}. Available mod_cfg_id values: [" . implode(',', $available_mod_cfg_ids) . "]."); - } -} - -if (!function_exists('billing_get_home_ip_port')) { - function billing_get_home_ip_port($db, string $db_prefix, int $home_id): array - { - $row = $db->resultQuery( - "SELECT ip_id, port - FROM `{$db_prefix}home_ip_ports` - WHERE home_id=" . $db->realEscapeSingle($home_id) . " - ORDER BY ip_id ASC, port ASC - LIMIT 1" - ); - if (!empty($row[0])) { - return array( - 'ok' => true, - 'ip_id' => intval($row[0]['ip_id'] ?? 0), - 'port' => intval($row[0]['port'] ?? 0), - ); - } - return array('ok' => false, 'ip_id' => 0, 'port' => 0); - } -} - -if (!function_exists('billing_write_provision_log')) { - /** - * Writes one JSON line per provisioning attempt to modules/billing/logs/provisioning.log. - * Fields include order/invoice/user/home/home_cfg/mod/ip/port/mechanism/install_result/error/message. - */ - function billing_write_provision_log(array $context): void - { - $logDir = __DIR__ . '/logs'; - if (!is_dir($logDir)) { - mkdir($logDir, 0755, true); - } - $status = strtoupper((string)($context['install_result'] ?? 'INFO')); - $line = '[' . date('Y-m-d H:i:s') . '] [' . $status . '] ' . json_encode($context, JSON_UNESCAPED_SLASHES) . PHP_EOL; - $result = file_put_contents($logDir . '/provisioning.log', $line, FILE_APPEND | LOCK_EX); - if ($result === false) { - error_log('billing_write_provision_log: failed to append provisioning.log'); - } - } -} - -function exec_ogp_module() -{ - global $db,$view,$settings,$table_prefix; - $db_prefix = isset($table_prefix) ? $table_prefix : ''; - - // $now is used in multiple branches below — define it once here so it is - // always a string that date() / strtotime() can handle safely (PHP 8 fix). - $now = date('Y-m-d H:i:s'); - - $override = isset($GLOBALS['BILLING_PROVISION_OVERRIDE']) ? $GLOBALS['BILLING_PROVISION_OVERRIDE'] : null; - $user_id = isset($override['user_id']) ? intval($override['user_id']) : (isset($_SESSION['user_id']) ? intval($_SESSION['user_id']) : 0); - $isAdmin = isset($override['is_admin']) ? (bool)$override['is_admin'] : $db->isAdmin($user_id); - $provision_all = $override ? !empty($override['provision_all']) : isset($_POST['provision_all']); - $caller_source = $override['caller_source'] ?? (__FILE__ . ':0'); - $dbNameRow = $db->resultQuery("SELECT DATABASE() AS db_name"); - $active_db_name = $dbNameRow[0]['db_name'] ?? ''; - $orderIds = array(); - if ($override && !empty($override['order_ids'])) { - $orderIds = array_map('intval', (array)$override['order_ids']); - } - if (empty($orderIds)) { - $order_id = null; - if (isset($_POST['order_id'])) { - $order_id = $_POST['order_id']; - } - if(isset($_GET['order_id'])){ - $order_id = $_GET['order_id']; - } - if (!empty($order_id)) { - $orderIds = array(intval($order_id)); - } - } - $traceReady = billing_ensure_trace_log_ready(); - if (empty($traceReady['ok'])) { - echo "

" . htmlspecialchars((string)$traceReady['error'], ENT_QUOTES, 'UTF-8') . "

"; - $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array( - 'provisioned_count' => 0, - 'failed_count' => 1, - 'orders' => array(), - 'details' => array(), - 'trace_log_path' => billing_provision_trace_relative_path(), - 'trace_error' => $traceReady['error'], - ); - return; - } - billing_provision_trace('START provisioning attempt', array( - 'caller_source' => $caller_source, - 'order_ids_received' => $orderIds, - 'user_id_received' => $user_id, - 'is_admin' => $isAdmin, - 'provision_all' => $provision_all, - 'active_db_name' => $active_db_name, - 'db_prefix' => $db_prefix, - )); - - // Handle provision_all request - provision all Active (paid) orders for this user - if ($provision_all) { - if ( $isAdmin ){ - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE status='Active' ORDER BY order_id" ); - } else { - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE user_id=".$db->realEscapeSingle($user_id)." AND status='Active' ORDER BY order_id" ); - } - billing_provision_trace('Loaded orders for provision_all request.', array('loaded_order_count' => count((array)$orders))); - } - // Handle provision_single or order_id parameter - provision specific order - else { - if (empty($orderIds)) { - billing_provision_trace('END failure: provisioning request returned early because no order ID was supplied.'); - echo "
No order ID specified.
"; - $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array('provisioned_count'=>0,'failed_count'=>0,'orders'=>array(),'details'=>array(),'trace_log_path'=>billing_provision_trace_relative_path()); - return; - } - $idList = implode(',', array_map('intval', $orderIds)); - if ( $isAdmin ){ - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND status='Active'" ); - } else { - $orders = $db->resultQuery( "SELECT * FROM `{$db_prefix}billing_orders` WHERE order_id IN ($idList) AND user_id=".$db->realEscapeSingle($user_id)." AND status='Active'" ); - } - billing_provision_trace('Loaded explicit order list for provisioning.', array('loaded_order_count' => count((array)$orders))); - } - $processed_orders = array(); - $order_results = array(); - if( !empty($orders) ) - { - $provisioned_count = 0; - $failed_count = 0; - $failed_messages = array(); - - foreach ((array)$orders as $order) - { - $trace_id = uniqid('prov_', true); - $GLOBALS['BILLING_PROVISION_TRACE_CONTEXT'] = array( - 'trace_id' => $trace_id, - 'order_id' => intval($order['order_id'] ?? 0), - ); - billing_provision_trace('Loaded billing order for provisioning.', array('order_row' => $order)); - try { - $home_id = 0; - $order_failed = false; - $order_failure_reason = ''; - $end_date = null; - $end_date_str = null; - $order_id = $order['order_id']; - $processed_orders[] = intval($order_id); - $service_id = $order['service_id']; - $home_name = $order['home_name']; - $remote_control_password = $order['remote_control_password']; - $ftp_password = $order['ftp_password']; - if (billing_should_regenerate_provision_password($remote_control_password)) { - $remote_control_password = billing_generate_provision_password(); - } - if (billing_should_regenerate_provision_password($ftp_password)) { - $ftp_password = billing_generate_provision_password(); - } - $ip = $order['ip']; - $max_players = $order['max_players']; - $user_id = $order['user_id']; - $extended = isset($order['extended']) && $order['extended'] == "1" ? TRUE : FALSE; - $already_provisioned = !$extended && intval($order['home_id'] ?? 0) > 0; - $provision_invoice_id = 0; - $selected_ip_id = 0; - $selected_port = 0; - $selected_mod_id = 0; - $resolved_mod_cfg_id = 0; - $home_cfg_id = 0; - $mod_cfg_id = 0; - $selected_config_xml = ''; - $selected_game_key = ''; - $selected_service_os = 'any'; - $install_mechanism = BILLING_INSTALL_MECHANISM; - $install_result = 'pending'; - $install_message = ''; - $install_attempted = false; - $needs_existing_home_retry = false; - $skip_reason = ''; - $home_info = array(); - $home_row_before = array(); - $home_row_after = array(); - $install_state = array(); - $autoInstall = array(); - $invoiceRow = $db->resultQuery( - "SELECT invoice_id - FROM `{$db_prefix}billing_invoices` - WHERE order_id=" . $db->realEscapeSingle($order_id) . " - ORDER BY invoice_id DESC - LIMIT 1" - ); - if (!empty($invoiceRow[0]['invoice_id'])) { - $provision_invoice_id = intval($invoiceRow[0]['invoice_id']); - } - billing_provision_trace('Resolved latest invoice row for order.', array('invoice_row' => $invoiceRow)); - //Query service info - $service = $db->resultQuery( - "SELECT bs.*, ch.home_cfg_file, ch.game_key - FROM `{$db_prefix}billing_services` bs - LEFT JOIN `{$db_prefix}config_homes` ch ON ch.home_cfg_id = bs.home_cfg_id - WHERE bs.service_id=" . $db->realEscapeSingle($service_id) - ); - billing_provision_trace('Loaded billing service row.', array( - 'service_id' => intval($service_id), - 'service_row_found' => !empty($service[0]), - 'service_row' => !empty($service[0]) ? $service[0] : array(), - )); - - if( !empty( $service[0] ) ) - { - $home_cfg_id = $service[0]['home_cfg_id']; - $mod_cfg_id = $service[0]['mod_cfg_id']; - $selected_config_xml = (string)($service[0]['home_cfg_file'] ?? ''); - $selected_game_key = (string)($service[0]['game_key'] ?? ''); - $selected_service_os = billing_detect_service_os($selected_config_xml, $selected_game_key); - //remote_server_id has been stored in IP_ID - //$remote_server_id = $service[0]['remote_server_id']; - $remote_server_id = $order['ip']; - - $ftp = $service[0]['ftp']; - $install_method = $service[0]['install_method']; - $manual_url = $service[0]['manual_url']; - $access_rights = $service[0]['access_rights']; - billing_provision_trace('Provisioning inputs resolved from order and service.', array( - 'service_id' => intval($service_id), - 'order_status' => $order['status'] ?? '', - 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), - 'selected_home_cfg_id' => intval($home_cfg_id), - 'selected_config_xml' => $selected_config_xml, - 'selected_service_os' => $selected_service_os, - 'selected_remote_server_id' => intval($remote_server_id), - )); - if (intval($home_cfg_id) <= 0) { - $order_failed = true; - $order_failure_reason = "Invalid home_cfg_id '{$home_cfg_id}' for service_id {$service_id}."; - } - if (!$order_failed && intval($remote_server_id) <= 0) { - $order_failed = true; - $order_failure_reason = "Invalid remote server selection '{$remote_server_id}' on order #{$order_id} for service_id {$service_id}."; - } - if (!$order_failed) { - $allowedRemote = array(); - foreach (explode(',', (string)($service[0]['remote_server_id'] ?? '')) as $part) { - $part = trim($part); - if ($part !== '' && ctype_digit($part)) { - $allowedRemote[(int)$part] = true; - } - } - if (!empty($allowedRemote) && !isset($allowedRemote[intval($remote_server_id)])) { - $order_failed = true; - $order_failure_reason = "Selected remote server #{$remote_server_id} is not enabled for service_id {$service_id}."; - } - } - if (!$order_failed && billing_remote_servers_has_os_column($db, $db_prefix)) { - $remoteRow = $db->resultQuery( - "SELECT remote_server_id, remote_server_name, server_os - FROM `{$db_prefix}remote_servers` - WHERE remote_server_id=" . $db->realEscapeSingle($remote_server_id) . " - LIMIT 1" - ); - if (empty($remoteRow[0])) { - $order_failed = true; - $order_failure_reason = "Remote server #{$remote_server_id} not found for order #{$order_id} (service_id {$service_id})."; - } else { - $node_os = billing_normalize_node_os((string)($remoteRow[0]['server_os'] ?? 'any')); - billing_provision_trace('Resolved remote server OS for compatibility check.', array( - 'selected_remote_server_id' => intval($remote_server_id), - 'selected_node_os' => $node_os, - 'selected_service_os' => $selected_service_os, - )); - if ($selected_service_os !== 'any' && $node_os !== 'any' && $selected_service_os !== $node_os) { - $order_failed = true; - $order_failure_reason = $selected_service_os === 'windows' - ? 'This service requires a Windows server location.' - : 'This service requires a Linux server location.'; - } - } - } - } - else - { - $order_failed = true; - $order_failure_reason = "Service ID {$service_id} not found."; - billing_provision_trace('Eligibility skip: billing service row is missing.', array('service_id' => intval($service_id))); - } - - if(!$order_failed && $already_provisioned) - { - $home_id = intval($order['home_id']); - $home_row_before = billing_get_server_home_row($db, $db_prefix, $home_id); - billing_provision_trace('Existing home_id detected on billing order.', array( - 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), - 'server_homes_row_before' => $home_row_before, - )); - $home_info = $db->getGameHome($home_id); - if (empty($home_row_before) || empty($home_info)) { - $order_failed = true; - $order_failure_reason = "Order #{$order_id} references home_id {$home_id} but server_homes row is missing."; - $db->logger('BILLING PROVISION DATA INTEGRITY ERROR: ' . $order_failure_reason); - billing_provision_trace('Eligibility failure: existing home_id is linked but server_homes row is missing.', array('home_id_after_creation_or_lookup' => $home_id)); - } - $existingIpPort = billing_get_home_ip_port($db, $db_prefix, intval($home_id)); - if (!empty($existingIpPort['ok'])) { - $selected_ip_id = intval($existingIpPort['ip_id']); - $selected_port = intval($existingIpPort['port']); - } - billing_provision_trace('Existing home IP:port lookup completed.', array( - 'home_id_after_creation_or_lookup' => $home_id, - 'ip_port_row_found' => !empty($existingIpPort['ok']), - 'selected_ip_id' => intval($selected_ip_id), - 'selected_port' => intval($selected_port), - )); - $has_ip_port = !empty($existingIpPort['ok']); - $has_mods = !empty($home_info['mods']) && is_array($home_info['mods']); - if (!$order_failed && (!$has_ip_port || !$has_mods)) { - $needs_existing_home_retry = true; - $install_message = "Existing home #{$home_id} requires provisioning completion (ip_port=" . ($has_ip_port ? 'yes' : 'no') . ", mods=" . ($has_mods ? 'yes' : 'no') . ")."; - billing_provision_trace('Existing home requires provisioning retry because prerequisites are incomplete.', array( - 'home_id_after_creation_or_lookup' => $home_id, - 'has_ip_port' => $has_ip_port, - 'has_mods' => $has_mods, - )); - } - if (!$order_failed && !$needs_existing_home_retry) { - $install_state = billing_detect_install_state((array)$home_info); - billing_provision_trace('Existing home install state verification completed.', array( - 'home_id_after_creation_or_lookup' => $home_id, - 'install_state' => $install_state, - )); - if (empty($install_state['complete'])) { - $needs_existing_home_retry = true; - $install_message = "Existing home #{$home_id} is not fully installed yet. " . ($install_state['reason'] ?? 'Install completion could not be verified.'); - } - } - if (!$order_failed && !$needs_existing_home_retry && !empty($install_state['complete'])) { - $install_result = 'completed'; - $install_message = $install_message !== '' ? $install_message : "Order #{$order_id} already provisioned and installed; no action required."; - $skip_reason = $install_message; - billing_provision_trace('Eligibility skip: existing home is already provisioned and install is complete.', array( - 'home_id_after_creation_or_lookup' => $home_id, - 'skip_reason' => $skip_reason, - )); - } - } - elseif(!$order_failed && $extended) - { - $home_id = $order['home_id']; - billing_provision_trace('Processing renewal for existing billing order.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - )); - - //Get The home info without mods in 1 array (Necesary for remote connection). - $home_info = $db->getGameHomeWithoutMods($home_id); - $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); - billing_provision_trace('Loaded renewal home state.', array( - 'server_homes_row_before' => $home_row_before, - 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), - )); - - //Create the remote connection - $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); - - //Reassign the server - $db->assignHomeTo( "user", $user_id, $home_id, $access_rights ); - - //Reenable the FTP account - if ($ftp == "enabled") - { - $remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']); - $db->changeFtpStatus('enabled',$home_info['home_id']); - } - echo "

Server Installed, Check your Email for Details


"; - -//Panel Log - $db->logger( "RENEWED SERVER " . $home_id); -// SEND EMAIL - $settings = $db->getSettings(); - $subject = "Gameserver Renewel at " . $settings['panel_name']; - $email = $db->resultQuery(" SELECT DISTINCT users_email - FROM {$table_prefix}users, {$table_prefix}billing_orders - WHERE {$table_prefix}users.user_id = $user_id")[0]["users_email"]; - - $message = "Your server, " . $home_name ." ID #". $home_id . " at " . $settings['panel_name'] . " has just been renewed.
- Thank You for your continued support.
- If you have any questions or requests, visit our website or contact us directly in our Discord Server."; - - $mail = mymail($email, $subject, $message, $settings); - $rundate = date('d/M/y G:i', is_numeric($now) ? (int)$now : strtotime($now)); - - if (!$mail) - $db->logger( "Email FAILED - Server Renewed " . $home_id); -// END EMAIL - - //WEBHOOK Discord - discordmsg(array('content' => "The ". $home_name ." server ID #". $home_id . " has just been renewed."), $settings['discord_webhook_main'] ?? ''); - //end WEBHOOK Discord - - } - elseif(!$order_failed) - { - billing_provision_trace('Provisioning new home because billing order has no completed install yet.', array( - 'order_home_id_before_provisioning' => intval($order['home_id'] ?? 0), - )); - //OPTIONS, change it at your choice; - $extra_params = "";//no extra params defined by default - $cpu_affinity = "NA";//Affinity to one core/thread of the cpu by number, use NA to disable it - $nice = "0";//Min priority=19 Max Priority=-19 - - //Add Game home to database - //HARD CODE TO /home/gameserver/ - $rserver = $db->getRemoteServer($remote_server_id); - if (empty($rserver)) { - $order_failed = true; - $order_failure_reason = "Remote server #{$remote_server_id} not found for order #{$order_id} (service_id {$service_id})."; - billing_provision_trace('Eligibility failure: selected remote server row is missing.', array( - 'selected_remote_server_id' => intval($remote_server_id), - )); - } - $game_path = "/home/gameserver/"; - if (!$order_failed) { - $home_id = $db->addGameHome($remote_server_id, $user_id, $home_cfg_id, $game_path, $home_name, $remote_control_password, $ftp_password); - billing_provision_trace('Attempted server_homes creation for billing order.', array( - 'selected_remote_server_id' => intval($remote_server_id), - 'selected_home_cfg_id' => intval($home_cfg_id), - 'home_id_after_creation_or_lookup' => intval($home_id), - )); - } - if (!$order_failed && (!$home_id || intval($home_id) <= 0)) { - $order_failed = true; - $order_failure_reason = "Could not create server_homes row for order #{$order_id}."; - } - if (!$order_failed) { - $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); - billing_provision_trace('Loaded server_homes row immediately after creation.', array( - 'server_homes_row_before' => $home_row_before, - )); - } - if (!$order_failed) { - // Billing storefront defaults FTP to enabled for newly provisioned homes so panel/account flows stay consistent after checkout. - $db->changeFtpStatus('enabled', intval($home_id)); - } - - // Add IP:Port pair with arrange_ports exact home_cfg_id preference and home_cfg_id=0 fallback. - if (!$order_failed) { - $allocatedPort = billing_allocate_home_port($db, $db_prefix, intval($home_id), intval($remote_server_id), intval($home_cfg_id)); - if (empty($allocatedPort['ok'])) { - $order_failed = true; - $order_failure_reason = (string)($allocatedPort['error'] ?? 'Port allocation failed.'); - $db->logger("Provisioning pending install for order #{$order_id}: {$order_failure_reason}"); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } else { - $selected_ip_id = intval($allocatedPort['ip_id'] ?? 0); - $selected_port = intval($allocatedPort['port'] ?? 0); - billing_provision_trace('Selected IP:port for new home.', array( - 'selected_ip_id' => $selected_ip_id, - 'selected_port' => $selected_port, - )); - } - } - - //Assign the Game Mod to the Game Home - $resolved_mod_cfg_id = intval($mod_cfg_id); - if (!$order_failed) { - $modResolution = billing_resolve_mod_cfg_id($db, intval($home_cfg_id), intval($mod_cfg_id)); - if (empty($modResolution['ok'])) { - $order_failed = true; - $order_failure_reason = (string)($modResolution['error'] ?? 'No mod profile available for base install.'); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } else { - $resolved_mod_cfg_id = intval($modResolution['mod_cfg_id']); - } - } - $mod_id = false; - if (!$order_failed) { - $mod_id = $db->addModToGameHome( $home_id, $resolved_mod_cfg_id ); - billing_provision_trace('Attempted game_mods attach for home.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_cfg_id' => intval($resolved_mod_cfg_id), - 'selected_mod_id' => intval($mod_id), - )); - if ($mod_id === false) { - $order_failed = true; - $order_failure_reason = "Could not attach mod_cfg_id {$resolved_mod_cfg_id} to home #{$home_id}."; - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - if (!$order_failed) { - $db->updateGameModParams( $max_players, $extra_params, $cpu_affinity, $nice, $home_id, $resolved_mod_cfg_id ); - $db->assignHomeTo( "user", $user_id, $home_id, $access_rights ); - $selected_mod_id = intval($mod_id); - billing_provision_trace('Updated game_mod params and assigned home to user.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_id' => intval($selected_mod_id), - 'user_id_received' => intval($user_id), - )); - } - - //Get The home info without mods in 1 array (Necesary for remote connection). - if (!$order_failed) { - $home_info = $db->getGameHomeWithoutMods($home_id); - if (empty($home_info)) { - $order_failed = true; - $order_failure_reason = "Could not load home info for home #{$home_id}."; - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - - //Create the remote connection - if (!$order_failed) { - $remote = new OGPRemoteLibrary($home_info['agent_ip'],$home_info['agent_port'],$home_info['encryption_key'],$home_info['timeout']); - } - - //Get Full home info in 1 array - if (!$order_failed) { - $home_info = $db->getGameHome($home_id); - if (empty($home_info) || empty($home_info['mods'])) { - $order_failed = true; - $order_failure_reason = "Mods are not configured for home #{$home_id}; base install profile could not be resolved."; - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - - //Enable FTP account in remote server - if (!$order_failed && $ftp == "enabled") - { - $remote->ftp_mgr("useradd", $home_info['home_id'], $home_info['ftp_password'], $home_info['home_path']); - $db->changeFtpStatus('enabled',$home_info['home_id']); - billing_provision_trace('Enabled FTP account for provisioned home.', array( - 'home_id_after_creation_or_lookup' => intval($home_info['home_id']), - )); - } - - if (!$order_failed) { - $install_attempted = true; - billing_provision_trace('Calling gamemanager_trigger_update_install for newly provisioned home.', array( - 'exact_call' => "gamemanager_trigger_update_install(\$db, \$home_info, {$mod_id}, ['settings' => ...])", - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_id' => intval($mod_id), - 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), - 'selected_settings' => billing_trace_settings_summary((array)$settings), - )); - $autoInstall = gamemanager_trigger_update_install( - $db, - $home_info, - intval($mod_id), - array('settings' => $settings) - ); - billing_provision_trace('gamemanager_trigger_update_install returned for newly provisioned home.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_id' => intval($mod_id), - 'gamemanager_trigger_update_install_result' => $autoInstall, - )); - $mod_id = intval($autoInstall['mod_id'] ?? $mod_id); - $selected_mod_id = intval($mod_id); - $install_message = (string)($autoInstall['message'] ?? ''); - if (!empty($autoInstall['already_running'])) { - $install_result = 'already_running'; - } elseif (!empty($autoInstall['started'])) { - $install_result = 'started'; - } elseif (!empty($autoInstall['completed'])) { - $install_result = 'completed'; - } else { - $install_result = 'pending'; - } - if (empty($autoInstall['ok'])) { - if (stripos((string)($autoInstall['message'] ?? ''), 'Agent is offline') !== false) { - $order_failure_reason = billing_agent_offline_reason(intval($remote_server_id), (array)$home_info); - } - $order_failed = true; - $order_failure_reason = $order_failure_reason !== '' ? $order_failure_reason : ("Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.')); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - if (!$order_failed) { - echo "


".get_lang('starting_installations')."


"; - //PANEL LOG - $db->logger( "CREATED NEW SERVER " . $home_id); - } - // SEND EMAIL to new server only - if(!$order_failed && $order['end_date'] == 0){ - $settings = $db->getSettings(); - $subject = "New Gameserver installed at " . $settings['panel_name']; - $email = $db->resultQuery(" SELECT DISTINCT users_email - FROM {$table_prefix}users, {$table_prefix}billing_orders - WHERE {$table_prefix}users.user_id = $user_id")[0]["users_email"]; - - $message = "Your server, " . $home_name ." ID #". $home_id . " at " . $settings['panel_name'] . " has just been created.
- Thank You for your continued support.
- If you have any questions or requests, visit our website or contact us directly in our Discord Server. - You can login to the Game Panel and click on Game Monitor to see your server.

- Thank you!
"; - $mail = mymail($email, $subject, $message, $settings); - $rundate = date('d/M/y G:i', is_numeric($now) ? (int)$now : strtotime($now)); - - if (!$mail) - $db->logger( "Email FAILED - Server Created " . $home_id); - - - //WEBHOOK Discord - discordmsg(array('content' => "A new server, ". $home_name ." ID #". $home_id . ", has just been created."), $settings['discord_webhook_main'] ?? ''); - //end WEBHOOK Discord - } - // END EMAIL - - - } - - // Retry install for orders that already have home_id but never triggered installation. - if (!$order_failed && !$extended && !$install_attempted && intval($home_id) > 0 && (!$already_provisioned || $needs_existing_home_retry)) { - billing_provision_trace('Continuing provisioning for existing home because install is incomplete.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'needs_existing_home_retry' => $needs_existing_home_retry, - 'install_message' => $install_message, - )); - if ($selected_ip_id <= 0 || $selected_port <= 0) { - $existingIpPort = billing_get_home_ip_port($db, $db_prefix, intval($home_id)); - if (!empty($existingIpPort['ok'])) { - $selected_ip_id = intval($existingIpPort['ip_id']); - $selected_port = intval($existingIpPort['port']); - billing_provision_trace('Reused existing IP:port for existing home retry.', array( - 'selected_ip_id' => $selected_ip_id, - 'selected_port' => $selected_port, - )); - } else { - $allocatedPort = billing_allocate_home_port($db, $db_prefix, intval($home_id), intval($remote_server_id), intval($home_cfg_id)); - if (empty($allocatedPort['ok'])) { - $order_failed = true; - $order_failure_reason = (string)($allocatedPort['error'] ?? 'Port allocation failed for existing home.'); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } else { - $selected_ip_id = intval($allocatedPort['ip_id'] ?? 0); - $selected_port = intval($allocatedPort['port'] ?? 0); - billing_provision_trace('Allocated new IP:port for existing home retry.', array( - 'selected_ip_id' => $selected_ip_id, - 'selected_port' => $selected_port, - )); - } - } - } - if (!$order_failed) { - if (empty($home_info)) { - $home_info = $db->getGameHome(intval($home_id)); - } - if (empty($home_info)) { - $order_failed = true; - $order_failure_reason = "Could not load home info for home #{$home_id}."; - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - if (!$order_failed && empty($home_info['mods'])) { - $modResolution = billing_resolve_mod_cfg_id($db, intval($home_cfg_id), intval($mod_cfg_id)); - if (empty($modResolution['ok'])) { - $order_failed = true; - $order_failure_reason = (string)($modResolution['error'] ?? "Mods are not configured for home #{$home_id}; base install profile could not be resolved."); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } else { - $resolved_mod_cfg_id = intval($modResolution['mod_cfg_id']); - $selected_mod_id = intval($db->addModToGameHome(intval($home_id), intval($resolved_mod_cfg_id))); - billing_provision_trace('Attempted to attach missing mod during existing home retry.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_cfg_id' => intval($resolved_mod_cfg_id), - 'selected_mod_id' => intval($selected_mod_id), - )); - if ($selected_mod_id <= 0) { - $order_failed = true; - $order_failure_reason = "Could not attach mod_cfg_id {$resolved_mod_cfg_id} to home #{$home_id}."; - $install_result = 'failed'; - $install_message = $order_failure_reason; - } else { - $db->updateGameModParams($max_players, '', BILLING_CPU_AFFINITY_NA, BILLING_NICE_DEFAULT, intval($home_id), intval($resolved_mod_cfg_id)); - $db->assignHomeTo("user", $user_id, intval($home_id), $access_rights); - $home_info = $db->getGameHome(intval($home_id)); - } - } - } - if (!$order_failed) { - $selected_mod_id = intval(gamemanager_choose_mod_id((array)$home_info, intval($selected_mod_id))); - $install_attempted = true; - billing_provision_trace('Calling gamemanager_trigger_update_install for existing home retry.', array( - 'exact_call' => "gamemanager_trigger_update_install(\$db, \$home_info, {$selected_mod_id}, ['settings' => ...])", - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_id' => intval($selected_mod_id), - 'home_info_summary' => billing_trace_home_info_summary((array)$home_info), - 'selected_settings' => billing_trace_settings_summary((array)$settings), - )); - $autoInstall = gamemanager_trigger_update_install( - $db, - (array)$home_info, - intval($selected_mod_id), - array('settings' => $settings) - ); - billing_provision_trace('gamemanager_trigger_update_install returned for existing home retry.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'selected_mod_id' => intval($selected_mod_id), - 'gamemanager_trigger_update_install_result' => $autoInstall, - )); - $selected_mod_id = intval($autoInstall['mod_id'] ?? $selected_mod_id); - $install_message = (string)($autoInstall['message'] ?? ''); - if (!empty($autoInstall['already_running'])) { - $install_result = 'already_running'; - } elseif (!empty($autoInstall['started'])) { - $install_result = 'started'; - } elseif (!empty($autoInstall['completed'])) { - $install_result = 'completed'; - } else { - $install_result = 'pending'; - } - if (empty($autoInstall['ok'])) { - if (stripos((string)($autoInstall['message'] ?? ''), 'Agent is offline') !== false) { - $order_failure_reason = billing_agent_offline_reason(intval($remote_server_id), (array)$home_info); - } - $order_failed = true; - $order_failure_reason = $order_failure_reason !== '' ? $order_failure_reason : ("Server files have not been installed yet. " . ($autoInstall['message'] ?? 'Auto install could not be started.')); - $install_result = 'failed'; - $install_message = $order_failure_reason; - } - } - } - // Set expiration date in panel database - // Status values: Active (provisioned & current), Invoiced (renewal invoice open), - // Expired (past due and awaiting deletion) - // end_date / next_invoice_date: when the next renewal invoice should be generated - if ($already_provisioned) - { - $existing_end = strtotime((string)($order['end_date'] ?? '')); - if ($existing_end === false || $existing_end <= 0) { - $existing_end = time(); - } - $end_date_str = date('Y-m-d H:i:s', $existing_end); - } - else - { - $qty_days = max(1, intval($order['qty'])) * 31; - if (empty($order['end_date']) || $order['end_date'] === NULL) { - $end_date = strtotime('+' . $qty_days . ' day'); - } else { - $current_end = strtotime($order['end_date']); - if ($current_end === false) { - $current_end = time(); - } - $end_date = strtotime('+' . $qty_days . ' day', $current_end); - } - } - if (!isset($end_date_str)) { - $end_date_str = date('Y-m-d H:i:s', $end_date); - } - - if ($home_id <= 0) { - $order_failed = true; - if ($order_failure_reason === '') { - $order_failure_reason = "No home_id was produced for order #{$order_id}."; - } - billing_provision_trace('Eligibility failure: provisioning finished without a valid home_id.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - )); - } - if ($home_id > 0 && empty($home_row_before)) { - $home_row_before = billing_get_server_home_row($db, $db_prefix, intval($home_id)); - } - - // Set order status to 'Active' (billing active even if install is pending) - $db->query("UPDATE `{$db_prefix}billing_orders` - SET status='Active' - WHERE order_id=".$db->realEscapeSingle($order_id)); - - // Set the order expiration / next renewal date - $db->query("UPDATE `{$db_prefix}billing_orders` - SET end_date='" . $db->realEscapeSingle($end_date_str) . "', - remote_control_password='" . $db->realEscapeSingle($remote_control_password) . "', - ftp_password='" . $db->realEscapeSingle($ftp_password) . "' - WHERE order_id=".$db->realEscapeSingle($order_id)); - - // Save home_id created by this order - $orderHomeUpdateOk = $db->query("UPDATE `{$db_prefix}billing_orders` - SET home_id='" . $db->realEscapeSingle($home_id) . "' WHERE order_id=".$db->realEscapeSingle($order_id)); - billing_provision_trace('Updated billing_orders.home_id.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'billing_orders_home_id_updated' => (bool)$orderHomeUpdateOk, - )); - - $invoiceHomeUpdateOk = $db->query("UPDATE `{$db_prefix}billing_invoices` - SET home_id=" . $db->realEscapeSingle($home_id) . ", - billing_status='Active', - status='paid' - WHERE order_id=" . $db->realEscapeSingle($order_id)); - billing_provision_trace('Updated billing_invoices.home_id and billing status.', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'billing_invoices_home_id_updated' => (bool)$invoiceHomeUpdateOk, - )); - - $db->query("UPDATE `{$db_prefix}billing_transactions` - SET home_id=" . $db->realEscapeSingle($home_id) . " - WHERE invoice_id IN (SELECT invoice_id FROM `{$db_prefix}billing_invoices` WHERE order_id=" . $db->realEscapeSingle($order_id) . ")"); - - if ($home_id > 0) { - $db->query("UPDATE `{$db_prefix}game_mods` - SET max_players=" . $db->realEscapeSingle($max_players) . " - WHERE home_id=" . $db->realEscapeSingle($home_id)); - } - - if ($home_id > 0) { - // Set billing_status, next_invoice_date, and server_expiration_date on server_homes. - // server_expiration_date must match end_date so the billing cron can determine - // when to suspend / delete the server. - $db->query("UPDATE `{$db_prefix}server_homes` - SET billing_status = 'Active', - next_invoice_date = '" . $db->realEscapeSingle($end_date_str) . "', - server_expiration_date = '" . $db->realEscapeSingle($end_date_str) . "', - billing_enabled = 1 - WHERE home_id = " . $db->realEscapeSingle($home_id)); - $home_row_after = billing_get_server_home_row($db, $db_prefix, intval($home_id)); - billing_provision_trace('Loaded server_homes row after billing linkage updates.', array( - 'server_homes_row_after' => $home_row_after, - )); - } - - $provisionContext = array( - 'order_id' => intval($order_id), - 'invoice_id' => intval($provision_invoice_id), - 'user_id' => intval($user_id), - 'service_id' => intval($service_id), - 'home_id' => intval($home_id), - 'home_cfg_id' => intval($home_cfg_id ?? 0), - 'config_xml' => (string)$selected_config_xml, - 'mod_id' => intval($selected_mod_id), - 'ip_id' => intval($selected_ip_id), - 'port' => intval($selected_port), - 'mechanism' => $install_mechanism, - 'install_result' => $order_failed ? 'failed' : (string)$install_result, - 'error' => $order_failed ? (string)$order_failure_reason : '', - 'message' => (string)$install_message, - 'skip_reason' => (string)$skip_reason, - ); - billing_write_provision_log($provisionContext); - $db->logger( - 'BILLING PROVISION RESULT order_id=' . intval($order_id) - . ' invoice_id=' . intval($provision_invoice_id) - . ' user_id=' . intval($user_id) - . ' service_id=' . intval($service_id) - . ' home_id=' . intval($home_id) - . ' home_cfg_id=' . intval($home_cfg_id ?? 0) - . ' config_xml=' . (string)$selected_config_xml - . ' mod_id=' . intval($selected_mod_id) - . ' ip_id=' . intval($selected_ip_id) - . ' port=' . intval($selected_port) - . ' mechanism=' . $install_mechanism - . ' install_result=' . ($order_failed ? 'failed' : (string)$install_result) - . ($order_failed ? ' error=' . (string)$order_failure_reason : '') - ); - - if ($order_failed) { - $failed_count++; - $failed_messages[] = "Order #{$order_id}: {$order_failure_reason}"; - $db->logger("Provisioning pending install for order #{$order_id}: {$order_failure_reason}"); - billing_provision_trace('END failure', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'end_reason' => $order_failure_reason, - )); - } else { - $provisioned_count++; - billing_provision_trace('END success', array( - 'home_id_after_creation_or_lookup' => intval($home_id), - 'install_result' => (string)$install_result, - )); - } - $order_results[] = array( - 'trace_id' => $trace_id, - 'order_id' => intval($order_id), - 'user_id' => intval($user_id), - 'service_id' => intval($service_id), - 'home_id' => intval($home_id), - 'mod_id' => intval($selected_mod_id), - 'install_result' => $order_failed ? 'failed' : (string)$install_result, - 'install_message' => (string)$install_message, - 'error' => $order_failed ? (string)$order_failure_reason : '', - 'trace_log_path' => billing_provision_trace_relative_path(), - ); - } catch (Throwable $e) { - $failed_count++; - $order_id = intval($order['order_id'] ?? 0); - $message = "Order #{$order_id} threw an exception during provisioning: " . $e->getMessage(); - $failed_messages[] = $message; - $db->logger('BILLING PROVISION EXCEPTION: ' . $message); - billing_provision_trace('Provisioning exception caught.', array('exception' => $e->getMessage())); - billing_provision_trace('END failure', array('end_reason' => $message)); - $order_results[] = array( - 'trace_id' => $trace_id, - 'order_id' => $order_id, - 'user_id' => intval($order['user_id'] ?? 0), - 'service_id' => intval($order['service_id'] ?? 0), - 'home_id' => intval($order['home_id'] ?? 0), - 'mod_id' => 0, - 'install_result' => 'failed', - 'install_message' => '', - 'error' => $e->getMessage(), - 'trace_log_path' => billing_provision_trace_relative_path(), - ); - } - unset($GLOBALS['BILLING_PROVISION_TRACE_CONTEXT']); - - } - - // Show results and redirect - if ($provisioned_count > 0) { - echo "
"; - echo "

Server Provisioning Complete

"; - echo "

Successfully provisioned $provisioned_count server(s). Your server(s) are now active.

"; - echo "
"; - if ($failed_count > 0) { - echo "
"; - echo "

{$failed_count} order(s) were linked but left pending install:

    "; - foreach ((array)$failed_messages as $failed_message) { - echo "
  • " . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "
  • "; - } - echo "
"; - } - echo "

View My Servers

"; - // Auto-redirect after 3 seconds - echo ""; - } else { - if ($failed_count > 0) { - echo "

No servers were auto-installed. Orders are active but pending install:

    "; - foreach ((array)$failed_messages as $failed_message) { - echo "
  • " . htmlspecialchars($failed_message, ENT_QUOTES, 'UTF-8') . "
  • "; - } - echo "
"; - } else { - echo "
"; - echo "

No servers to provision. All orders have already been processed.

"; - echo "
"; - } - echo "

View My Orders

"; - } - - } else { - billing_provision_trace('END failure: no paid orders matched provisioning request.', array( - 'caller_source' => $caller_source, - 'order_ids_received' => $orderIds, - )); - echo "
"; - echo "

No paid orders found to provision.

"; - echo "
"; - echo "

View My Orders

"; - $provisioned_count = 0; - $failed_count = 0; - } - $GLOBALS['BILLING_PROVISION_LAST_RESULT'] = array( - 'provisioned_count' => isset($provisioned_count) ? $provisioned_count : 0, - 'failed_count' => isset($failed_count) ? $failed_count : 0, - 'orders' => $processed_orders, - 'details' => $order_results, - 'trace_log_path' => billing_provision_trace_relative_path(), - 'trace_error' => $GLOBALS['BILLING_PROVISION_TRACE_ERROR'] ?? '', - ); - billing_provision_trace('END provisioning attempt', array( - 'provisioned_count' => intval($GLOBALS['BILLING_PROVISION_LAST_RESULT']['provisioned_count'] ?? 0), - 'failed_count' => intval($GLOBALS['BILLING_PROVISION_LAST_RESULT']['failed_count'] ?? 0), - )); -} -?> diff --git a/Panel/modules/billing/cron-shop.php b/Panel/modules/billing/cron-shop.php deleted file mode 100644 index 7b229c8b..00000000 --- a/Panel/modules/billing/cron-shop.php +++ /dev/null @@ -1,433 +0,0 @@ - Invoiced : next_invoice_date has arrived -> create {prefix}invoices record. - * B. Invoiced -> Expired : server_expiration_date passed and invoice unpaid. - * C. Expired -> Deleted : past delete_after_expired_days grace window -> remove server. - * D. Paid invoices (safety net): set server and invoice back to Active. - * - * Prerequisites (run once): - * sql/update_billing_status_active_invoiced_expired.sql - */ - -chdir(realpath(dirname(__FILE__))); /* Change to the billing module directory */ -chdir("../.."); /* Step back to the OGP/GSP web root */ - -error_reporting(E_ALL); -ini_set('display_errors', '1'); - -define("CONFIG_FILE", "includes/config.inc.php"); -require_once("includes/functions.php"); -require_once("includes/helpers.php"); -require_once("includes/html_functions.php"); -require_once("modules/config_games/server_config_parser.php"); -require_once("includes/lib_remote.php"); -require_once(CONFIG_FILE); - -// Connect using the panel's DB helper (provides $db with logger(), resultQuery(), etc.) -$db = createDatabaseConnection( - $db_type, $db_host, $db_user, $db_pass, $db_name, $table_prefix, - isset($db_port) ? $db_port : null -); - -$panel_settings = $db->getSettings(); -if (!empty($panel_settings['time_zone'])) { - date_default_timezone_set($panel_settings['time_zone']); -} - -$rundate = date('Y-m-d H:i:s'); -$db->logger("BILLING-CRON: ===== Lifecycle automation started at {$rundate} ====="); - -// ---------------------------------------------------------------- -// Load global billing config (grace_days, delete_after_expired_days) -// Falls back to safe defaults when {prefix}billing_config is empty. -// ---------------------------------------------------------------- -$cfg_rows = $db->resultQuery( - "SELECT * FROM {$table_prefix}billing_config WHERE game_key IS NULL AND enabled = 1 ORDER BY config_id ASC LIMIT 1" -); -$global_cfg = is_array($cfg_rows) && !empty($cfg_rows) ? $cfg_rows[0] : []; -$grace_days = intval($global_cfg['grace_days'] ?? 0); -$delete_after_days = intval($global_cfg['delete_after_expired_days'] ?? 7); -$default_rate_type = $global_cfg['rate_type'] ?? 'monthly'; -$default_price_player = floatval($global_cfg['price_per_player'] ?? 0.00); - -$db->logger("BILLING-CRON: Config => grace_days={$grace_days}, delete_after={$delete_after_days}, rate={$default_rate_type}"); - -// ====================================================================== -// STEP A - Active -> Invoiced -// Find billing-enabled servers whose next_invoice_date has arrived -// and that do not already have an open 'Invoiced' renewal invoice. -// ====================================================================== -$db->logger("BILLING-CRON: --- Step A: Active -> Invoiced ---"); - -$due_for_invoice = $db->resultQuery(" - SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id, - sh.next_invoice_date, sh.server_expiration_date, - bo.price, bo.invoice_duration, bo.qty, bo.order_id, - COALESCE(bs.price_monthly, 0) AS svc_price_monthly, - u.users_email, - CONCAT(COALESCE(u.users_fname,''), ' ', COALESCE(u.users_lname,'')) AS customer_name - FROM {$table_prefix}server_homes sh - LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main - LEFT JOIN {$table_prefix}billing_orders bo - ON bo.home_id = sh.home_id AND bo.status = 'Active' - LEFT JOIN {$table_prefix}billing_services bs ON bs.service_id = bo.service_id - WHERE sh.billing_enabled = 1 - AND sh.billing_status = 'Active' - AND sh.next_invoice_date IS NOT NULL - AND sh.next_invoice_date <= NOW() - AND NOT EXISTS ( - SELECT 1 FROM {$table_prefix}billing_invoices inv - WHERE inv.home_id = sh.home_id AND inv.billing_status = 'Invoiced' - ) - ORDER BY sh.home_id ASC -"); - -if (is_array($due_for_invoice)) { - foreach ($due_for_invoice as $srv) { - $home_id = intval($srv['home_id']); - $user_id = intval($srv['user_id']); - $home_name = $srv['home_name'] ?? 'Server #' . $home_id; - $qty = max(1, intval($srv['qty'] ?? 1)); - - // Normalise rate_type to the ENUM values used in {prefix}invoices - $raw_rate = strtolower($srv['invoice_duration'] ?? $default_rate_type); - $rate_map = ['day' => 'daily', 'month' => 'monthly', 'year' => 'yearly']; - $rate_type = $rate_map[$raw_rate] ?? $raw_rate; - - // Pricing: billing_config > billing_orders flat price - $price_per_player = $default_price_player; - $player_slots = max(0, intval($srv['qty'] ?? 0)); - $subtotal = $price_per_player * max(1, $player_slots); - if ($subtotal == 0.00 && floatval($srv['price'] ?? 0) > 0) { - $subtotal = floatval($srv['price']); - } - $total_due = $subtotal; - - // Calculate due_date: now + 1 billing period - $period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year']; - $due_date_ts = strtotime($period_map[$rate_type], time()); - $due_date = date('Y-m-d H:i:s', $due_date_ts); - - // Guard: skip if an invoice for this exact period already exists - $exists = $db->resultQuery(" - SELECT invoice_id FROM {$table_prefix}billing_invoices - WHERE home_id = {$home_id} - AND due_date = '" . $db->realEscapeSingle($due_date) . "' - LIMIT 1 - "); - if (is_array($exists) && !empty($exists)) { - $db->logger("BILLING-CRON: Step A - SKIP home {$home_id}: invoice for this period already exists"); - continue; - } - - // Create renewal invoice in {prefix}billing_invoices - $db->query(" - INSERT INTO {$table_prefix}billing_invoices - (home_id, user_id, due_date, billing_status, rate_type, - rate_per_player, players, qty, subtotal, total_due) - VALUES ( - {$home_id}, {$user_id}, - '" . $db->realEscapeSingle($due_date) . "', - 'Invoiced', - '" . $db->realEscapeSingle($rate_type) . "', - " . number_format($price_per_player, 2, '.', '') . ", - {$player_slots}, - {$qty}, - " . number_format($subtotal, 2, '.', '') . ", - " . number_format($total_due, 2, '.', '') . " - ) - "); - $new_invoice_id = $db->lastInsertId(); - - // Update server_homes: set Invoiced, store invoice id and expiration date - $db->query(" - UPDATE {$table_prefix}server_homes - SET billing_status = 'Invoiced', - server_expiration_date = '" . $db->realEscapeSingle($due_date) . "', - last_invoice_id = " . intval($new_invoice_id) . " - WHERE home_id = {$home_id} - "); - - $db->logger("BILLING-CRON: Step A - INVOICED home {$home_id} (invoice #{$new_invoice_id}, due {$due_date})"); - - // Send renewal notice - if (!empty($srv['users_email'])) { - $settings = $db->getSettings(); - $subject = "Renewal Invoice for {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel'); - $message = "Your server '{$home_name}' (ID: {$home_id}) has a renewal invoice due on " - . date('F j, Y', $due_date_ts) . "." - . "

Amount Due: \$" . number_format($total_due, 2) - . "
Due Date: " . date('F j, Y', $due_date_ts) - . "

Please log in to pay your invoice and keep your server active." - . "

Thank you!"; - if (!mymail($srv['users_email'], $subject, $message, $settings)) { - $db->logger("BILLING-CRON: Step A - Email FAILED for home {$home_id}"); - } - } - } -} - -// ====================================================================== -// STEP B - Invoiced -> Expired -// Servers whose expiration date has passed and whose last invoice -// is still unpaid. -// ====================================================================== -$db->logger("BILLING-CRON: --- Step B: Invoiced -> Expired (grace_days={$grace_days}) ---"); - -$past_due = $db->resultQuery(" - SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id, - sh.last_invoice_id, sh.server_expiration_date, - u.users_email - FROM {$table_prefix}server_homes sh - LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main - WHERE sh.billing_enabled = 1 - AND sh.billing_status = 'Invoiced' - AND sh.server_expiration_date IS NOT NULL - AND DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$grace_days} DAY) - AND ( - sh.last_invoice_id IS NULL - OR EXISTS ( - SELECT 1 FROM {$table_prefix}billing_invoices inv - WHERE inv.invoice_id = sh.last_invoice_id - AND inv.billing_status = 'Invoiced' - AND inv.paid_date IS NULL - ) - ) - ORDER BY sh.home_id ASC -"); - -if (is_array($past_due)) { - foreach ($past_due as $srv) { - $home_id = intval($srv['home_id']); - $last_invoice_id = intval($srv['last_invoice_id'] ?? 0); - - // Mark server Expired - $db->query(" - UPDATE {$table_prefix}server_homes - SET billing_status = 'Expired' - WHERE home_id = {$home_id} - "); - - // Mark matching invoice Expired (if still unpaid) - if ($last_invoice_id > 0) { - $db->query(" - UPDATE {$table_prefix}billing_invoices - SET billing_status = 'Expired' - WHERE invoice_id = {$last_invoice_id} - AND billing_status = 'Invoiced' - AND paid_date IS NULL - "); - } - - $db->logger("BILLING-CRON: Step B - EXPIRED home {$home_id}"); - - // Notify user - if (!empty($srv['users_email'])) { - $settings = $db->getSettings(); - $home_name = $srv['home_name'] ?? 'Server #' . $home_id; - $subject = "Server Expired - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel'); - $message = "Your server '{$home_name}' (ID: {$home_id}) has expired due to non-payment." - . "

The server will be permanently deleted in {$delete_after_days} day(s) if payment is not received." - . "

Please log in and pay your outstanding invoice to restore service." - . "

Thank you."; - if (!mymail($srv['users_email'], $subject, $message, $settings)) { - $db->logger("BILLING-CRON: Step B - Email FAILED for home {$home_id}"); - } - } - } -} - -// ====================================================================== -// STEP C - Expired -> Deleted -// Servers that have been Expired longer than delete_after_expired_days. -// ====================================================================== -$db->logger("BILLING-CRON: --- Step C: Expired -> Deleted (window={$delete_after_days}d) ---"); - -$to_delete = $db->resultQuery(" - SELECT sh.home_id, sh.home_name, sh.user_id_main AS user_id, - sh.server_expiration_date, - u.users_email - FROM {$table_prefix}server_homes sh - LEFT JOIN {$table_prefix}users u ON u.user_id = sh.user_id_main - WHERE sh.billing_enabled = 1 - AND sh.billing_status = 'Expired' - AND sh.server_expiration_date IS NOT NULL - AND DATE(sh.server_expiration_date) < DATE_SUB(CURDATE(), INTERVAL {$delete_after_days} DAY) - ORDER BY sh.home_id ASC -"); - -if (is_array($to_delete)) { - foreach ($to_delete as $srv) { - $home_id = intval($srv['home_id']); - $user_id = intval($srv['user_id']); - $home_name = $srv['home_name'] ?? 'Server #' . $home_id; - - // Fetch home info for remote deletion - $home_info = $db->getGameHomeWithoutMods($home_id); - if ($home_info) { - $server_info = $db->getRemoteServerById($home_info['remote_server_id']); - if ($server_info) { - $remote = new OGPRemoteLibrary( - $server_info['agent_ip'], - $server_info['agent_port'], - $server_info['encryption_key'], - $server_info['timeout'] - ); - - // Stop the running server process - $server_xml = read_server_config(SERVER_CONFIG_LOCATION . "/" . $home_info['home_cfg_file']); - $control_type = isset($server_xml->control_protocol_type) - ? (string)$server_xml->control_protocol_type : ""; - $addresses = $db->getHomeIpPorts($home_id); - foreach ((array)$addresses as $addr) { - $remote->remote_stop_server( - $home_id, $addr['ip'], $addr['port'], - $server_xml->control_protocol, - $home_info['control_password'], - $control_type, - $home_info['home_path'] - ); - } - - // Disable FTP - $ftp_login = !empty($home_info['ftp_login']) ? $home_info['ftp_login'] : $home_id; - $remote->ftp_mgr("userdel", $ftp_login); - $db->changeFtpStatus('disabled', $home_id); - - // Unassign from user - $db->unassignHomeFrom("user", $user_id, $home_id); - - // Delete home record from panel DB - $db->deleteGameHome($home_id); - - // Remove server files on remote agent - $remote->remove_home($home_info['home_path']); - - // Drop any per-server database/user accounts - @$db->query("DROP USER 'user_{$home_id}'@'%'"); - @$db->query("DROP USER 'user_{$home_id}'@'localhost'"); - @$db->query("DROP USER 'server_{$home_id}'@'%'"); - @$db->query("DROP USER 'server_{$home_id}'@'localhost'"); - @$db->query("DROP DATABASE IF EXISTS user_{$home_id}"); - @$db->query("DROP DATABASE IF EXISTS server_{$home_id}"); - } else { - $db->logger("BILLING-CRON: Step C - WARNING: no remote server info for home {$home_id}; removing panel record only"); - $db->deleteGameHome($home_id); - } - } else { - $db->logger("BILLING-CRON: Step C - WARNING: home {$home_id} not found in panel DB (already removed)"); - } - - // Mark billing_orders record as Expired and clear home_id reference - $db->query(" - UPDATE {$table_prefix}billing_orders - SET status = 'Expired', - home_id = '0' - WHERE home_id = '{$home_id}' - "); - - // Mark any open billing_invoices for this home as Expired - $db->query(" - UPDATE {$table_prefix}billing_invoices - SET billing_status = 'Expired' - WHERE home_id = {$home_id} - AND billing_status = 'Invoiced' - "); - - $db->logger("BILLING-CRON: Step C - DELETED home {$home_id}"); - - // Notify user - if (!empty($srv['users_email'])) { - $settings = $db->getSettings(); - $subject = "Server Permanently Deleted - {$home_name} - " . ($panel_settings['panel_name'] ?? 'Game Server Panel'); - $message = "Your server '{$home_name}' (ID: {$home_id}) has been permanently deleted." - . "

The server expired and was removed after the grace period." - . "

If this was an error, contact us immediately - we may be able to restore from backup." - . "

Thank you for being a customer. We hope to serve you again."; - if (!mymail($srv['users_email'], $subject, $message, $settings)) { - $db->logger("BILLING-CRON: Step C - Email FAILED for home {$home_id}"); - } - } - } -} - -// ====================================================================== -// STEP D - Paid invoice safety net -// If a payment was recorded on a {prefix}invoices row but the -// server_home was not updated (e.g. race condition at capture time), -// correct it here so the server is restored to Active. -// ====================================================================== -$db->logger("BILLING-CRON: --- Step D: Paid invoice safety-net ---"); - -$paid_invoices = $db->resultQuery(" - SELECT inv.invoice_id, inv.home_id, inv.rate_type, - sh.billing_status - FROM {$table_prefix}billing_invoices inv - INNER JOIN {$table_prefix}server_homes sh ON sh.home_id = inv.home_id - WHERE inv.billing_status = 'Invoiced' - AND sh.billing_status = 'Invoiced' - AND (inv.paid_date IS NOT NULL OR inv.payment_txid IS NOT NULL) - ORDER BY inv.invoice_id ASC -"); - -if (is_array($paid_invoices)) { - foreach ($paid_invoices as $inv) { - $home_id = intval($inv['home_id']); - $invoice_id = intval($inv['invoice_id']); - $rate_type = $inv['rate_type'] ?? 'monthly'; - - // Calculate next_invoice_date based on rate_type - $period_map = ['daily' => '+1 day', 'monthly' => '+1 month', 'yearly' => '+1 year']; - $next_invoice_date = date('Y-m-d H:i:s', strtotime($period_map[$rate_type] ?? '+1 month')); - - $db->query(" - UPDATE {$table_prefix}billing_invoices - SET billing_status = 'Active' - WHERE invoice_id = {$invoice_id} - "); - - $db->query(" - UPDATE {$table_prefix}server_homes - SET billing_status = 'Active', - next_invoice_date = '" . $db->realEscapeSingle($next_invoice_date) . "', - server_expiration_date = NULL - WHERE home_id = {$home_id} - "); - - $db->logger("BILLING-CRON: Step D - RESTORED home {$home_id} to Active via paid invoice #{$invoice_id}"); - } -} - -$db->logger("BILLING-CRON: ===== Lifecycle automation completed at " . date('Y-m-d H:i:s') . " ====="); diff --git a/Panel/modules/billing/css/header.css b/Panel/modules/billing/css/header.css deleted file mode 100644 index f0970baf..00000000 --- a/Panel/modules/billing/css/header.css +++ /dev/null @@ -1,423 +0,0 @@ -/* Global font family - legible sans-serif stack */ -html, -body { - width: 100%; - max-width: 100%; - overflow-x: hidden; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; -} - -#gsw-site { - width: 100%; - max-width: 100%; - overflow-x: hidden; -} - -.gsw-top{display:flex;align-items:center;gap:12px;padding:12px 24px;background:#fff;border-bottom:1px solid rgba(0,0,0,0.05);} -.gsw-top img{height:40px;width:auto;display:block} -.gsw-top .gsw-site-name{font-weight:700;font-size:1.1rem;color:#333} -@media(max-width:480px){.gsw-top{padding:10px}.gsw-top img{height:32px}.gsw-top .gsw-site-name{font-size:1rem}} - -/* Header: two-row layout with left/right divs on top row */ -.gsw-header{display:flex;flex-direction:column;align-items:stretch;padding:0;background:transparent;margin-bottom:18px;} - -/* Top row: contains left (logo/title) and right (login) divs as separate blocks */ -#gsw-site .gsw-header-top{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:12px 20px;background:#0b3b6f !important;backdrop-filter:blur(6px);box-shadow:0 2px 6px rgba(0,0,0,0.18);width:100%;max-width:100%;} - -/* Left div: logo + title, takes up available space */ -#gsw-site .gsw-header-left{flex:1 1 auto;display:flex;align-items:center;font-weight:700;font-size:1.4rem;color:#fff;padding-left:8px;} - -/* Right div: login/logout button area, shrinks to content */ -#gsw-site .gsw-header-right{flex:0 0 auto;display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-right:8px;} - -.gsw-logo{height:48px;width:auto;margin-right:12px;display:block} -.gsw-logo-link{display:flex;align-items:center;gap:10px;color:#fff;text-decoration:none} -.gsw-header-left a{color:#fff;text-decoration:none;} - -/* Bottom row: centered navigation menu */ -#gsw-site .gsw-header-bottom{display:flex;justify-content:center;padding:10px 20px;background:#0b3b6f !important;width:100%;max-width:100%;} -.gsw-header-nav{display:flex;gap:22px;align-items:center;max-width:100%;} -.gsw-nav-link{color:#fff;text-decoration:none;font-size:0.98rem;transition:opacity 0.2s;padding:6px 8px;border-radius:6px;} -.gsw-nav-link:hover{opacity:0.9;text-decoration:underline;background:rgba(255,255,255,0.03);} -/* My Account link styling - larger font in middle of menu */ -.gsw-nav-link-myaccount{font-size:1.15rem;font-weight:600;padding:6px 12px;} - -.gsw-user-info{color:#fff;font-size:0.95rem;margin-right:8px;} - -/* Login/Logout button with gradient */ -#gsw-site .gsw-header-btn, -#gsw-site a.gsw-header-btn{padding:10px 18px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) !important;color:#fff !important;border-radius:8px;text-decoration:none;font-weight:700;transition:transform 0.2s;border:none;display:inline-block;cursor:pointer;} -#gsw-site .gsw-header-btn:hover, -#gsw-site a.gsw-header-btn:hover{transform:translateY(-2px);text-decoration:none;color:#fff !important;} -@media(max-width:768px){ - .gsw-header{flex-direction:column;gap:12px;} - .gsw-header-nav{flex-wrap:wrap;justify-content:center;} -} - -/* Banner styling (index only) */ -.gsw-banner{width:100%;text-align:center;margin-bottom:18px} -.gsw-banner img{max-width:100%;height:auto;display:inline-block} - -/* Footer styles: ultra-specific to override any theme CSS */ -html body #gsw-site footer.gsw-footer{background:#0b3b6f !important;color:#fff;padding:18px 12px;text-align:center;margin-top:28px;} -#gsw-site footer.gsw-footer a{color:#cfe6ff;text-decoration:none;} -#gsw-site footer.gsw-footer a:hover{text-decoration:underline;} - -/* Page color scheme: prefer dark text on light backgrounds by default */ -/* Dark site theme: dark background with light text */ -body { color: #fff; background: #0b1020; } - -/* Make links readable on dark background */ -a { color: #7fb3ff; } -/* But override for styled buttons/links inside our site wrapper */ -#gsw-site a.gsw-btn, -#gsw-site a.gsw-btn-secondary, -#gsw-site a.gsw-header-btn, -#gsw-site .gsw-nav-link{color:#fff !important;} - -/* Form inputs: light text on darker inputs by default */ -input, textarea, select, button { color: #fff; background: #11141f; border: 1px solid rgba(255,255,255,0.06); } - -.cart-badge{display:inline-block;background:#ff3b30;color:#fff;font-size:0.8rem;padding:2px 6px;border-radius:12px;margin-left:6px;vertical-align:middle} -.site-panel{width:100%; max-width:1000px; margin:auto; padding:1rem; background:rgba(0,0,0,0.25); border-radius:0.75rem;} -.site-panel-title{font-size:1.5rem; font-weight:bold; color:#fff; margin-bottom:1.5rem; text-align:center} -.cart-table{border-collapse:separate; border-spacing:0; width:100%; color:#fff} -.cart-table thead{background:rgba(255,255,255,0.03)} -.cart-table th, .cart-table td{padding:1rem 1.5rem; text-align:left; border-bottom:1px solid rgba(255,255,255,0.03)} -.cart-total-row{background:transparent; font-weight:bold} -.cart-total-label{padding:1rem 1.5rem; text-align:right; border-top:2px solid rgba(255,255,255,0.06); font-weight:600; color:#fff} -.cart-total-value{padding:1rem 1.5rem; text-align:left; border-top:2px solid rgba(255,255,255,0.06); font-weight:600; color:#fff; font-size:1.1rem} - -/* Utility classes */ -.container-wide{width:100%; max-width:1000px; margin:28px auto; padding-inline:12px; box-sizing:border-box;} -.panel{background:rgba(0,0,0,0.25); padding:16px; border-radius:8px} -.muted{color:rgba(255,255,255,0.6)} -.center{text-align:center} -.pad-40{padding:40px} -.btn-danger{background:#ef4444;color:#fff;border:none;padding:6px 10px;border-radius:6px} -.btn-primary{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;border:none;padding:8px 14px;border-radius:8px;font-weight:700} - -/* Primary gradient button for links and buttons */ -#gsw-site .gsw-btn, -#gsw-site a.gsw-btn, -#gsw-site button.gsw-btn{display:inline-block;padding:12px 24px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%) !important;color:#fff !important;text-decoration:none;border-radius:8px;font-weight:600;transition:transform 0.2s;border:none;cursor:pointer;} -#gsw-site .gsw-btn:hover, -#gsw-site a.gsw-btn:hover, -#gsw-site button.gsw-btn:hover{transform:translateY(-2px);color:#fff !important;text-decoration:none;} - -/* Renew button: slightly smaller but matching gradient, used on My Account cards */ -#gsw-site .renew-btn, #gsw-site a.renew-btn, #gsw-site button.renew-btn{ - display:inline-block; - padding:8px 14px; - background:linear-gradient(135deg,#f59e0b 0%,#ef4444 100%) !important; - color:#fff !important; - text-decoration:none; - border-radius:8px; - font-weight:700; - transition:transform 0.12s; - border:none; - cursor:pointer; -} -#gsw-site .renew-btn:hover, #gsw-site a.renew-btn:hover, #gsw-site button.renew-btn:hover{transform:translateY(-2px);} - -#gsw-site .gsw-btn-secondary, -#gsw-site a.gsw-btn-secondary{display:inline-block;padding:10px 16px;background:rgba(255,255,255,0.06);color:#fff !important;text-decoration:none;border-radius:8px;border:1px solid rgba(255,255,255,0.06);cursor:pointer;} -#gsw-site .gsw-btn-secondary:hover, -#gsw-site a.gsw-btn-secondary:hover{color:#fff !important;text-decoration:none;} -.float-left{float:left} -.clearfix::after{content:"";display:table;clear:both} -.table-compact th,.table-compact td{padding:0.5rem} - -/* Small spacing utilities used by a few pages */ -.mb-18{margin-bottom:18px} -.mt-6{margin-top:6px} -.mt-12{margin-top:12px} - -/* Padding helper used where a wider card/panel is desired */ -.p-30-20{padding:30px 20px} - -/* Decorative container used in a few places */ -.decorative-bottom{border:4px solid transparent;border-bottom:25px solid transparent} - -/* Inline form helper (used for small inline forms inside table cells) */ -.inline-form{margin:0;display:inline} - -/* Small square button (used for delete icons) */ -.btn-square{width:2rem;height:2rem;display:inline-flex;align-items:center;justify-content:center;font-weight:bold;border-radius:0.25rem;border:none} - -.text-right{text-align:right} -.text-danger{color:#ef4444} -.text-center{text-align:center} - -/* small helpers for admin server list inputs */ -.min-w-260{min-width:260px} -.min-w-240{min-width:240px} -.w-90{width:90px} -.img-preview{max-height:48px; max-width:120px; border:1px solid #eee; display:block} -.loc-label{border:1px solid #eee;border-radius:6px;padding:6px 8px; display:inline-flex; align-items:center} -.small-muted{color:#777;font-size:12px;margin-top:2px} - -/* PayPal status */ -.pp-status{margin-top:12px;font:14px system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif} - -/* AI UI helpers */ -.ai-container{max-width:760px; margin:20px auto; font-family:Arial, sans-serif} -.ai-panel{margin:10px 0; padding:8px; border-radius:6px} -.ai-alert{margin:10px 0; padding:8px; border-radius:6px; border:1px solid #c00} -.ai-textarea{width:100%; padding:6px} -.ai-message{margin-top:16px; padding:10px; border:1px solid #ccc; border-radius:8px} -.ai-msg-title{font-weight:bold} -.ai-msg-meta{margin-top:6px; font-size:12px} -.flex-gap-wrap{display:flex;flex-wrap:wrap;gap:10px} -.table-center{text-align:center;width:100%;border-collapse:collapse} -.tb-row-bottom{border-bottom:1px solid #f0f0f0;padding:8px 6px;text-align:left} -.locs-box{display:flex;flex-wrap:wrap;gap:8px} -.mb-12{margin-bottom:12px} -.mt-10{margin-top:10px} -.mt-14{margin-top:14px} -.mt-20{margin-top:20px} -.mt-8{margin-top:8px} -.btn-small{padding:3px 8px;font-size:12px} -.mr-6{margin-right:6px} -.ml-8{margin-left:8px} -.flex-row-gap{display:flex;gap:8px;align-items:center} - -/* Account page styles */ -.account-container{max-width:1000px;margin:20px auto;padding:20px} -.account-section{background:rgba(0,0,0,0.25);padding:20px;border-radius:8px;margin-bottom:20px} -.account-section h2{margin:0 0 15px 0;font-size:1.3rem;color:#fff;border-bottom:2px solid rgba(255,255,255,0.1);padding-bottom:10px} -.account-info-grid{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px} -.account-info-item{padding:10px;background:rgba(255,255,255,0.03);border-radius:6px} -.account-info-label{font-weight:600;color:rgba(255,255,255,0.7);font-size:0.9rem;margin-bottom:5px} -.account-info-value{color:#fff;font-size:1rem} -.account-edit-summary{cursor:pointer;color:#667eea;font-weight:600;margin-top:10px} - -/* Form styles */ -.form-group{margin-bottom:15px} -.form-group label{display:block;margin-bottom:5px;color:#fff;font-weight:500} -.form-group input{width:100%;padding:10px;border:1px solid rgba(255,255,255,0.1);border-radius:6px;background:rgba(0,0,0,0.3);color:#fff} - -/* Alert messages */ -.alert{padding:12px 16px;border-radius:8px;margin-bottom:20px;font-size:0.95rem} -.alert-error{background-color:rgba(255,0,0,0.2);border:1px solid rgba(255,0,0,0.3);color:#ffcccc} -.alert-success{background-color:rgba(0,255,0,0.2);border:1px solid rgba(0,255,0,0.3);color:#ccffcc} - -/* Server item cards */ -.server-item{background:rgba(255,255,255,0.03);padding:15px;border-radius:6px;margin-bottom:10px;border-left:3px solid #667eea} -.server-name{font-size:1.1rem;font-weight:600;color:#fff;margin-bottom:8px} -.server-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:10px} -.server-detail{font-size:0.9rem} -.server-detail-label{color:rgba(255,255,255,0.6)} -.server-detail-value{color:#fff;font-weight:500} - -/* Invoice items */ -.invoice-item{background:rgba(255,255,255,0.03);padding:12px 15px;border-radius:6px;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center} -.invoice-id{font-weight:600;color:#fff} -.invoice-amount{color:#10b981;font-weight:600} -.invoice-status{padding:4px 10px;border-radius:4px;font-size:0.85rem;font-weight:600} -.invoice-status-paid{background:rgba(16,185,129,0.2);color:#10b981} -.invoice-status-pending{background:rgba(245,158,11,0.2);color:#f59e0b} -.invoice-status-expired{background:rgba(239,68,68,0.2);color:#ef4444} -.invoice-date{color:rgba(255,255,255,0.6);font-size:0.9rem} - -/* Login placeholder for non-logged-in users */ -.login-placeholder{padding:12px;background:rgba(255,255,255,0.03);border-radius:8px;color:#fff} -.login-placeholder a{color:#cfe6ff;text-decoration:underline} - -/* No data state */ -.no-data{text-align:center;padding:30px;color:rgba(255,255,255,0.6)} - -/* Service description text */ -.service-desc{color:gray;width:230px} -.service-desc-wide{color:gray;width:280px} -.service-textarea{resize:none;width:230px;height:132px} - -/* Admin helpers */ -.admin-note{font-size:11px;color:#666;margin-top:4px} -.admin-flex-wrap{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px} - -@media (max-width:768px){ - .account-info-grid{grid-template-columns:1fr} -} - -/* Responsive improvements for mobile devices */ -@media (max-width: 600px) { - /* Stack header top and make logo smaller */ - #gsw-site .gsw-header-top{flex-direction:column;align-items:flex-start;padding:10px 12px} - #gsw-site .gsw-header-left{width:100%;justify-content:flex-start;padding-left:4px} - .gsw-logo{height:40px} - .gsw-site-name{font-size:1rem} - - /* Make header right area flow beneath the logo */ - #gsw-site .gsw-header-right{width:100%;margin-top:8px;justify-content:flex-start;padding-right:4px;gap:8px} - - /* Navigation: wrap and stack for easier tapping */ - #gsw-site .gsw-header-bottom{padding:8px 12px} - .gsw-header-nav{flex-direction:column;align-items:stretch;gap:10px;width:100%;max-width:100%} - .gsw-nav-link{display:block;padding:12px 10px;border-radius:8px} - .gsw-nav-link-myaccount{font-size:1rem} - - /* Make main panel use full width with reduced padding */ - .site-panel{padding:0.75rem;margin:8px;border-radius:0.5rem;max-width:100%} - - /* Tables and cart spacing adjustments */ - .cart-table th, .cart-table td{padding:0.6rem 0.8rem} - - /* Buttons become full-width for easier tapping on small screens */ - #gsw-site .gsw-btn, #gsw-site a.gsw-btn, #gsw-site button.gsw-btn, #gsw-site .gsw-header-btn{ - width:100%;display:block;text-align:center;padding:12px;border-radius:10px - } - - /* Server cards: stack details and move actions below */ - .server-details{grid-template-columns:1fr} - .server-actions{margin-top:12px;display:block} - .server-item{padding:12px} - - /* Forms: make inputs and action buttons full width */ - .form-group input, .form-group textarea, .form-group select{width:100%;box-sizing:border-box;max-width:100%} - - /* Invoice items: stack label and amount for readability */ - .invoice-item{flex-direction:column;align-items:flex-start;gap:8px} - .invoice-amount{font-size:1rem} -} - -@media (max-width:420px){ - /* Extra small devices: tighten spacing, smaller fonts */ - .gsw-logo{height:34px} - .gsw-site-name{font-size:0.95rem} - .site-panel-title{font-size:1.25rem} - .server-name{font-size:1rem} - .account-section h2{font-size:1.1rem} - - /* Reduce large paddings that consume screen real estate */ - .panel{padding:10px} - .form-group{margin-bottom:12px} - .btn-primary, .btn-small{padding:10px} -} - -/* Server status and utility classes */ -#gsw-site .text-success { - color: #10b981 !important; - font-weight: 600 !important; -} - -#gsw-site .text-danger { - color: #ef4444 !important; - font-weight: 600 !important; -} - -#gsw-site .text-muted { - color: rgba(255,255,255,0.7) !important; -} - -#gsw-site .text-center { - text-align: center !important; -} - -#gsw-site .mb-20 { - margin-bottom: 20px !important; -} - -#gsw-site .server-notes { - padding-left: 40px !important; - font-size: 0.9rem !important; - color: rgba(255,255,255,0.7) !important; -} - -/* Status badges */ -#gsw-site .status-badge { - display: inline-block; - padding: 4px 12px; - border-radius: 12px; - font-size: 0.85rem; - font-weight: 600; - text-transform: uppercase; -} - -#gsw-site .status-online { - background-color: rgba(16, 185, 129, 0.2); - color: #10b981; -} - -#gsw-site .status-offline { - background-color: rgba(239, 68, 68, 0.2); - color: #ef4444; -} - -#gsw-site .status-maintenance { - background-color: rgba(251, 191, 36, 0.2); - color: #fbbf24; -} - -#gsw-site .status-unknown { - background-color: rgba(156, 163, 175, 0.2); - color: #9ca3af; -} - -/* Form radio labels in renewal page */ -#gsw-site .form-group label { - display: block; - margin-bottom: 10px; - cursor: pointer; - padding: 12px; - border: 2px solid #e1e8ed; - border-radius: 8px; - background: rgba(255,255,255,0.05); - transition: background 0.2s ease; -} - -#gsw-site .form-group label:hover { - background: rgba(255,255,255,0.1); -} - -/* Ensure header-right sits flush to the far right */ -#gsw-site .gsw-header-top .gsw-header-right{margin-left:auto} - -/* User dropdown menu (small, CSS-only). Uses :focus-within and :hover for accessibility */ -.gsw-user-menu{position:relative;display:inline-block} -.gsw-user-link{color:#fff;text-decoration:none;font-weight:600;padding:8px 12px;display:inline-block} -.gsw-user-caret{margin-left:6px;font-size:0.85rem;opacity:0.85} -.gsw-user-dropdown{display:none;position:absolute;right:0;top:calc(100% + 8px);background:rgba(255,255,255,0.06);backdrop-filter:blur(6px);border-radius:8px;padding:8px;min-width:160px;box-shadow:0 8px 24px rgba(0,0,0,0.35);z-index:60} -.gsw-user-dropdown-item{display:block;color:#fff;text-decoration:none;padding:8px 10px;border-radius:6px;margin:2px 0} -.gsw-user-dropdown-item:hover{background:rgba(255,255,255,0.03)} -.gsw-user-menu:hover .gsw-user-dropdown, .gsw-user-menu:focus-within .gsw-user-dropdown{display:block} - -/* Mobile: make dropdown inline under header and full-width */ -@media (max-width:600px){ - .gsw-user-dropdown{position:static;top:auto;right:auto;margin-top:8px;background:rgba(255,255,255,0.03);width:100%;box-shadow:none;padding:6px} - .gsw-user-menu{width:100%} - .gsw-user-link{width:100%;display:flex;justify-content:space-between;padding:12px} -} - -/* Prevent serverlist images from overflowing on small screens */ -#gsw-site .server-item img, -#gsw-site .game-thumb, -#gsw-site .server-card img, -.server-list img{max-width:100%;height:auto;display:block;object-fit:cover} - -#gsw-site img, -#gsw-site video, -#gsw-site iframe, -#gsw-site canvas, -#gsw-site svg { - max-width: 100%; - height: auto; -} - -#gsw-site table { - max-width: 100%; -} - -#gsw-site input, -#gsw-site select, -#gsw-site textarea, -#gsw-site button, -#gsw-site .btn, -#gsw-site .gsw-btn, -#gsw-site .gsw-btn-secondary { - max-width: 100%; - box-sizing: border-box; -} diff --git a/Panel/modules/billing/diag_remote.php b/Panel/modules/billing/diag_remote.php deleted file mode 100644 index 8b9c5b41..00000000 --- a/Panel/modules/billing/diag_remote.php +++ /dev/null @@ -1,77 +0,0 @@ - - diff --git a/Panel/modules/billing/docs.php b/Panel/modules/billing/docs.php deleted file mode 100644 index 3dc6bbf8..00000000 --- a/Panel/modules/billing/docs.php +++ /dev/null @@ -1,431 +0,0 @@ - $folder, - 'name' => $displayName, - 'description' => $metadata['description'] ?? '', - 'category' => trim($metadata['category'] ?? 'other'), - 'order' => $metadata['order'] ?? 999, - 'icon' => $icon - ]; - } - - // Sort alphabetically by name within categories - usort($categories, function($a, $b) { - if ($a['category'] !== $b['category']) { - // Keep category grouping (game, mods, other) - return strcmp($a['category'], $b['category']); - } - // Sort alphabetically by name (case-insensitive) - return strcasecmp($a['name'], $b['name']); - }); - - return $categories; -} - -// Get all categories -$categories = getDocCategories($docsDir); - -// Group by category -$grouped = []; -foreach ((array)$categories as $cat) { - $category = $cat['category']; - if (!isset($grouped[$category])) { - $grouped[$category] = []; - } - $grouped[$category][] = $cat; -} - -// Category labels - can be extended via JSON -$categoryLabels = [ - 'todo' => 'TODO', - 'game' => 'Game Servers', - 'mods' => 'Mods & Plugins', - 'panel' => 'Panel Documentation', - 'troubleshooting' => 'Troubleshooting', - 'other' => 'Other' -]; - -// Define category display order - $categoryOrder = ['todo', 'panel', 'game', 'mods', 'troubleshooting', 'other']; - -// Sort categories by defined order -uksort($grouped, function($a, $b) use ($categoryOrder) { - $posA = array_search($a, $categoryOrder); - $posB = array_search($b, $categoryOrder); - - // If not in order array, put at end - if ($posA === false) $posA = 999; - if ($posB === false) $posB = 999; - - return $posA - $posB; -}); -?> - - - - - - - - - <?php echo htmlspecialchars('Documentation - GSP', ENT_QUOTES, 'UTF-8'); ?> - - - - - - - -
- - - ← Back to Documentation List - -
- Documentation not found.

'; - } - ?> -
- - - -
-

Documentation

-

Browse our comprehensive documentation for game servers, panel features, and troubleshooting guides.

-
- - -
-

No documentation available yet. Documentation folders should contain:

-
    -
  • index.php - The documentation content
  • -
  • metadata.json - Category and ordering information
  • -
  • icon.png or icon.jpg - Category icon
  • -
-
- - - - - $docs): ?> - - - - -
- - diff --git a/Panel/modules/billing/docs/7daystodie/icon.jpg b/Panel/modules/billing/docs/7daystodie/icon.jpg deleted file mode 100644 index 923a1562a4282d618cb5aebaf43ea91c6bc595e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40986 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

#kd0QT0(JebvtJ%z8+%5esN&N7B;~jR^ zg)X}{cYk)qs$Fl|5S3juR%LeaaV?Gi089t&`2PUnAO4}8{{X#&D|Pn2q@*)0s?6eb zar^e%8sLy^i(NiH_=7KKXa4}3C22ThlS;IcyGtplaLnCFCig$D@y3N3XD=p3;P2F* zB`eDj+NQ&TN0+7;k%ik3@@%!+QO0K*OjnV)xxYY9zn&=%$m$v_eO;Q3F{Y|TG`-tY ze@2QjdhQ-3A3=NvJe3RiV#<3#z>{`?lM^3*9CaPzC$gzHmTJnoaoZTXS*oSYDk#<| zrI~1%nfJ-L9gjd4r-#=U!ZPBOw_Z+v&8dWWoW)kb*pd0+50lX2+z>Wu8zBSBo8k;l zVAOBi9mK4#2;N zhMwZ&afBU$J!LrV+QXber1Vv~?TMDcP*N1;G33Y0oBCjD;=9r>y{vtagSkCFRpljK zwY&Npsh(EVHhq{$Jc{B;3XQF~YzeSa{`%v?zu$Upqc>NYyI=ITRQ!M0`kD{y&$hjf z?C-PQ&QkZ#+jUJz#xEnBYNu&c@|;qzT?7lVqON7Uaf7rI)x5Fix-4*U%hu*!3~s;q zK0oq(lkGQM)Wa{c{>^(cBU!Sz_q4dh6;f?gD_8K{=s%;hZBA`J%}pNIK-{~>41DfA zG1B%&vU26fx}55lD*V5y^gKuH5B7o4bX_FL%b|8A^OC!(Z&ehD9Xk&ZRk1g>C-oSpxUV}Y+ZOZv9X&qxl7)(?D5P4z-0^RGTxPhi8fA4o zJkGUHPR!BNC=4VW2qNFh8fj^-0@$fnmAy4YixV;eMZzAV{BYFt0SYQsdA7_CQ*wO2 z*uyW-pq&qONmQ;?URa*tk5Uxh_~4TTtI*-AE2;GrJrzwkkdR0_{{VG(za!rK92{SP zDlKvt&~B7f-SqBO6;P`2CMC+0p2oYxlBs29k&E?if9Frm~%YI zVyek%E-V#8-B+d-l`}-=-5VD&sxOzNnmFX(Nw|g`ZZOI-Hb|_^q{_7RXzvFSg@!p( zpmWmxW6uK7aNF%(O6xDEs-%``xS(m`2k$I8fyw6-uaivS86_6!kn`=-kv8NB8&IPKiVgcpg6U~is~*>Q1F0x zl#)DqE@VH(*rvjS5)w07cAjX+1958$kOw~JrWDSuHQ^O!Xd0F4JBn<+R)PhHtc3|v zDQl8jljK0R0>h=rxI1e{LNHKYQ8J4@&2pFB9aBn|UCB3$6&_sj2h!LJ3DZTB4AEv3 zkkmtVid77^>-0Y3q55Lko~fE1KNmrYEa~A)s-pdH)1GWd6rM%#3kcIE$-HvMV8Hah zKI0GuyR^qFw>cE?hT2)S_-xiU^1H1WJ#K4!dik^LkT2y^FBlM!CfF+YD4l? zLhis$jQ(IAKU^+{tdVZ7Wi>y>CuY=ozD$xz%GZK7F}80NHu@`ty}=AzjourCAf9ix z#ej2M8-I73RzF82He*i}RW!*_PdY5H$rA=gMi*8Da6ty(<4QPfG}>b>LmCQ&1C65s z_#1=$L@?|OMX-~pWK6&a()^Du1|nN3Qc5LhtSE!5mrz3jNh5>x!AgwSrzceR{XXNA zdq$(DOyK;0{*K4@RPEj8Yx!fseWH$BotW|bt^IlW{{Un4hwN`ZL(%^A)pczwu!`Gp zRSmY{xox%aKSY5GPbO_O7EwhL38Z1Xe`&pbmbu3>t;FvavhXeG%j*9C>2LBq*V%t) zWA|Nu+s|sv`?cK+N#DZ+_)FGKqib#VTiZH2OK8~g?6MTO+Zw7a?>l?r=vXi;QI0sh zMJ4$kkA11_Or3sg-CksrvdU6*$HeXH>EL>y%LEaF(X0s`nBJGE8gD| z+2sXac!D|U>maCsw$xKjVd7|8$?m^KU`Er%4lS%6ROWG zJ;F#fB8e1vUxp*dl031Jcihdee+m@yY=B28k8EXxn;q-p5!ODIS`sWrUS6 zlIWlVf2isCU?s-JAmZe5nI+nc&jDS{7bC=OEPC6Y%Nd-FF%D$;Wux)R!+$IpxVMnw zp0_vi>4eGbY8`S6nAAEfvOeI(<8l0e!X*6%M3dWud6#9~k;kdBC8d1Edibe(%9wlP zf-;WI`w!wi5C!-bzc|qhYDqT8xF>Wm~Si78OIbNLAT5C^MW*L1Q zW_HuOan{bUw6MaUxQr4m<(@SjgK@F83K-m;!E&M}Zgo?3S=nx()ija7`Gq7(lxtX| z!{12iiqZxAO1rmPi`w3JKXK*oQD-kO$26aK{PM%w{PBjZSi_&YdhKgA z?AJP@k1G^4wO!brvI@wIrO}6hRNQO?55z8RZRL$vFh?wILS*TVxy>)_Iyxqb5lZo= zd2Xy3fHq_1MaaG}Em;{&W}1GcnrB|=jA74SZ^y1SsU|6j+0I`iZn6m23v%ALa$>+W zE(D5WQnyeaaQ*K*HYFROr%0wolx|yG?dnDvor1#A=E@)tM7D@Gg&Wh~1=_B`;FU{Q zP)QPTVj4ERt$q#-FCyI*7K!Z-HOO-q-Ln~K7FUnIs7f@cnLD*$tr%+yfaF-4bI(i~ zlR`UEcC=XE54zr3Oy-kF7-6T&DynMch0#mO^%5TNE0e+~Rc@EL!yzruxmr|o9Y$Go zwZS&Vgv?Wosu#2Djc3c`ARhWNa6E|f7<^vNZb*DTQB5+Yzf3&J(+lOaEY2f5T=|?> zwwT3PmdiGss)Y+Ai_QFq9#~KNtkA9DYtU25@@Ej$G3XgbTb_puVt%6bu1P*xIzR@{ z{qr}OYgh~eAB8KhJk(8>IanbY6w6xHyOY+_8$0G||F-^#QdLw_k zViHQ#jE0%ql8Q+77sUXRd>tuOnbFfoW0r7^I;c{Aki%^lR9h#~K$9xGY_cKf%5D$m zh*Jr{L|lNYxZ(g8%Vzp)s0O`^zo+6{{Z=lZO6a9Em)Z(z}={7 z7So-V>S~!KRgH4_V_os9uXyXR0agSNxt8F9Eqja#8l6&+5IQk4uRA$BKO1rBkC`Y zZ5E>=^9NA<l{6W}RMSGU$kVisPqx5^v)ixtg@?8=PnM05bdbAU zM6)>VTfPqDZ?%ZO?%4Vt<%GR*IX#f5wBtcIzXaTD+si0Er=a5Gj<`hSXnaQ~J2#$D z1h7>iEWpXSMJu~VU|?1(Yi2vqRZ2rhh~2%-kKC>nfJxsN~)k+ZNUf6t`^~& zC&d-7Q$$o(Bb^q2T!2Ruu|krNt`z`f04=#V&p6K_$upLU zw=~Q$_GoB`8zWonSqbV5$=hyCvA-TUT5Xx~O(kAVW$e@9gHv{OreVqH=(5byC!kwF zLsb!t>{zq?@}1Wt-sBr&OtHpqVq=m~RChOLI*TdoD@^5jhb53rM_h!M*n=bzjvf~t zOs9kE!RT?HvZdN;jQf?G6Y*`9WIB(wtnW6;wFMrmrOeS`nkXgj1qpuY`IliJm&33S;4HU z>fcM#=9*(h&9}MJa#p+3DHpt^5XBLjj|$ui3mfx-boetVY`F9XY8lsK-wGL4q{wB+ z{{Uvw=MR_H<$0Y0cgAxgmn$rC4f~gC63e+%9!O9zRNQE7*2_b*PSWUmIn&T)dZ$cO zQcakT{5l#e!YZh%7@%{t?5v2Of=e$#0J%2C7~oCPVM=w$%(;fKr|maiWOF92x_UMimr}y?;)tBZPB{l zhEh6=a%CRZOgphlKGU;J1XIBnc(*Z|a*HbSz#fGE06a^rfc*-SROKlBtt&x{|IV@~XKDc6Dh6&D1>`vDP?m@E^ zAS<3* zFvm1yDKX0~t2&QJQs*(!$xj7k7^v>w#j@anxAUYmS08n`Y4MrE} zY1GtTwlubHL}@>0e8nWu3Q;1)WhDp-9f1l-unb2BpDYvNii7N$XP%meBA+G9UY5R! ze7>nH%}-GqM(c5Lb#8hbWUo`cq?)wL+u+)S)oTpJRbtwd232W5B^)}aW%CDXxVZ8p zdEnv6kE@q%Kg8h~TI|Osspe9r!+C{{U0QGh}G$^h$D)^y2IFy5OYswn;v7G^P2 z7twtZ*Wg+iKW4dQcAuxONMn+z;`_v?W|A2Q`goLq<}c5Y#^}@8=H}&9dLd-mda8-D zie?NlW!ge9Du`dQqBm#k51N#+f*c3m2*~QawN3PG)5^l+{i+OQEObb;cIyuWS7jw zrT~_f3R=}GO#SGPBMAl3Sl@Fpp1o{5xV{lBq0wZ@T&gId`O3*6c)QRe0j|<8Bftkw zPMi-sWs|)lOj&5;v@*p*O?qNdL{n1x!3codp>Ckxn*qSa3DLhCnl3z_B>8$&dg^Kz zfnOJe+rw@Lq2sN%!A2=lnpO3-Zd_=Io>N4XfYT&vO7W;c5Zb&8o;~oK$#>j1rNBw+ zXB2#HQ&qw70Q02u^;E|rq$x~p>l9$PDIoswB=B!y4m#12Yeqba;wai0Iy%{^QYn9D z$0pSaR{Nt-e-Pe0URDOg^Ud%;7tV^w`(n$q)?uW3GRbp+q%*3&nWxSck~pR@%AtSe zoGRMD29Z>^gxc7sruRYoTt|YM%ryARja3|iKbfMamW_LvM1c1sq>FGY!*bb3;DL!? zEjlm1f{fL2uGwSD4BgUZd1RFR-$5gfychK&X|X)=acS(h>Q;0|#m{Iz@F!%o8C5Rt z+S;5xSx#4u>Jq=wCH#=>AKrDtE|AQRRB&c#k=5l{omB-*H1thJ_Y%`j8I93^#K3-N>e4pcP48rkWo z>!X%Eo9^5rIO(y!w0Rah8`}74da*Qi{{X`FUq`7s5Xvawcx9=|sw!z}8Gm`1%&6oZ zKy-2c0K!MEGh~N!tYTJ%Jnul&)tURKyX>}i?%pImA#vmfBG}0&Qx@X+MGvYpjC47a zOEum(1;nY*#k`NNsPe-jXv-;A0*vv-ZU&D0V6zdSZkq?$3}%I4aT zQ?WXwqU|G0heaW)&=AXUVhHE;z{PxMEU)-&opn<*Zdrq%TiCE|zIN4kZiE|N&3_<1LeykDjf4AP3F zv!j=#$C0@=OtCU8O{Vq6LzrcCRAQoO;gLRCzMK%^H}RrBnMW6}AcM%puK48mTG_{z z9Nj#YSW8#Re!i>ffRx>VX-wLJwy0I+c`VR?)agqgk~Cv`5FA`{V_-*~5k8&|gQX6? zG!@F3SayM=HC9(08P9VAo6z^HwDk)sIteZ27;aW8ex%;Vo-m2RaY;$qI#@E|n{OOx z`2*$It#(Z$GSSu4B(TO&6inM}P4?~SZ);;bqui*~QGBJppujagYdlm{mGn?YOH#tq zMPyUYHW%X7H@+O6SS4=P*PqPJ@!@U88h(M@LsuM6xmjdJ9vz12eeZ@dE+pQB%DICx z5;;D&S-l`VQ-(Glo*-xwH1bl&#Xk$9M(Rflq*#Og`06;d)^!EM!y6A3EmF?06`=HOxULxD=6CfnJPdZg-7m##woI{!7{=_n}RRT z7=%0j03*}KFfUZ^PuxWJ*)+(6_(>gDb>_mx`%StWMO?EKn_5FWHg($Ky#sTc z>KwQT=_c=LTcBSS&Xzan{-XfjpxDK~nG>{LP5U-PX~20VU96r+FME6P2kDDQ46?PM z$1ty>$>?6LuBI9)NbR~=oFXmA0_5?@AP;gdS4K$Ooh_A)toW7FdVf#T*JZ4^y-f4T z7@!jOQYs@d_l;oNK_Nbdy!(v~&^CnHB?1 ztYlE*x$y zSts*J{!7CA7xS~f?QXvoc;e|gO<}~6SEng8@y)OO(&N%9E5NHouk#H1TBe`VI$mtv z6sq|;is`C8Te>0LC6pr}ylCiCaVlNa#A^N9m3o(<#dteo-8Wya>UCaSmB*Lmbkwx5 zm8XR^A+`7&Nj{|9;poQRsD$)sTUx4jQ`Mj)WQ2uq4^pPqWj5pxea0EeGpn&-@*3Q) zF@(=r!U|6EN=(ZyfsFK#pT!#mxm2}|+cJ+JIKo;rMom}HtF!4UDf3LpW^|b*mY~Ne zi)g!&d%(O{F&DRyH{%&6O|h%$2cpPx7Rw^cqfYJ(sU%HMY|cgAVS zT^begY=#-D>jfNwofx@|E&x=&UV13%Z|#h-q^x{wd(ia#)KRa`%#_sV#73&F8O7|( zr{3q>A9IYet2E<&rI$mP$qWlpB_A+j4ijK3$}M5XPg{9oJ?_qoWuUs|5j7;$l{KqR zPYg{g^;0m4IM8)wCx8IzPo@n%<{j$JZ|zSi%Qf$2eWiAnPaGAHW);z8`4w9sFC`r> z-MoOCZ4CRZ=0c7+#`rGOkeTmiBBozg@~;?=mmT7Ow{5_6Uqgo7Y+F5Z(RA=-+Fwu8 z)BMS%f`*(zwUTd`FLtB{Z#EaT{jd>gG?A@s9(MNopldrk?T1lB%|{ed*GZXBtrK`- zidQlyWjw1~o2m9a8rUgS=*v{vWS+(Kwwlm7>Wfn97tSN5qp3=W{{UpHEQxSc%)$I+ z6gKNe$pDN}X+4o8dpaXlWyiNqiiXOvp-DYHh{}tHu8p3OO40<8?@&Q_`^SWk2;3uX zxPLB}fTX*ma=v{@n%3tT+}W(sRpzT*GiEbPHc2GGio28`6$GyW!otGCrWW6*G;~kE zELBw5#!t_n-kV$(V&FlwB%RYy!V zOb7*7az*_9hZ!YgZn!92PYh5_V3si?48$1C_#m4ypCj*uCr377UZu&^A%nX+p)y=M zjzIIjy}w*)y$M{D?y6W;n=VqF^*O7N+r1@suuv`6rLm$2*Aqj`kWey;X=(e|!Hc@6 zkOL;~5!<@fwX7|JSA3Sz3uROjRnt?y%*>`~mNbt39uFn2*ZePrH^`SogW3&ZW_dLO zbYKA^{?UiGX&lT-ry+Mumdf+ozEH8Wi98P7=sOiRi|+RJ`uSmNl+7~SS~?S0X7yQi zQ&UwPOfuKTN6N!B62%3%JRxa}HN zO*gY1!{`#Xu{tT{k^XA8Hm%NMbSA>{l|%H}hmM{jhDE0ZZ_SyygyYmc)ufXCL!~Zc zVecXg7AZ~pyWHWu)zf#5|%%-qNByZA5n>6D1xdbE*Xy_5HyODwA}S8YonruV9`^lE*AFh39$#r*nXEd zi|RHgucVoj5WEA39T1vZo}}UzM|D$)C5}$#e3F0yB7#d?fnYz5D9PZHLUVZ|5W^IU z6Wwm!1t-j0*kS_QZ(*#HBkoA4tMOC@@FxT>xjtw4eR^6{CUdlkrJFob1dpz zNwM`Db-)@v@?%uf)Tl@yN>V>~-tInv_@$tmQqs|)dOzs^G@?t`+yie?IRmBt0N-2? znQ4RbI2x2MCd{`7*A`Iq@a@}GPi1|fQ5xF0Mx3l;1$8yqkOr_4>=(N!vA6`=a2;`J z(-oT<$qHQfJrc?4KqAF)!4$E_X z_pclqE&5<=xY{|pw|?83v%I2gn(OPRVg8g{{SY1Y$Rs~a~-4=Q*KWp!o^aT zq0zyH-*lG0sGab;qdP{^TdOE?RF^EymYS|Qs*Jt|Uh8q($cC;06o*}`1A+&H0mClZ z;L)8zO-4_Dk=V&WB{oe1W^~h5=B8CK%aqeeK$1zk_evMOc2l>gEN``qh3qvZa+=<_ z{KeekR#xSCojy~T)Yn!@*Q$1v4LdAibw^}HUlpTp!)XTUcW%E4bA8G9;r{?&;RPs4 zJU&tSO<`A;X0&;RRh$h)lt5ByiR`TLBN7%<(U}eG>(NfmdA1qc(-xxio|vzcM!J)= zdO=jiriy&Us;03m8^ndBbK>V_)I4>@S-mq%BXU+_S1mSbvDDSe4K+nm8RXvo0MeK{ zT{ok0yMrdw77Dw1+~X711JbirVWgs%<{5@t8b?h`p{fHpaT>Cat$$W=vfV*Gmd1BC z!!)RJ(Yr3oD{}f;dTQ7tps1RiD$*uz6mdB=X+1XuxI@oEK)w?$LfmT09Mq4OIeu9Z z=Gn~3-?QjroGaBzsko-|;Xz<-?crO2$siOcxl6ShBGXMSNV?BJ>Tc9@Luu0{TSrBh zRMDn)OAs@BwF-3F1t*R0JC32ZNF6N<~+qtfDPD$2dBA?{CHc?vau}`*_g}o9J0Mi6?-rVBJe>H1-Sf;!NqI2CS4t| zrSt^)L*jQ)W;D60y#}MnpERwdl_O~k=`uu#y4!?lb}Q2NAm45q6#JX-!+%4SzAG}C zETiHZRl!Y8A&#MGYUM=W8Ki|vzvp0oTvYiI^x4s$1GP<4*#>P*WeH23Oc7VeG;zQ4 zB>s3f{H!Ps{{RqF;%>zCqM1Y=&ln3JnCmu5O^;uGQDv)Oew%zo0!DCMDpGNqI6VQ9&P)s%v=qX^KHU_rO9ICFND zj7reVx{9MSqMjP8sO8m}Jxb;EP{7uOq(oo5iu{OO3bGdJ4Zn98GD?7xD{W~)l2tuK zOS%`7qRDXfW8_Ec&Jvol21W5Qzs=1+rkYt{1}D6BQ{p1l1fO2NrZh%KQqZGcmC@2= zbu+v%BnU`%m;#o)fwA*E3l460!;^Pb9lH~Cwri7DMUquznOsmvDkP_^mXcKoD*~Pt zLDEHC+m%T?`tgl1)|hclov?)($CuYqNfc2UMpsIDsHzEwU4^_10pwf9&>ju1gYmIV zv7a%fj;fXsQ6f{I024XbslhBs>0lJ#yS1YvRZW0&r0t$bm{*SV=^T+yL;xNDZnH?{ zz%y9wP<$#y`r`OwC#?-S!#m9X0Agl!l)`EXtfMHX?x{P@>S*paZY6;R>J%#hW51}w zY|9lJr{#sT<#i5qmL+ahg(YcrElRK7MKM(@mpt*f0y;3nx&le3QZ!L&Z`yU=(W>Jn z@;ogIUKxOWtkIMBmvzV1;9l3o+goEh6;&IxE^P5+xz$H_U}UA3EBT=jl3VfskIxaw z=#j-1E_QRD&8Pb@({Qq~#F<<`AU!TOZT!n|i0{;7qMX~H+GplTk=Z_6@Vjr**5}sv zswq+{{?U7W$~0z3l{6Jpb@_~qGDRDb!Zwd^VnX=3soT&u1RGeE`?i(OkLGBE9G;QM z+NZa@Td1d-yUywW?7BJ5s2#+9=)sOe3C_??A$)_G&f|-rhkInr6l*z=yn8Pg z2Hq{p{u{AK=F5Q9Emg?o!xv&`FHbmfEO-a5DX=JouQd&3nbCQkWtY;;S62)ySGWPY zo)6xzH@dG=!RgI~$QHxL6)B|7DkQrSPdF2<%*@TU*W&^Szb`SxP>l9)8=)7zAR`aW zQl%V{g1zWoH$`@_Cgfj^KDdc6n@my8$oXn_V%_Od7;$TYf5O-xP@z>Tq}RVgfwA4T zj@I~2_^-(#(=|G0SyV@q%a&5^rp%cw4A3D(NQhgLCx0I62;x4nU|(+UD5Ih z?Qi3C4Nhg2c7H)VJn|NR=JiB(VJ>cCr{zKEXFf!NxT|5qrcrk-gnkseDyhjbYR=R( zmP)nDFyBv0n#M$u#3W%f%3N7w7FJN)ur1SqM!U2YY_2b|JhQO$?yR8HsYRG&D%1SE zN<~>s#BWO!w^J*RHB+#Qdg9V)(>Z;zw%E&=*8IL_lg7y_Mc3D|I zmy&drDi|b=qcg9IziJ~h7u-nO-T^3O+i(>e2fcg6$foKOTKR8?^Cm@TwB5l3X7M0c|;|Ux(|_j7l(fc7QY;yR8nj zugWQEC#BDG0cXprX}jntDF_adDf}-`tK(%OVt~C$i>I+pJ>CtOamxg|eA2(Y?!OHW z*>=98$nxZ<%d+g>GnTc@V9e|5AwyT<3p1LDn#I&E;#Eq7P!wE&jMB&4>4OIL(CMYK zy16T)&NBHjnw;jXL0qaD&%2g17lsILA9#|B?kpL%$j!MxwX@F}UMVl@>THHonqDft z@B6*qk=~!Q+`_gRn!K+@KCGT}_#ME=|ix z*u^KulDqh&uA%J@vzizop0g*LFU{oniYn;dmZ}zxHjIEx(SR(LA;=+=f^UaoxUgp= z(G7A~^?zg4+CG;huc58WB8NYQhL$5VXu(-391`11Qc8|ON6N8rv~hDyz4C!uS`~7< z+a{0Ur?gEgHeevm=9p&G6bVokTE&b-EtNldG}?D?E-lD7kZnuoNYhNm;Zw4zii+&R zUdUjQDm&$QO2=tuineBrTBIo(M|Tsp(oa@yIK%zoi?1URvRA||l#jE&h}^SN(ZFKK z^CL-|jm%o}!z!goE_oz{MkMlXPAXHiWz{pfjXV?AJxp;z^GG(OLMSZJles`{rEYI) z+UEG#cF1tYZaE_B&xn0xnaNEd$ugRE6|EbEe&Uhr2=23t$GxVF6yDLsu{(q28P%=qJ@9bcLW(Nmd-^kkoH&Oe`lhMZ`O6DusMrErdrynIQbx)L}rMzUUa!TyJ*V=^o zgXnOT#foQ0d@g5gmv$MKMOG>*WqjK*6jbz3_q7!ftA=tC;9Xd^7w3yyn_-IFP;qpJ z9YhpXB8_*u(Gi|)Jb+0QD!M?_-%XLhOBqs zIH%x+taA+Bx*VOedU{H1t}-U1jxt3i)(U(9Pl1_AlXG!!7lDUDyb>vHi5V=i!KT); zHSei@b`mM2fmFmJYm|+dh2vt@ZhD(w?{tMpZje_r6t!T%ED_ZTH%V$|t0l~{A-IGV zHc`pk!_*I45)-w*P1gs zAbwt*7p=hYn{&CZh||>ydf?I86-j*usM;AP`GurFrj9+!OA|KKC#kvd zAAW<6AO-?X+1HAs0GdO4lVt=GZVA8!ek{rpXt-m|ypADf*%85ZzSDc{907E z7a!pJ(bkF zkQo>;Qu_g4TnnLcD-PFtQiC_j>onF-%TZ4ylCGxL+*QMEPheM;^XNF_)tfNk?fB)% zUQu>qQ(K{PElb}=6+I}rDI)Ta=%il!SUvCNYzLjFbDc>#Zp_NHJEd;H#8a6kHk0X)$zFSWh z0P}tw8FwmUq49m;>R#HPQBw+R&o6q4=|X~(as^48Tg7rgj>K*x9z3ueoxVzO-K_~- zre;-HmRU(1LrYU4RCkXg%7et}Q3b)|00^-cu)m%TI(iNXKe%70&U2k*9$%;F-leOa z5lHe0f+393zEf01i9b=frMI5yO+=F>A`MFPh2hqG|g91YHW@y(w;hse6qedD(Le@VHHHT zDkDBg8Q_pDz+r%rX*C@;=g@R&J9}T$t#mGDSvP8$msuiBGm6=1pv<9L3TKnN+(dq3 zxrvBVz)|PGz{)GLv%vX1E9I94N1fzaM@G#~s``_u@(^}{ zPd2tP$vI19CUg}=Qs=sNGMh8afwWB}9#aWsNM$|9%H|H7S+jV2o z(YvzDhc(ff^>lt|o=>PU*=DZ`EL1?lHCNrcIFy~~KH}{0S&^faSP;nAxvWp(Y;UDBd|`m=EgU6a2CuHt$+CJb(eE7$s?p?Y$In zdOannvl1^ z6$fRHE|1jOpC+uRmKkz9%8jb&7FfHwkCv>7QRD;*R3~^D5L~!hFe=`Y6qrW92jXYq2P)AL>3+g8Jsh*o zXew&rY`_}Kxx2#f-3n}JWKkrpNw7x0Qn=MPrH{30fooI~?5n)Zbfk23IWv}Tu^Dtd}&b1asoOrk8O zETk}e>p~Q?kt}LfL_OyAc5B$=RM(GUuL}nxdg&pDM12!$VON z5`#QW$~VI!JeTRn1ON^-;=bo$Lrk&tHB|bPi`L-b#j*)F6b182f&zQHS7z0A@X%PzDF#XL)sd_5atc{CWL^60J z0eWrRQMl*VrVSMhNHIa1OIwo0`+<(N5_d%w3dfcJbQf!r?l3~!Y0)HZrOBw{s=H*h zbutza5=+XKCY^`wmb$pVRp-kA8C;xvuju^$0AaI&?Tq8+`8E2mt#-7ihf8NsQpFW! zWL_%Bgz<+i&@mxX`ytS}vGM}`LjjU6zaB*Dtv|V{-`we-V3kTQ&-Fk}XP;44Xeg^` zD?6GuG1RCMkn#|2s0W;z?Z=gk@X5dS*51F*;8Y`0CU#K2vNTN_0zP86y>&otH!~Xm zPqKmfoE_GM&0E;F+vayu4G1fBmMVVsomAb_z153bjk|4cJE*oaW3I;2*vwI>9D$gj zd7amWZy-8ft`g3YBb~FQx+pxc^B+u64Ux!~sde%S?>k9}iX^-SXq;ZUu&p!l{i0RP(4I@bmWuH;%rT>cJ(_=fwl*Yvs|R2t(UiJ*=#yzvFhBeB#^90 z8a(pt21hk0`-lK_vE$0)n+xDmw`K^*&o6PwlgI;}dAx{s*aoM&i*Ljw{o@|aryyjbE;gxO9YTBLnzxKNh1|-dWPo=S{d8uR{8Y=+IKQpshQr6M!Zg~ z;zk}<{?PJ1V+>78k(F!ee$BJR&hir^wIEhUCwnXaNNZY23^cN$5|b-0B(I=T&!Y}8QIl%ruTtX%<=Tb1yppp;>AH;6m`wE4k2HW( zgd*c&!pYUh+!!$E+!4+3P4{SfNxso{vy6s@i$v4a*5_ZdDrc&acw^kN7iF|*DTc(@ zY+ymV2-9n*ica*gJC%0tQ~i-~j(qaPO6h)d zYK$Kg8X0z*p~>jAK5MD-9Pcv8W;u;A>PhNoBPlmEizI#(?;ax{@m#LeK%ip`vt@Jl zYC3$@_~YwW{!I{qYVY$Z%}K1G&$R_kL)gpJR@Bj~R2g2V%Kqi%rdb+!qn1geRA8*= zBc8#RyWB2rPPjr?Bkww=N$sckE;sk0tvy5Y1!=uCoc0x0NvLy8bEdOxL7Gs{meFYV zN@vp6!BAD1EV4$Hc0d^Sw;+?c6vqxD647W%qz}gHcBXx)U;B%FnQRG%il@3I=ZPK$8wl;idicgxh;MF09}vwaf%>e#$-_mXNWKtG0*eFO0s+ec^hd4%+N7tfKJPnKqok9G@htxT=OcYhfdbefgLt&qjI?twvacB0W|y7Sg^Y` z!dUJE+>?w^%aR(O^MBws6!wM8*D9>6u7GBF-nXu))5)@pmK3E(X`Ml3R!cK09%MJU zaxQEwhx_L>q__Bss?Z)_ zVOLeAa+oJZL|WNeIbLON{DKPXQQ_Nv&&wSw30t9YRLe@6G^WWinpdWvSkH9~W|gV~ zw6C-cx7vD}o-76LieD4k6xQsH8mf+2WO(Tws!}A9bq3*xw$-(OMH~U=$vARrz%3A} zSkA5{5gYJ!q388DVqj1!GoqHWEUTvP_;?v^6axNWAFYM(#K@I9AZXfv`6j}RBSj%l zfY^Y1L;P?>$!sMg63G=pnB|Sg4<6z|?i~%fUff^u#owR_R)ZORT@`L$6ycUCgWza| zxNkr$=hS_1PtovB8CvQLt~H*b7z~X`6G&oN2MXkPfVUlb+XWq^Wse!Qel(^_1tk$i ztyp6b?iUgG04ztJJqNBUlDln{?M_`;U8i#@snzNYQvzm@&h}A#?oXj@g5F>?x?@H* zl4!f8kw90&ElqHSG+<W9R!D`hPbrydP zWfIiED7jtT+wke-VmyyrPE3w1X_{o1yAn;w^T8Ab)Y+3sSR|;BM2Ql@6p?!b0Dmr? z__5b$1CgOQgyfO;Ly_hf{{Wykr=S_5I;6_}ZGLKLVUuYgQ;M5#G zDfcl90lwB-?(+Wt91FIzXOBAr*JgBCVoO&sXegG&tf84lpcfZDg5&Bs;$F5FZ*!zq z`fe=SUS?l0o69iEf)w2uuvG^A(gJw7VtDmq&o~}UHymiaG7QTo%JS-5wIq64DsAl= zLYp?Rwa*8Z-~vx3z+S)rF8mlLa(37cwp~R%EJBGe-G~5kPh3JBDWKy_vq@#jDQ=cPjI)dY_P?sz1apj8meFW2AQY<;l^zs_0q*wtwcP;pS zm}Z`YwC?x{&8f2~*w@7|inPMft1^WfNbv3Qu;%ycbA>e+uHOMgTR=gsbJU%ltas$x zk=> z0(n5%=$;iFcs4ftu#UwWuP*-pFuAK2ve$+@;+mpZk}S4KiG$4M*@ z(^iUOwbDxq${4N1?n9B!n6VgC;or|8Cro{dKBUP+OEnWx#To+}BB4uO<$>Hu_r1yL zae`5Dzb42^G2=(v+u|mNBB6?pOi|C9W-Jv}S)q_5PIv)88<-Mrw}F1A2>sGicQ;6L z&lU3Rid1^qma1s8{?oNE&Sg^O43@5-%CH;pdT@9kjs?9gaICoIm%8ZVUtjwQ9j*80 ze}Gdl_=?s2qKDJk2OvzNByuBa$)JqL2^|nx(F(eBf*5{QGU~f*5;0;GLtNZ*dwF4b7Efu(Lvh5u{{ZLy z2KGmzO}dod@+at>HC3r|^QgN(q(;plA)}#^nly@$4=LeLLaqHp`r{W>)kCI-xanWR z>u$@()6XOKnGs;>F4A;!9!EIr0?k| z(g-`2Hx^YI^YLTVbPFPf@g;xHXAi85R!2xj(e?E7dZ#6pXw&6|TFE%r0g<F4W>`BRoTCFo;?S`2(Zc9AE<2EC)v*($?Nq;#^$SD4n*LXnE9lW66ra+VB0DNP;v zo$Y&*gmiKJ+X9wPnp`K((SLS7GVjRLUYDN(j)K?nX%6C=M2Gf#Z4{KTlniN|Tq^fh z{5KaDTN~J%C#mWE^6p&!08voO*y-?H8f#Lnag)@;Q5{xgRFTNvEl`G|W3v-=2d2?r zZ^IMEOmh9#Ywk|Jk)>`MR#ch(R}aj&%4U#5G@bfUw}8WY-2lHVdW#;H$7>v&RQ~`o z1mWf+dS21KQC4ZHA!l1e<>cpYLCC*R;o5lf!&VF6PjX7l((3HKZu!JwTfou78xl-S z$2<=!Uf-4qI9zxs$v#S_w#w>!-z&+ZgRO()Bmiy1a8`?3!%Jq{&anQv&%H>{o6Z}t*!SJaBm#AK=KBG(h_!dy999C_L zI0Msxii>HINmZ2ItI?3qQ!Rd8hNYTAB}VDlSVg&tjmG!4C7G@GRQVgdGUGdR%95?- zb(ThmkCy-uU@S`>Pp_xvfM%p=qoaD+cQw!xa^6e+?b8FuLncR-bcDLan|+`CR6Pwl9_~5t8d0 zL&!D_dy;SDJddACVyvpM?q1PqYBG5#VWSQUv%@sA6*T7K#J;7C`i_8F-EjW>Qa~(w zD}8aOYHPCj(pgJwVPm+Pk-$9L$dUg5cfJt5Vd^wjG}@A~iQLBG_a%jgi29zq{cvG8 zr{LWNtfdOaFv`-tp^f|g{o*8f0umDa7I%=7wos%8sMOBs~H)*Z@|;SAOb-Zh+sy~vD!Sc_myQKW~AqKxHq zP*F!`G|Xb(PvR0W@$LkAhXM)xPsFQLUZE=08B!Ix^|wL zvp1%vt9fIBR@hz;3j%mk&y~o(wees}O3`%WS=AO*mP1M6uP~uOG-+#)Y%j^_YjyI) zQHpmbbQ6bCJc+g(wk2O#NK$?ARrLm_%39i~;)r)}Kp29~k9(*-o>(-+%078FRA(&( zqVErPdw@v0i}k;d{&*L?hZs1jWbC&vgv!%2vRy3EERx%4=g@Ko7+Y*QOwFs>5%Sd( ztKNPg^ARQa{d$}iJ4Ghapm~yokjT&|0NCufwhH_cW}Lq#f~~esQzbh{Zk^B#Zg}h0 z7k+_XmM-2&>%}VBr0+~%KNmN*IG0-)R;XZFmdm3Jn)eb_LidHY769J&QP45=7xluG zaJUJ#CgjEZ!iDu_eA$guOG!+DK}9xVpxj^7pIl$csJmlm)rpq}G|^sW8&#(z)qTMA z`r%!+9idY)teQq-l1G_KUuzqS{V{Y^pJ={v9n!&8*=Zu(?|4y~{Xn-T37>D3V`1HT z(8-i&Y>`%;TIn66lJYfWSt2=9pP@EVIp+N1pOZF9Y6$6L!;aI)`6O!nQKqt-xw7=h zX!nVMKQB>qm@S!0ZQHiZ;tV86?S?bGNvv~xzM<)B-in2!GRs*Zj!B~jo62^E z+s&?T&%Zdw933ngQi^ZF{{RMw=g*a|h!I0q9$8cK5$5pcS)v;x#sb@JLENKt`rHc+ zP6}{&BiiHJA4?e&qWinMpQ%u4&1q2=#7DyNbP z7-xbuLriL9*j27M+hP1k#_VuX+cQn}@Gy2Cl);ly$a6WSj*~K%xta*-gmAi`B|Fvv z>TXWkn_rw}>g6W>1-10w){Phxds>rw{Jw)uVNFQ1vcXGVHB<{B*|@qgl0SO>VBBBx z7$~_qZEVW>RnXSijz>{JO4adHGwxQJrU~DIRI36BWby1<7>4+g-T`}lFH`ixlq={XzTFG8`J8anJcxoW@b2&;i~gU> z8sQxoXUVB+#{fySTap)&r|X9HiS8FQeosAlj#%cBK&)7!TH&q!UU+PE$yRH~bEe8_ zViQX8Mn>Jw<}Jv+YC>`1)rkMqwhp>Af|4<*TEG;(JZx6 zpr>Wt_K$NLsTbn@op3IzWLjXzhkKbCIV2GP%&gNDMUFUERtmqjcD; zEp|K*I`PgFT>ZGNN9{cn5$02X(qZ_SYj#8&c!|Fos&X>eP(A zERyYnSa@%1Zs3dbxB~qKGq)+fM1oON2(Ha)A2DbEj!L5TJ<4dmJ#j@A!)A|7Xr3U2dTczA?r~{f21d3_YSyGcvoez477960 z>3>`ngx_}u=xY{IOicSd8_(;3Y-(PldFzrnZ6t;<=0e{Uu#D+*jqs^~NhPwH2**?Mf=MbQ5~}d%w-#=no+IA?MW)EL`Sa40 z?$`pBU<%&H1fP(*Z%mCrMLwii>SB}$)xmde0AhOmE-@SMUH0fVCPh_Bz*ZHiswum? z-Ee%aFVhL{^c~njmQw_VGjQL9uhS8bi5#R%1IQw?Z7f%C9;VnQ!8?2gXr`v4VNkO% zXuN9XAU7U=NPJ#enuTF})c9mxv1n_kad zCv!#LQ^OthLen&gg!L+0k%ERKzHNZ=H^8e+1lgTXX(TPU_>J(%H5H;kIWgZW6nSgI z8i^i7V7B7qup;1_i{AVmJ@L++&y%Z;3M$a&ShWWyijpZwYI5?*B~ZJFuwij>MY@7W zzd>ws+Nt+5Tzh3vU#2Ag0K{vQ+GLVgsP>kOs;FZi7Zw0`dfxv4UYODD{o~_**j$>2 z#d<64E0KT1S-xSZYN;WaqGovFcH-cT&+lvA$MWfo`DgCZ#F>W_9PcYyLmbjIB&d<1 z!i)@|0loJgdGfg!7^Nj;PvLaPbXD@@#;bD2^0G<2p=Aut_UIe;e!Xy0jnnc}B}u2j zn2=9PB|KLY+kWs^Sx;U(?Z`Iv#IDzDwec!MO)FU@S@Su4ObH_+s zkHjj@{p#{a^W&+%`#8}Z@@JZ^7(P1YjnJtlclNN_6JZK`!KSD&D{VZR+XE#?WLsvc z$|F}!Vvs7Z`@qZOacQJmOo2g|$wIxvmSBg2<}iGM!xY(+aNT zO2A1IQf~gQoyV#3{c%g`M%8SYPhC?>ICZIXb+a9WgK_17X+H#wpDXnR6@1bf%4p`P zZ^7Xr!`|PSz^M8$x9Sw^VJdSU=MtM+OQ-QM9 zQ~-7IHPgf0V_WiKj9yuH%Y&J<#ZyZZlFnGkEMiyO+wK?#zdbl%`5bZN+c!ldR9U8G zizy(P4&!n~i1X|D^tKm6qJ^yLRh5~Pfa*UP9e%#PSU9@S+SQPxa}o&)U5Oqbw&(ee zE^y{8G7QE=sAO_ol#tt)kPYsCjslVn+dnL-w;Q}%UgO&qQ$I=KL1T&`7DWNM^Tm{p F|Jk#Jh -

- -

7 Days to Die Server Hosting Guide

- -

Overview

-

7 Days to Die is a multiplayer game server that can be hosted on a VPS or dedicated server. This comprehensive guide covers everything you need to know about hosting a 7 Days to Die server for your community.

- -

Quick Info

-
-
    -
  • Default Port: Varies (see configuration)
  • -
  • Protocol: TCP/UDP
  • -
  • Minimum RAM: 1GB
  • -
  • Engine: Various
  • -
  • Steam App ID: 294420
  • -
  • Recommended OS: Linux (Ubuntu/Debian) or Windows Server
  • -
  • Configuration Files:
      -
    • serverconfig.xml - Server Configurations
    • -
    • Saves/serveradmin.xml - Admin Configurations
    • -
  • -
-
- -

🔌 Network Ports

-
-

Required Ports

-

The 7 Days to Die server typically uses a configurable port. Check your server configuration files for the specific port settings.

- -

Firewall Configuration

-

Allow server ports through your firewall:

-
# UFW (Ubuntu/Debian)
-sudo ufw allow [PORT]/tcp
-sudo ufw allow [PORT]/udp
-sudo ufw reload
-
-# FirewallD (CentOS/RHEL)
-sudo firewall-cmd --permanent --add-port=[PORT]/tcp
-sudo firewall-cmd --permanent --add-port=[PORT]/udp
-sudo firewall-cmd --reload
-
-# Windows Firewall
-netsh advfirewall firewall add rule name="7 Days to Die Server" dir=in action=allow protocol=TCP localport=[PORT]
-netsh advfirewall firewall add rule name="7 Days to Die Server" dir=in action=allow protocol=UDP localport=[PORT]
-
- -

⚠️ Port Security Notes

-
    -
  • Only open ports that are necessary for the game server to function
  • -
  • Consider using non-standard ports to reduce automated attacks
  • -
  • If using cloud hosting, configure security groups properly
  • -
  • Monitor connection attempts and unusual traffic patterns
  • -
-
- -

Installation & Setup

- -

System Requirements

-
    -
  • OS: Linux (Ubuntu 20.04+ or Debian 11+ recommended) or Windows Server 2019+
  • -
  • CPU: 2+ cores recommended (single-threaded performance important for most game servers)
  • -
  • RAM: 1GB minimum (more for larger player counts)
  • -
  • Storage: 5GB+ for server files (SSD recommended for better performance)
  • -
  • Network: Stable internet connection with low latency
  • -
- -

Installation Steps

- -

Linux (Ubuntu/Debian)

-
# Update system packages
-sudo apt update && sudo apt upgrade -y
-
-# Create server directory
-mkdir -p ~/gameserver
-cd ~/gameserver
-
-# Download server files (method varies by game)
-# Check official documentation for download links
-
- -

Windows Server

-

Download the server files from the official game website or through Steam (if applicable). Extract to a dedicated folder and run the server executable.

- -

Using SteamCMD - RECOMMENDED METHOD

-

This game can be installed via SteamCMD using App ID: 294420

- -

Install SteamCMD (Ubuntu/Debian)

-
# Update package list
-sudo apt update
-
-# Enable 32-bit architecture
-sudo dpkg --add-architecture i386
-sudo apt update
-
-# Install SteamCMD
-sudo apt install -y lib32gcc-s1 steamcmd
-
- -

Download Server Files

-
# Create directory for game server
-mkdir -p ~/gameservers/7daystodie
-
-# Run SteamCMD and download
-steamcmd +login anonymous \
-         +force_install_dir ~/gameservers/7daystodie \
-         +app_update 294420 validate \
-         +quit
-
-# Server files are now in ~/gameservers/7daystodie/
-cd ~/gameservers/7daystodie
-ls -la
-
- -

Windows Installation with SteamCMD

-
    -
  1. Download SteamCMD from: https://steamcdn-a.akamaihd.net/client/installer/steamcmd.zip
  2. -
  3. Extract to C:\steamcmd\
  4. -
  5. Open Command Prompt and run:
  6. -
-
cd C:\steamcmd
-steamcmd.exe +login anonymous ^
-             +force_install_dir C:\gameservers\7daystodie ^
-             +app_update 294420 validate ^
-             +quit
-
- - -

Server Configuration

- -

After installation, you'll need to configure your server. Here's where to find the configuration files and what settings you can change.

- -

Essential Settings

-
    -
  • Server Name: Set a descriptive name for your server
  • -
  • Max Players: Configure based on your server's resources
  • -
  • Password: Optional password protection for private servers
  • -
  • Admin/RCON Password: Set a strong password for remote administration
  • -
  • Game Mode: Configure game-specific modes and settings
  • -
- -

Configuration Files

-

Important configuration files for this server:

-
    -
  • serverconfig.xml - Server Configurations
  • -
  • Saves/serveradmin.xml - Admin Configurations
  • -
- -

Server Commands

-

Common administrative commands (access via console or RCON):

-
# Kick player
-kick [player_name]
-
-# Ban player
-ban [player_name]
-
-# Change map/level (syntax varies by game)
-changelevel [map_name]
-
-# Set admin password (if supported)
-setadminpassword [password]
-
- -

⚙️ Startup Parameters

- -

Basic Startup

-
# Generic startup command structure
-./server_executable [parameters]
-
- -

Common Parameters

-
    -
  • -port [number] - Set the server port
  • -
  • -maxplayers [number] - Maximum player slots
  • -
  • -map [name] - Starting map/level
  • -
  • -console - Enable console output
  • -
  • -nographics - Run without graphics (headless mode)
  • -
- -

Creating a Start Script

- -

Linux (start.sh):

-
#!/bin/bash
-cd /path/to/server
-./server_executable [parameters] 2>&1 | tee server.log
-
-
chmod +x start.sh
-./start.sh
-
- -

Windows (start.bat):

-
@echo off
-cd /d "%~dp0"
-server_executable.exe [parameters]
-pause
-
- -

Running as a Service

- -

Linux (systemd):

-
# Create service file: /etc/systemd/system/gameserver.service
-[Unit]
-Description=7 Days to Die Server
-After=network.target
-
-[Service]
-Type=simple
-User=gameserver
-WorkingDirectory=/home/gameserver/server
-ExecStart=/home/gameserver/server/start.sh
-Restart=on-failure
-RestartSec=10
-
-[Install]
-WantedBy=multi-user.target
-
- -
# Enable and start service
-sudo systemctl daemon-reload
-sudo systemctl enable gameserver
-sudo systemctl start gameserver
-sudo systemctl status gameserver
-
- -

🔧 Troubleshooting

- -

Server Won't Start

- -

Check Server Logs

-
# View recent log entries
-tail -f server.log
-
-# Or check system logs
-journalctl -u gameserver -f
-
- -

Port Already in Use

-
# Find what's using the port
-sudo lsof -i :[PORT]
-sudo netstat -tulpn | grep [PORT]
-
-# Kill the process or change server port
-
- -

Missing Dependencies

-

Ensure all required dependencies are installed. Check the error messages for missing libraries or packages.

- -

Connection Issues

- -

Can't Connect to Server

-
    -
  1. Verify server is running: ps aux | grep server
  2. -
  3. Check port is listening: netstat -an | grep [PORT]
  4. -
  5. Verify firewall rules (see Ports section above)
  6. -
  7. Check server IP: Use external IP, not localhost
  8. -
  9. Router/NAT: Ensure port forwarding is configured
  10. -
- -

High Latency/Lag

-
    -
  • Check server resource usage (CPU, RAM, disk I/O)
  • -
  • Verify network bandwidth is adequate
  • -
  • Consider server location relative to players
  • -
  • Check for background processes consuming resources
  • -
- -

Performance Issues

- -

Server Lag

-
    -
  1. Monitor resources: Use htop or top
  2. -
  3. Check disk I/O: Use iotop
  4. -
  5. Review server logs for errors or warnings
  6. -
  7. Reduce player count or increase server resources
  8. -
  9. Optimize configuration based on server capacity
  10. -
- -

Memory Leaks

-
# Monitor memory usage
-free -h
-top -p $(pgrep -f server)
-
-# Restart server regularly via cron if needed
-0 4 * * * /home/gameserver/restart.sh
-
- -

Performance Optimization

- -

Server Tuning

-
    -
  • CPU: Ensure adequate CPU allocation; most game servers are single-threaded
  • -
  • RAM: Allocate sufficient memory; monitor usage and adjust as needed
  • -
  • Disk: Use SSD storage for better I/O performance
  • -
  • Network: Ensure stable, low-latency connection
  • -
- -

Operating System Optimization

-
# Increase file descriptor limits
-echo "* soft nofile 65536" >> /etc/security/limits.conf
-echo "* hard nofile 65536" >> /etc/security/limits.conf
-
-# Network tuning
-sysctl -w net.core.rmem_max=16777216
-sysctl -w net.core.wmem_max=16777216
-sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
-sysctl -w net.ipv4.tcp_wmem="4096 87380 16777216"
-
- -

Monitoring

-

Set up monitoring to track server health:

-
    -
  • CPU and memory usage
  • -
  • Network traffic and latency
  • -
  • Player count and activity
  • -
  • Error rates and crash logs
  • -
- -

Backup Strategy

-
#!/bin/bash
-# backup.sh - Run via cron
-DATE=$(date +%Y%m%d_%H%M%S)
-BACKUP_DIR="/backups/gameserver"
-SERVER_DIR="/home/gameserver/server"
-
-# Create backup
-tar -czf $BACKUP_DIR/backup_$DATE.tar.gz -C $SERVER_DIR .
-
-# Keep only last 7 days
-find $BACKUP_DIR -name "backup_*.tar.gz" -mtime +7 -delete
-
- -

Security Best Practices

- -

Firewall Configuration

-
# Minimal firewall - only allow necessary ports
-sudo ufw default deny incoming
-sudo ufw default allow outgoing
-sudo ufw allow [SERVER_PORT]/tcp
-sudo ufw allow [SERVER_PORT]/udp
-sudo ufw allow 22/tcp  # SSH
-sudo ufw enable
-
- -

Strong Passwords

-
    -
  • Use strong, unique passwords for admin/RCON access
  • -
  • Never use default passwords
  • -
  • Change passwords regularly
  • -
  • Don't share admin credentials unnecessarily
  • -
- -

Regular Updates

-
    -
  • Keep server software updated to the latest stable version
  • -
  • Update operating system and dependencies regularly
  • -
  • Subscribe to security advisories for your game
  • -
  • Test updates on a staging server before production deployment
  • -
- -

Access Control

-
    -
  • Limit SSH access to specific IPs if possible
  • -
  • Use SSH keys instead of passwords
  • -
  • Disable root login via SSH
  • -
  • Implement fail2ban or similar intrusion prevention
  • -
- -

DDoS Protection

-
    -
  • Consider DDoS protection services (Cloudflare, OVH, etc.)
  • -
  • Implement rate limiting where supported
  • -
  • Monitor for unusual traffic patterns
  • -
  • Have an incident response plan
  • -
- -

Additional Resources

-
    -
  • Official 7 Days to Die documentation and forums
  • -
  • Community wikis and guides
  • -
  • Game-specific Discord or Reddit communities
  • -
  • Server hosting provider documentation
  • -
- -
-

Important Notes

-
    -
  • Always make backups before making configuration changes
  • -
  • Keep your server and dependencies updated
  • -
  • Monitor server resources and player activity
  • -
  • Follow the game's End User License Agreement (EULA) and Terms of Service
  • -
  • Join community forums for support and best practices
  • -
-
- -

- Last updated: November 2025 | For 7 Days to Die server hosting -

diff --git a/Panel/modules/billing/docs/7daystodie/metadata.json b/Panel/modules/billing/docs/7daystodie/metadata.json deleted file mode 100644 index 6c9e3e49..00000000 --- a/Panel/modules/billing/docs/7daystodie/metadata.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "description": "Comprehensive guide for 7 Days to Die dedicated servers with XML modding, ports, web control panel, and zombie survival hosting", - "name": "7 Days to Die", - "order": 33, - "category": "game", - "complete": true -} \ No newline at end of file diff --git a/Panel/modules/billing/docs/COMPREHENSIVE_DOCUMENTATION_UPDATE.md b/Panel/modules/billing/docs/COMPREHENSIVE_DOCUMENTATION_UPDATE.md deleted file mode 100644 index 43fce000..00000000 --- a/Panel/modules/billing/docs/COMPREHENSIVE_DOCUMENTATION_UPDATE.md +++ /dev/null @@ -1,234 +0,0 @@ -# Comprehensive Game Documentation Update - -## Date: November 22, 2025 - -## Overview - -This document describes the comprehensive enhancement of ALL game server documentation in the GSP project. The goal was to replace generic placeholder text with detailed, actionable instructions for end users who want to install game servers on their own PC (Windows or Ubuntu). - -## Problem Statement - -The original documentation had several issues: -1. **Generic Port Placeholders**: Many games showed "Check server configuration" as the port number instead of actual ports -2. **Missing Installation Details**: No specific SteamCMD commands with App IDs -3. **Vague Configuration**: Generic instructions like "check configuration files" without specifics -4. **No Startup Parameters**: Missing detailed startup command explanations -5. **Generic Troubleshooting**: Common "check the logs" advice instead of game-specific solutions - -## Solution Implemented - -### 1. Enhanced Documentation Generator Script - -Modified `tools/generate_game_docs.py` to: - -- **Extract Real Data from XML Configs**: Parse actual port numbers, configuration files, and settings from the 244 XML server configs -- **Steam App ID Database**: Added lookup table for 50+ popular games with their Steam App IDs -- **Generate Exact Commands**: Create specific SteamCMD installation commands with real App IDs -- **Parse Configuration Details**: Extract all settings from XML `replace_texts` and `custom_fields` sections -- **Include Startup Parameters**: Extract parameters from XML `cli_template` and `server_params` -- **Add Troubleshooting**: Pull game-specific troubleshooting from knowledgepack YAML data - -### 2. Processing Results - -**Processed: 134 games** -**Skipped: 15 games** (already complete and no generic text) -**Errors: 0** - -### 3. What Each Game Now Has - -Every game documentation now includes: - -1. **Quick Info Section** - - Actual port numbers (or "Varies" with explanation) - - Protocol (TCP/UDP) - - Memory requirements - - Engine information - - **Steam App ID** (e.g., 320850 for Life is Feudal) - - Recommended OS - -2. **Comprehensive Port Information** - - Complete list of ALL ports the game uses - - What each port is for - - Whether it's required or optional - - Firewall configuration examples for: - - UFW (Ubuntu/Debian) - - FirewallD (CentOS/RHEL) - - Windows Firewall - - Router port forwarding instructions - -3. **Detailed Installation Instructions** - - **For Steam games**: Exact SteamCMD commands - ```bash - steamcmd +login anonymous \ - +force_install_dir ~/gameservers/GAME \ - +app_update APPID validate \ - +quit - ``` - - Step-by-step for both Ubuntu and Windows - - System requirements - - Required dependencies - -4. **Configuration File Details** - - Exact file paths from XML configs - - What each configuration file does - - Available settings extracted from XML - - Example configurations - -5. **Startup Commands** - - Actual startup commands from XML - - Parameter explanations - - Example start scripts for Linux and Windows - - Systemd service file template - -6. **Troubleshooting** - - Game-specific issues from knowledgepack where available - - Common server startup problems - - Connection troubleshooting - - Performance optimization tips - -7. **Security Best Practices** - - Firewall configuration - - Password management - - Regular updates - - Backup strategies - -## Example: Life is Feudal - -### Before -- Default Port: "Check server configuration" -- No App ID mentioned -- Generic "download server files" instruction - -### After -- Steam App ID: **320850** -- Exact command: - ```bash - steamcmd +login anonymous \ - +force_install_dir ~/gameservers/lifeisfeudal \ - +app_update 320850 validate \ - +quit - ``` -- Configuration file: `config/world_1.xml` -- Settings: name, adminPassword, port, maxPlayers (all extracted from XML) - -## Files Modified - -### Scripts -- `tools/generate_game_docs.py` - Enhanced with comprehensive data extraction -- `tools/find_missing_game_icons.py` - NEW - Icon checker script - -### Documentation Files -- 134 `modules/billing/docs/*/index.php` files regenerated -- 134 `modules/billing/docs/*/metadata.json` files marked as complete - -### Total Changes -- 299 files changed -- ~20,000 lines of new/updated documentation -- 0 errors during processing - -## Verification - -### Generic Text Check -**Before**: 95+ games with "Check server configuration" as port placeholder -**After**: Only 1 non-game file (common-issues) has placeholder text - -### Port Information -- Real ports extracted from knowledgepack YAML -- Fallback to "Varies (see configuration)" when specific port unavailable -- All games have firewall configuration examples - -### Steam App IDs -50+ games now have correct App IDs: -- Life is Feudal: 320850 -- CS:GO: 740 -- Rust: 258550 -- Squad: 403240 -- Valheim: 896660 -- (and 45+ more) - -## Remaining Tasks - -### 1. Game Icons (Low Priority) -Only 4 games missing icons (all plugin/mod systems, not actual games): -- amxmodx -- b3 -- metamodsource -- oxide - -Use `tools/find_missing_game_icons.py` to check for missing icons. - -### 2. Future Enhancements (Optional) -- Add web search capability to find game-specific troubleshooting solutions -- Expand knowledgepack YAML with more games -- Add more Steam App IDs to the database -- Include mod/plugin installation guides - -## How to Use the Generator - -### Process All Incomplete Games -```bash -cd /home/runner/work/GSP/GSP -python3 tools/generate_game_docs.py -``` - -### Check for Missing Icons -```bash -python3 tools/find_missing_game_icons.py -``` - -## Technical Details - -### Steam App ID Database -Located in `generate_game_docs.py`, the `get_steam_app_id()` method contains a dictionary with 50+ mappings: - -```python -app_ids = { - '7daystodie': '294420', - 'arkse': '376030', - 'arma3': '233780', - # ... 45+ more -} -``` - -### XML Config Parsing -The script extracts: -- Port configurations from `replace_texts` section -- Configuration files from `configuration_files` section -- Startup parameters from `cli_template` and `server_params` -- App IDs from `mods/mod/installer_name` - -### Knowledgepack Integration -Pulls from `gameserver_knowledgepack_v2.yaml`: -- Port information with purposes -- System requirements -- Typical startup commands -- Troubleshooting issues and fixes - -## Documentation Standards - -All generated documentation follows this structure: - -1. Quick Navigation (anchor links) -2. Overview -3. Quick Info box -4. System Requirements -5. Complete Port List -6. Installation (with exact commands) -7. Configuration (file-by-file) -8. Startup Parameters -9. Troubleshooting -10. Performance Optimization -11. Security Best Practices -12. Additional Resources - -## Conclusion - -The game documentation enhancement is **COMPLETE** with 134 games now having comprehensive, actionable installation and configuration guides. The documentation is suitable for end users with no prior knowledge of game server hosting, providing step-by-step instructions for both Windows and Ubuntu. - -**Key Achievement**: Zero games now display "Check server configuration" as a port placeholder. - ---- - -*Last Updated: November 22, 2025* -*Script: tools/generate_game_docs.py* -*Processed: 134 games successfully* diff --git a/Panel/modules/billing/docs/DOCUMENTATION_ENHANCEMENT_SUMMARY.md b/Panel/modules/billing/docs/DOCUMENTATION_ENHANCEMENT_SUMMARY.md deleted file mode 100644 index 26d0a3d5..00000000 --- a/Panel/modules/billing/docs/DOCUMENTATION_ENHANCEMENT_SUMMARY.md +++ /dev/null @@ -1,250 +0,0 @@ -# Documentation Enhancement Summary - -## Overview -This document summarizes the comprehensive enhancements made to the billing module's documentation system and session handling. - -## Issues Resolved - -### 1. Documentation Page Login Button Issue ✅ -**Problem:** Documentation page showed "Login" button even when user was logged in. -**Root Cause:** docs.php used basic `session_start()` instead of the website's session name. -**Solution:** Changed to use `opengamepanel_web` session name to match rest of website. - -### 2. Cart Page Display Issue ✅ -**Problem:** Cart page didn't display when clicking menu link. -**Root Cause:** cart.php also used basic `session_start()` causing session inconsistency. -**Solution:** Changed to use `opengamepanel_web` session name for consistency. - -### 3. Documentation Content Enhancement ✅ -**Problem:** Documentation was basic, system-specific, and not comprehensive enough for SEO. -**Solution:** Created detailed, XML-independent, general hosting guides for major games. - -## Changes Made - -### Session Fixes -**Files Modified:** -- `modules/billing/docs.php` -- `modules/billing/cart.php` - -**Change:** -```php -// OLD -session_start(); - -// NEW -if (session_status() === PHP_SESSION_NONE) { - session_name("opengamepanel_web"); - session_start(); -} -``` - -This ensures the documentation and cart pages use the same session as the rest of the website (login.php, menu.php, etc.), so login state is properly detected. - -### Documentation Enhancements - -#### Games Enhanced (3 of 151 total) -1. **Minecraft Java Edition** (549 lines) -2. **CS:GO & CS2** (584 lines) -3. **Rust** (455 lines) - -#### Documentation Structure (Template for All Games) -Each comprehensive guide includes: - -1. **Navigation Bar** - Quick links to all sections -2. **Quick Info Section** - Essential details at a glance: - - Default ports (game, RCON, query) - - RAM requirements (min/recommended) - - Storage requirements - - Log file locations - - Default configurations - - Protocol information - -3. **Installation & Setup** - Complete instructions: - - System requirements (CPU, RAM, storage, bandwidth) - - Linux installation steps - - Windows installation steps - - SteamCMD usage (where applicable) - - First-time setup procedures - -4. **Server Configuration** - Detailed config guides: - - All configuration files explained - - Every parameter documented - - Example configurations - - Best practices - -5. **Startup Parameters** - Complete reference: - - All command-line parameters - - Parameter breakdown and explanations - - Startup script examples (Linux & Windows) - - Advanced optimization flags - -6. **Plugins & Mods** - Enhancement guides: - - Plugin/mod platform installation - - Popular plugins/mods list with descriptions - - Installation procedures - - Configuration examples - -7. **Troubleshooting** - Common issues & solutions: - - Server won't start - - Connection issues - - Performance problems - - Error messages and fixes - - Diagnostic commands - -8. **Performance Optimization** - Tuning guides: - - Configuration optimization - - Resource management - - Automation scripts - - Monitoring tips - - Scheduled maintenance - -9. **Additional Resources** - External links: - - Official documentation - - Community resources - - Tools and utilities - - Support forums - -## Documentation Principles - -### ✅ XML-Independent -- Does NOT pull information from panel XML files -- Does NOT reference `modules/config_games/server_configs/` -- Stands alone as general game server hosting information - -### ✅ General Hosting Focus -- Written from VPS/dedicated server perspective -- Not specific to our panel system -- Applicable to any hosting environment -- User could follow these guides on any server - -### ✅ SEO-Optimized -- Comprehensive content (400-600 lines per game) -- Covers all aspects of server hosting -- Natural keyword integration -- Designed to rank in Google search results -- Goal: Become go-to resource for game server hosting - -### ✅ Professional Quality -- Clean, modern formatting -- Code examples with syntax highlighting -- Internal navigation between sections -- Consistent structure across all games -- Production-ready commands and configs - -## Benefits - -### For Users -- Complete guides for setting up game servers -- Troubleshooting help for common issues -- Performance optimization tips -- All info in one place - -### For Business -- SEO boost - comprehensive guides rank well -- Authority building - comprehensive content -- Traffic generation - users find guides via Google -- Reduced support load - self-service documentation - -### For Future Development -- Template established for remaining 148 games -- Consistent structure makes expansion easy -- Can be enhanced incrementally -- Scalable approach - -## Remaining Games (148) - -The same comprehensive template can be applied to all remaining games: -- ARK: Survival Evolved -- Valheim -- 7 Days to Die -- Team Fortress 2 -- Garry's Mod -- Terraria -- Don't Starve Together -- Project Zomboid -- Satisfactory -- V Rising -- Palworld -- And 138 more... - -## Testing Completed - -✅ PHP syntax validation - No errors -✅ CodeQL security scan - No issues -✅ Session handling verified -✅ Documentation structure validated -✅ No XML references confirmed -✅ File permissions correct - -## Implementation Notes - -### Session Name Consistency -The entire billing module now uses `opengamepanel_web` session name: -- login.php ✅ -- register.php ✅ -- logout.php ✅ -- menu.php ✅ -- docs.php ✅ (FIXED) -- cart.php ✅ (FIXED) -- my_account.php ✅ -- All other pages ✅ - -### Documentation File Structure -``` -docs/ -├── minecraft/ -│ ├── index.php (549 lines - comprehensive) -│ ├── index_old.php (backup) -│ ├── metadata.json -│ └── icon.png -├── csgo/ -│ ├── index.php (584 lines - comprehensive) -│ ├── index_old.php (backup) -│ ├── metadata.json -│ └── icon.jpg -├── rust/ -│ ├── index.php (455 lines - comprehensive) -│ ├── index_old.php (backup) -│ ├── metadata.json -│ └── icon.png -└── [148 other games with basic docs to be enhanced] -``` - -## Future Enhancement Ideas - -1. **Add More Games** - Apply template to remaining 148 games -2. **Video Tutorials** - Link to video guides where available -3. **Interactive Commands** - Copy-to-clipboard for commands -4. **Version History** - Track game version updates -5. **Community Contributions** - Allow user-submitted tips -6. **Search Functionality** - Cross-game documentation search -7. **Translations** - Multi-language support - -## Maintenance - -### Keeping Documentation Current -- Monitor game updates and patches -- Update documentation quarterly -- Track breaking changes in games -- Community feedback integration - -### Backup Strategy -All original documentation files are preserved as `index_old.php` in each game folder for reference and potential rollback if needed. - -## Conclusion - -The documentation system is now: -- ✅ Fully functional with correct session handling -- ✅ Comprehensive for 3 major games (Minecraft, CS:GO/CS2, Rust) -- ✅ Template-based for easy expansion to remaining games -- ✅ SEO-optimized for Google search ranking -- ✅ XML-independent and general hosting focused -- ✅ Production-ready and tested - -**Status:** Ready for review and deployment - ---- - -*Created: November 8, 2024* -*Last Updated: November 8, 2024* - diff --git a/Panel/modules/billing/docs/DOCUMENTATION_EXPANSION_PLAN.md b/Panel/modules/billing/docs/DOCUMENTATION_EXPANSION_PLAN.md deleted file mode 100644 index 351e52c1..00000000 --- a/Panel/modules/billing/docs/DOCUMENTATION_EXPANSION_PLAN.md +++ /dev/null @@ -1,271 +0,0 @@ -# Game Server Documentation Expansion Plan - -## Executive Summary - -This document outlines the comprehensive plan for enhancing documentation for all 151 games supported by the GameServerPanel billing module. As of the current phase, 6 games have comprehensive documentation (200+ lines each), with 145 games remaining at basic level (67 lines average). - -## Completed Games (6/151) - -### Phase 1 - Already Enhanced (3 games) -1. **Minecraft Java Edition** (549 lines) - Complete -2. **CS:GO & CS2** (584 lines) - Complete -3. **Rust** (455 lines) - Complete - -### Phase 2 - Recently Enhanced (3 games) -4. **Valheim** (325 lines) - Complete -5. **ARK: Survival Evolved** (303 lines) - Complete -6. **Terraria** (359 lines) - Complete - -## Documentation Enhancement Template - -Each enhanced game documentation includes: - -### 1. Navigation Bar -- Quick links to all major sections -- Improves user experience and SEO -- Anchor links for easy jumping - -### 2. Quick Info Section (Required Details) -- Default ports (game, query, RCON) -- Protocol (TCP/UDP) -- RAM requirements (min/recommended) -- CPU recommendations -- Storage requirements -- SteamCMD App ID (if applicable) -- Max players -- Config file locations -- Log file paths - -### 3. Installation & Setup -- System requirements breakdown -- Windows installation steps -- Linux installation steps (preferred with SteamCMD) -- macOS installation (if supported) -- First-time setup procedures -- Directory structure explanation - -### 4. Server Configuration -- Configuration file locations -- Complete parameter reference -- Example configurations -- Best practices for settings -- Multiple configuration scenarios - -### 5. Startup Parameters -- Command-line options table -- Parameter descriptions -- Example startup scripts (Windows & Linux) -- Advanced optimization flags -- Launch parameter combinations - -### 6. Port Forwarding & Networking -- Required ports list with protocols -- Router configuration examples -- Firewall rules (UFW for Linux, Windows Firewall) -- NAT configuration guidance -- DMZ considerations - -### 7. Plugins/Mods/Extensions -- Popular mod loaders (if applicable) -- Plugin installation procedures -- Popular plugins/mods list -- Configuration examples -- Compatibility notes - -### 8. Troubleshooting -- Server won't start solutions -- Connection issues diagnosis -- Performance problems -- Common error messages -- Log file analysis -- Diagnostic commands - -### 9. Performance Optimization -- Server sizing guidelines by player count -- Resource management tips -- Configuration tuning -- Automated maintenance -- Monitoring recommendations - -### 10. Admin Tools & Commands -- Console commands reference -- Admin authentication -- User management -- Server control commands -- Debugging tools - -### 11. Backup & Recovery -- Backup strategy recommendations -- Automated backup scripts (Linux/Windows) -- World/save file locations -- Recovery procedures -- Disaster recovery planning - -### 12. Additional Resources -- Official documentation links -- Community resources -- Forums and support -- Tool recommendations -- Related guides - -## Priority Game List (Next 20 Games) - -### High Priority (Most Popular) -1. Team Fortress 2 (TF2) -2. Garry's Mod -3. Don't Starve Together -4. Left 4 Dead 2 -5. Counter-Strike: Source -6. Counter-Strike 1.6 -7. Project Zomboid -8. V Rising -9. Satisfactory -10. Conan Exiles - -### Medium Priority (Popular) -11. 7 Days to Die -12. Killing Floor 2 -13. Insurgency Sandstorm -14. Squad -15. Arma 3 -16. DayZ -17. Space Engineers -18. Eco -19. Factorio -20. Unturned - -## Research Sources for Each Game - -### Primary Sources -1. Official game websites and documentation -2. Official game wikis (Fandom, Wiki.gg) -3. Steam Community guides -4. Developer documentation - -### Secondary Sources -1. Hosting provider knowledge bases (Nitrado, GTXGaming, etc.) -2. Reddit communities (r/[gamename]) -3. GitHub repositories for tools/mods -4. YouTube server setup tutorials -5. Forum threads (AlliedModders, SRCDS, etc.) - -### Information to Gather -- SteamCMD App ID -- Default ports and protocols -- Minimum and recommended hardware -- Configuration file formats and locations -- Startup parameters and options -- Common troubleshooting issues -- Popular mods/plugins -- Admin tools and commands -- Performance optimization tips - -## Implementation Strategy - -### Batch Processing Approach -1. **Research Phase** - Gather information for 5-10 games at once -2. **Documentation Phase** - Write comprehensive guides using template -3. **Review Phase** - Syntax check, link validation, formatting -4. **Commit Phase** - Commit in batches to track progress - -### Quality Standards -- Minimum 300 lines per enhanced game -- All sections from template must be present -- At least 5 external resource links -- Proper formatting with code blocks and tables -- No syntax errors (PHP validation) -- SEO-optimized content - -### Estimated Timeline -- **Per game:** 30-45 minutes (research + writing) -- **Batch of 10:** 5-8 hours -- **All 145 remaining:** 72-108 hours total work - -## Automation Opportunities - -### Possible Automations -1. **Port extraction** from XML config files -2. **Template generation** with game-specific placeholders -3. **Batch PHP syntax checking** -4. **Link validation** across all docs -5. **Formatting consistency** checks - -### Manual Work Required -- Game-specific troubleshooting research -- Community resource identification -- Mod/plugin ecosystem understanding -- Performance optimization specifics -- Platform-specific considerations - -## Progress Tracking - -### Current Status -- **Enhanced:** 6 games (4% complete) -- **Remaining:** 145 games (96% to do) -- **Total Documentation Lines:** ~2,575 lines (enhanced games only) -- **Average Lines per Enhanced Game:** 429 lines - -### Completion Milestones -- **10% (15 games):** Target date TBD -- **25% (38 games):** Target date TBD -- **50% (76 games):** Target date TBD -- **75% (113 games):** Target date TBD -- **100% (151 games):** Target date TBD - -## Benefits of Completion - -### For Users -- Comprehensive self-service documentation -- Reduced setup time and frustration -- Better troubleshooting guidance -- Performance optimization tips -- Community resource discovery - -### For Business -- **SEO boost** - 145 new comprehensive pages ranking for game server hosting -- **Authority building** - Comprehensive resource destination -- **Traffic generation** - Organic search traffic from game communities -- **Support reduction** - Self-service documentation reduces tickets -- **Competitive advantage** - Most comprehensive game server hosting documentation - -### For Search Rankings -- Long-form content (300+ lines per game) -- Natural keyword integration -- Internal linking structure -- External authoritative links -- Regular update potential -- User engagement (navigation, resource links) - -## Maintenance Plan - -### Regular Updates -- **Quarterly review** - Check for game updates, new versions -- **Version tracking** - Monitor major game releases -- **Link validation** - Ensure external resources remain valid -- **Community feedback** - Incorporate user suggestions -- **Error corrections** - Fix reported issues promptly - -### Update Triggers -- Major game version releases -- New DLC or expansion launches -- Significant mod ecosystem changes -- Breaking configuration changes -- New hosting best practices - -## Next Steps - -1. **Immediate:** Complete next batch of 10-15 popular games -2. **Short-term:** Develop automation for repetitive tasks -3. **Mid-term:** Complete top 50 most popular games -4. **Long-term:** Achieve 100% documentation coverage -5. **Ongoing:** Maintain and update as games evolve - -## Conclusion - -The documentation expansion project is critical for establishing the platform as the authoritative resource for game server hosting. While comprehensive, the systematic approach outlined ensures quality, consistency, and long-term maintainability. - ---- - -**Created:** November 2024 -**Last Updated:** November 2024 -**Status:** In Progress (6/151 games enhanced) diff --git a/Panel/modules/billing/docs/GAME_SERVER_LIST.md b/Panel/modules/billing/docs/GAME_SERVER_LIST.md deleted file mode 100644 index 8f268f9a..00000000 --- a/Panel/modules/billing/docs/GAME_SERVER_LIST.md +++ /dev/null @@ -1,282 +0,0 @@ -# Multiplayer Games with Dedicated Server Support - -**Last Updated:** November 10, 2025 - -This list contains multiplayer games that support dedicated server hosting, ordered by popularity (most to least popular based on player counts, community activity, and hosting demand). - -## Legend -- ~~Strikethrough~~ = Documentation complete -- Normal text = Documentation incomplete (shows with "TODO:" prefix on site) - ---- - -## Top Tier (Extremely Popular) - -1. ~~Minecraft~~ - Sandbox building and survival -2. Counter-Strike 2 - Tactical FPS (CS2 not yet in GSP, covered in CS:GO docs) -3. ~~Counter-Strike: Global Offensive~~ - Tactical FPS & CS2 (Source 2 engine) -4. ~~Rust~~ - Survival crafting and PvP -5. ~~Arma 3~~ - Military simulation -6. ~~ARK: Survival Evolved~~ - Dinosaur survival -7. Garry's Mod - Sandbox multiplayer -8. ~~Valheim~~ - Viking survival co-op -9. ~~7 Days to Die~~ - Zombie survival crafting -10. ~~Terraria~~ - 2D sandbox adventure - -## High Popularity - -11. ~~DayZ Standalone~~ - Zombie survival -12. ~~Team Fortress 2~~ - Team-based FPS -13. ~~Left 4 Dead 2~~ - Co-op zombie shooter -14. Squad - Tactical military FPS -15. ~~Killing Floor 2~~ - Co-op wave shooter -16. 21. ~~Insurgency: Sandstorm~~ - Tactical FPS -17. Space Engineers - Space sandbox engineering -18. Don't Starve Together - Survival co-op -19. ~~Conan Exiles~~ - Survival and building -20. Unturned - Zombie survival - -## Medium-High Popularity - -21. ~~Counter-Strike: Source~~ - Classic Source engine tactical FPS -22. ~~Counter-Strike 1.6~~ - Original CS (GoldSrc engine) -23. ~~Arma 2: Operation Arrowhead~~ - Military sim (DayZ Mod base) -24. ~~Arma 2: Combined Operations~~ - Arma 2 + OA (DayZ Mod) -25. ~~Left 4 Dead~~ - Co-op zombie shooter -26. 13. ~~Killing Floor~~ - Co-op horror shooter -27. Insurgency - Tactical FPS -28. The Forest - Survival horror co-op -29. Starbound - 2D space exploration -30. Project Zomboid - Zombie survival RPG - -## Medium Popularity - -31. Factorio - Factory building automation -32. Eco - Environmental survival simulation -33. V Rising - Vampire survival -34. Satisfactory - Factory building 3D -35. Stationeers - Space station engineering -36. Mordhau - Medieval combat -37. Red Orchestra 2 - WWII tactical shooter -38. Rising Storm 2: Vietnam - Tactical FPS -39. Day of Infamy - WWII FPS -40. Pavlov VR - VR tactical shooter - -## Legacy / Niche Popular - -41. Arma 2 - Military simulation -42. Arma Reforger - Modern military sim -43. Team Fortress Classic - Classic team FPS -44. Day of Defeat: Source - WWII team shooter -45. Natural Selection 2 - FPS/RTS hybrid -46. Nuclear Dawn - FPS/RTS hybrid -47. Dystopia - Cyberpunk source mod -48. Pirates, Vikings and Knights II - Medieval combat mod -49. Zombie Master: Reborn - Asymmetric zombie game -50. The Ship - Murder mystery multiplayer - -## Specialized / Modding Communities - -51. Multi Theft Auto (MTA) - GTA multiplayer mod -52. San Andreas Multiplayer (SAMP) - GTA SA multiplayer -53. FiveM - GTA V multiplayer mod -54. Just Cause 2 Multiplayer - JC2 mod -55. Mafia 2 Online - Mafia 2 multiplayer -56. Vice City Multiplayer - GTA VC multiplayer -57. IV Multiplayer - GTA IV multiplayer - -## Survival & Building Games - -58. Hurtworld - Survival crafting -59. Miscreated - Survival horror -60. Reign of Kings - Medieval survival -61. Life is Feudal - Medieval MMO survival -62. Empyrion: Galactic Survival - Space survival -63. ATLAS - Pirate MMO survival -64. PixARK - Voxel ARK variant -65. Wurm Unlimited - Medieval sandbox MMO - -## Racing & Simulation - -66. Assetto Corsa - Racing simulation -67. Euro Truck Simulator 2 - Truck driving sim -68. BeamNG.drive - Vehicle physics sim -69. Trackmania Nations Forever - Racing arcade -70. Trackmania - Modern racing arcade - -## Tactical & Military Shooters - -71. Battlefield 2 - Combined arms warfare -72. Battlefield: Bad Company 2 - Modern warfare -73. Call of Duty (original) - WWII FPS -74. Call of Duty 2 - WWII FPS -75. Call of Duty 4: Modern Warfare - Modern FPS -76. Call of Duty: World at War - WWII FPS -77. Call of Duty: Modern Warfare 2 - Modern FPS -78. Call of Duty: Modern Warfare 3 - Modern FPS -79. Call of Duty: United Offensive - WWII expansion -80. Call of Duty: Black Ops - Cold War FPS -81. Medal of Honor: Allied Assault - WWII FPS -82. Medal of Honor: Spearhead - WWII expansion -83. Medal of Honor: Breakthrough - WWII expansion -84. Homefront - Modern warfare FPS -85. Sniper Elite V2 - Tactical sniper game - -## Classic Source Engine Games - -86. Half-Life 2: Deathmatch - Physics-based deathmatch -87. Half-Life Deathmatch - Classic deathmatch -88. Deathmatch Classic - Quake-style deathmatch -89. Synergy - Half-Life 2 co-op mod -90. The Hidden: Source - Asymmetric multiplayer mod -91. Fistful of Frags - Western multiplayer mod -92. GoldenEye: Source - GoldenEye remake mod - -## Arena Shooters - -93. Quake 3 Arena - Classic arena shooter -94. Quake 4 - Sci-fi arena shooter -95. Unreal Tournament 99 - Classic arena shooter -96. Unreal Tournament 2004 - Arena shooter -97. Unreal Tournament 3 - Modern arena shooter -98. Warsow - Fast-paced arena shooter -99. Xonotic - Open-source arena shooter -100. Nexuiz - Arena shooter -101. Alien Arena - Sci-fi arena shooter - -## RTS & Strategy - -102. Age of Chivalry - Medieval Source mod -103. Chivalry: Medieval Warfare - Medieval slasher - -## Zombie & Horror Co-op - -104. No More Room in Hell - Realistic zombie co-op -105. Brain Bread 2 - Zombie co-op shooter - -## MMO & Persistent Worlds - -106. Soldat - 2D multiplayer shooter -107. OpenTTD - Transport simulation -108. Minetest - Open-source voxel game -109. Free Orion - Space strategy -110. Freeciv - Civilization-like strategy - -## Voice & Communication Servers - -111. TeamSpeak 2 - Voice communication -112. TeamSpeak 3 - Voice communication -113. Mumble - Low-latency voice chat -114. Ventrilo - Voice communication - -## Streaming & Broadcasting - -115. Shoutcast - Internet radio streaming -116. SinusBot - TeamSpeak music bot - -## Specialized Game Servers - -117. BattlEye - Anti-cheat system -118. BigBrotherBot - Game server admin bot -119. SpunkyBot - Urban Terror admin bot -120. Jedi Knight 2: Jedi Outcast - Star Wars FPS -121. Jedi Knight: Jedi Academy - Star Wars FPS -122. Halo: Combat Evolved - Sci-fi FPS -123. Serious Sam HD: First Encounter - Co-op shooter -124. Serious Sam HD: Second Encounter - Co-op shooter -125. Blood Frontier - Arena shooter -126. Citadel: Forged with Fire - Fantasy survival -127. Wreckfest - Demolition racing -128. Alien Swarm: Reactive Drop - Top-down co-op shooter -129. Aliens vs Predator - Sci-fi multiplayer FPS - -## Flight Simulators - -130. IL-2 Sturmovik - WWII flight sim -131. FlightGear Multi-Simulator (FGMS) - Open-source flight sim - -## Legacy & Retro Games - -132. Half-Life TV - HL spectator system -133. Ricochet - Disc-throwing arena game -134. Smashball - Sport/combat hybrid -135. Condition Zero - Counter-Strike variant - -## Mods & Total Conversions - -136. Counter-Strike: Promod - Competitive CS mod -137. Age of Chivalry - Source engine medieval mod -138. Empires Mod - RTS/FPS hybrid mod -139. Epsilon Source Mod - Source engine mod -140. Obsidian Conflict - Half-Life 2 co-op mod -141. Pirates, Vikings & Knights - Medieval combat -142. Smoking Guns - Western shooter mod -143. Soldier of Fortune - Tactical shooter -144. Urban Terror - Tactical realism mod -145. Wolfenstein: Return to Castle Wolfenstein - WWII FPS -146. Zombie Panic: Source - Zombie infection mod -147. ROR Server - Rigs of Rods multiplayer - -## VPS/Panel Management Tools - -148. Getting Started Guide - Server hosting basics -149. Common Issues - General troubleshooting guide - ---- - -## Summary Statistics - -- **Total Games Listed:** 149 -- **Documentation Complete:** 8 (5.4%) - - ~~Minecraft~~ - - ~~Arma 3~~ - - ~~Arma 2: Operation Arrowhead~~ - - ~~Arma 2: Combined Operations~~ - - ~~DayZ Standalone~~ - - ~~Counter-Strike: Global Offensive~~ (includes CS2) - - ~~Counter-Strike: Source~~ - - ~~Counter-Strike 1.6~~ -- **Documentation Incomplete:** 141 (94.6%) -## Next Priority Games for Documentation - -### Phase 3 - Counter-Strike Family ✅ COMPLETE -All Counter-Strike games now documented: -- ~~Counter-Strike: Global Offensive~~ (includes CS2 coverage) -- ~~Counter-Strike: Source~~ -- ~~Counter-Strike 1.6~~ - -### Phase 4 - Popular Survival Games (Target: 6 games) -4. Counter-Strike 1.6 - -### Phase 4 - Popular Survival Games (Target: 6 games) -1. Rust -2. ARK: Survival Evolved -3. Valheim -4. Terraria -5. 7 Days to Die -6. Conan Exiles - -### Phase 5 - Co-op Shooters (Target: 6 games) -1. Left 4 Dead 2 -2. Left 4 Dead -3. Killing Floor 2 -4. Killing Floor -5. Team Fortress 2 -6. Insurgency: Sandstorm - ---- - -**Note:** Popularity rankings based on: -- Current Steam player counts (November 2025) -- Community activity and server hosting demand -- Active modding communities -- Longevity and continued support - -**Documentation Standard:** Each complete game includes: -- ✅ Comprehensive ports table (all ports with purposes) -- ✅ Firewall configurations (UFW, FirewallD, Windows, iptables) -- ✅ Startup parameters with detailed explanations -- ✅ Troubleshooting sections with specific solutions -- ✅ Performance optimization tips -- ✅ Security best practices -- ✅ Resource links with citations diff --git a/Panel/modules/billing/docs/GENERATION_README.md b/Panel/modules/billing/docs/GENERATION_README.md deleted file mode 100644 index b6075c30..00000000 --- a/Panel/modules/billing/docs/GENERATION_README.md +++ /dev/null @@ -1,199 +0,0 @@ -# Game Server Documentation Generation - -## Overview - -This directory contains comprehensive game server hosting documentation for 143+ games. The documentation follows a consistent template structure based on the Minecraft server guide. - -## Generated Documentation - -In November 2024, we generated comprehensive documentation for 98 game servers that were previously in the "todo" category. Each game now has: - -- **Quick Navigation Menu** - Easy access to all sections -- **Quick Info** - Default ports, protocols, RAM requirements, engine info -- **Network Ports** - Detailed port tables with firewall configuration examples -- **Installation & Setup** - System requirements and installation steps -- **Server Configuration** - Configuration files and essential settings -- **Startup Parameters** - Command-line parameters and service setup -- **Troubleshooting** - Common issues and solutions -- **Performance Optimization** - Tuning and monitoring tips -- **Security Best Practices** - Firewall, passwords, updates, DDoS protection -- **Additional Resources** - External references and community links - -## Documentation Structure - -Each game documentation folder contains: - -``` -gamename/ -├── index.php - Main documentation content -├── metadata.json - Category, name, description, order -└── icon.png/jpg - Game icon (optional) -``` - -### metadata.json Format - -```json -{ - "name": "Game Name", - "description": "Brief description for the game", - "category": "game", - "order": 1 -} -``` - -## Categories - -Documentation is organized into categories: - -- **game** - Game server documentation (143+ servers) -- **mods** - Mod/plugin documentation -- **panel** - Panel-specific documentation -- **troubleshooting** - General troubleshooting guides -- **other** - Other documentation - -## Generation Tool - -The documentation was generated using the `generate_game_docs.py` script located in `/tools/`. - -### Data Sources - -The generator uses multiple data sources: - -1. **XML Configurations** (`/modules/config_games/server_configs/*.xml`) - - Port configurations - - Configuration file paths - - Custom fields and parameters - -2. **YAML Knowledgepack** (`/modules/billing/docs/gameserver_knowledgepack_v2.yaml`) - - Network port details - - System requirements - - Startup commands - - Troubleshooting tips - - External references - -3. **Template Structure** (Based on Minecraft documentation) - - Consistent formatting - - Comprehensive coverage - - User-friendly navigation - -### Running the Generator - -```bash -cd /home/runner/work/GSP/GSP -python3 tools/generate_game_docs.py -``` - -The script will: -1. Load XML configurations and YAML knowledgepack -2. Find all folders with `category: "todo"` in metadata.json -3. Generate comprehensive PHP documentation for each game -4. Update metadata.json to change category to "game" - -## Games Documented - -The following games now have comprehensive hosting documentation: - -### Action/FPS Games -- Aliens vs Predator, Call of Duty series (COD, COD2, COD4, MW2, MW3, WAW, Black Ops) -- Counter-Strike variants (CS 1.6, CS:CZ, CS:S, CS:GO, CS:Promod, CS 2D) -- Battlefield 2, Battlefield Bad Company 2 -- Half-Life variants (HLDM, HL2DM, HLTV) -- Insurgency, Medal of Honor series (MOHAA, MOHBR, MOHSP, MOHSPDEMO) -- Quake 3, Quake 4, Sniper Elite V2 - -### Source Engine Games -- Dystopia, Hidden: Source, Natural Selection 2, Nuclear Dawn -- Pirates Vikings and Knights II, Zombie Panic Source, Synergy -- Brain Bread 2, Day of Defeat: Source - -### Open World/Survival -- Atlas, Hurtworld, Life is Feudal, Miscreated -- Reign of Kings, The Forest, Space Engineers -- Wurm Unlimited, PixArk - -### Racing/Simulation -- Assetto Corsa, Euro Truck Simulator 2 -- Trackmania Nations, Trackmania Forever -- Wreckfest - -### Multiplayer Mods -- FiveM (GTA V), Multi Theft Auto (GTA SA/VC) -- IV:MP (GTA IV), JC:MP (Just Cause 2) -- Mafia II Online, Epoch Mod - -### Strategy/Building -- Avorion, Colony Survival, Eco -- FreeCol, OpenTTD, Empyrion Galactic Survival - -### Arena/Combat -- Jedi Knight 2, Jedi Knight: Jedi Academy -- Mount & Blade: Warband, Mordhau -- Soldat, Smashball, Blood Frontier -- Citadel: Forged with Fire, Red Orchestra 2, Rising Storm 2 -- Arma Reforger, Homefront - -### Voice/Communication -- TeamSpeak 2, TeamSpeak 3, Mumble, Ventrilo -- SinusBot, Shoutcast, Shoutcast Bot - -### Classic/Retro -- Unreal Tournament 99, UT2004, UT3 -- Serious Sam HD TFE, Serious Sam HD TSE -- Roadkill, Wolfenstein: Return to Castle Wolfenstein -- Enemy Territory, Warsow, Nexuiz, Xonotic -- IL-2 Sturmovik, Halo: Combat Evolved - -### Other -- Feed the Beast (Minecraft modpack) -- Spigot MC (Minecraft server software) -- Rigs of Rods, Flight Gear Multiplayer Server -- Virtual Box, Smokinguns, DMC, Gearbox, ESMod -- SpunkyBot, AoC, SMS - -## Viewing Documentation - -Access the documentation through the billing website: - -``` -/modules/billing/docs.php -``` - -Or view a specific game: - -``` -/modules/billing/docs.php?action=view&doc=gamename -``` - -## Maintenance - -To update or regenerate documentation: - -1. Update data sources (XML configs, YAML knowledgepack) -2. Modify the generator script if needed -3. Run the generator script -4. Commit changes to the repository - -## Template Customization - -To customize the documentation template, edit the `build_php_content()` method in `generate_game_docs.py`. - -The template includes: -- Inline CSS styling matching the site theme -- Responsive design for mobile/desktop -- Color-coded information boxes -- Syntax-highlighted code blocks -- Professional formatting - -## Contributing - -When adding new game documentation: - -1. Create a folder with the game's slug name -2. Add metadata.json with game information -3. Add icon.png or icon.jpg (optional) -4. Either manually create index.php or add to "todo" category and run generator -5. Update this README if adding new categories - -## License - -Documentation follows the same license as the GSP project. See main repository LICENSE file. diff --git a/Panel/modules/billing/docs/Game Server Hosting Reference (Multiplayer PC Games).pdf b/Panel/modules/billing/docs/Game Server Hosting Reference (Multiplayer PC Games).pdf deleted file mode 100644 index 2370e66ce8a4d836731a901b1a7167e69dc45662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 135853 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�&kt55)yr8jSvWl+Y)GfK^b_V!}(onT=rG#c{>*g z1}$=FH=6IUm3p6ff9m(PYm%Jc;GDE5Zfw+FBBFci--E+__6^nM@PMRR`>Yz0|pyDVfk^?66*1?uK>UP&Wz_Sq8P#uQ zDFDKMCiI*$Ay^`sq#!(oW4vt|$ztOB=Y3bBkJFU_Yp7Ml#d=fI z(pghxHk|C00+T2Hq}KY#wf7b1FzlMr)|uE8Wb$dlh4EjkSc6u@bLF%2^o_vF==mGc zz5cYwvLYe(8{Sjn4XpP-8Y;sP^b+CHf7LgdOd419s$!*fn0b`<31c0*#-u~T*g~e$BUO{{3x)an!C;#$VM!d+VfyM=c!OnOsEqPb z((fiW{gTc`RVpKWoo<~FLhE{PhPrGPLp-DPLTR$u48st_b4*!Sc=BF_Xhzui-rtGM zCoZ3-XrZFdvs74Y9K_&q?H^wyC4lVOwX4EP@?z$Faspkd;SuhVc!f{tN$OnX9IB+y zTKpzc2QACHq;@i1Z(n=@q=i)42>Rq%*DJPCnRmOvthHr%UcnNs9L`Zm%=Q`ovwbd* zppGNsJk>JEP`tPy)VgQf2HJcCdcGdWb*W^p=aCkMEVBELOpL{RHEX`-e0(JAK!Bm- z;LO(tV1)}}lwy}~+|uDWE_$cOp?vLvXywQSrf;1)DVf`38;N07su9!FuH$L@3}uX3 zt=N@42$n9toO=!CiA_}mfp$`W&yHR~Qd+F4%F4X8DKjsJY&8QrX*#qwX7I~>qV=zO(veGxRcbQO4`VzJjlLRI6v%_2+TR~Qzk>P z5j`MqZ=}&FT>rld;@-Xr22qiV!O4<1j^CZV>nn<@)>a)xu2Ib7JwtnXC9={t#4Gj0 zYIG>{qK_4IEI85Nrl6Wz1Xbo%2Hc@lP`gg^q1s;{hwRlo(%a9*i--;YHoKLO7DqWp+U1jH^*Jw0qwLpS~V1NKf5w1ls zSv96d|Ev&?MQt?y+nHRtO;5nZ5))I?BDLyYOomW`ZOQ6i2)ON=i5BdinUZj)&}D-I zCwX%!{6q`!=sFLTq}-5W+Gv!wJ%|Q4Ux){_4;&Vx5j>(i8I(jch zWLtmd;sHa$4fUaSMuH^CW3;CIQPA6Hc_=uurlVNrv`tyd)*IA@WQXvB#_v(V0*F3w z>jV*reL%DcTmAMjj)Jwa@aB#(NkMwnIkdKP^|p+f4@o+VQM4Vj%l#lTY+bvu1b?D$fc zBcmUB6rOu08sDsPP|x(;;%XxS;w}Bu?pL#6q=7-Sfg!XYtR;JYGUTxf_KD*BX>*1! z$R;YNqJsmfhTnG+gr(MAIf(}v)l-!T>f(pT02127S{}3WVYz8)3^mfSwc46V@3`}N zvUOF;q+HtIw5d>jQq*XmH_5vW z$1n~ibZ2|ntMPIBa;83eJ*}+dburSB(y6%0apaxr@Er))RlE2 zi4XSz(L+5mf3QFMsa1o#r*&}sDF;EN?{RJZ5#KnAV(_ zGat4{5~7TD{YJ#r98CWyCNF^)sQoHz3HEw{8kAgUfsKwf8__nLAP-P`>wpX2ly$&z zl6;Ff*|~TTqb9i95aV&+ybz^mIchm=5VF6nv9EEfjl;Kq;R1bZOB5@Ge;O0!yl#*d zOcnV≩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%0A;AuRUw6jow`D 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 diff --git a/Panel/modules/billing/docs/IMPLEMENTATION_SUMMARY.md b/Panel/modules/billing/docs/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 5b2e99df..00000000 --- a/Panel/modules/billing/docs/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,198 +0,0 @@ -# Game Server Documentation Generation - Implementation Summary - -## Task Completed - -Successfully generated comprehensive hosting documentation for **98 game servers** that were previously in the "TODO" category, bringing the total documented game servers to **143**. - -## Approach - -### 1. Analysis Phase -- Examined the existing Minecraft documentation as the reference template -- Analyzed the `docs.php` file to understand the documentation framework -- Reviewed available data sources: - - XML server configurations (`/modules/config_games/server_configs/*.xml`) - - YAML knowledgepack (`/modules/billing/docs/gameserver_knowledgepack_v2.yaml`) - - Existing documentation structure - -### 2. Implementation -Created a Python script (`tools/generate_game_docs.py`) that: -- Loads and parses 244 XML configuration files -- Loads YAML knowledgepack with detailed info for 20 games -- Finds all folders with `category: "todo"` in their metadata.json -- Generates comprehensive PHP documentation for each game -- Updates metadata.json from "todo" to "game" category - -### 3. Documentation Template - -Each generated documentation includes: - -#### Navigation & Overview -- Quick navigation menu with anchor links -- Game name and comprehensive introduction -- Target audience: VPS/dedicated server administrators - -#### Quick Info Section -- Default port and protocol -- Minimum RAM requirements -- Game engine information -- Configuration file paths (from XML) - -#### Network Ports -- Detailed port tables with purpose descriptions -- Firewall configuration for UFW, FirewallD, iptables, Windows -- Port security best practices - -#### Installation & Setup -- System requirements -- Installation steps for Linux and Windows -- SteamCMD instructions (where applicable) -- Dependency information (from knowledgepack) - -#### Server Configuration -- Essential settings overview -- Configuration file documentation (from XML) -- Server console commands -- Admin/RCON setup - -#### Startup Parameters -- Basic and advanced startup commands (from knowledgepack) -- Parameter explanations -- Start script examples for Linux/Windows -- systemd service configuration - -#### Troubleshooting -- Common issues and solutions (from knowledgepack) -- Server won't start scenarios -- Connection problems -- Performance issues -- Log file locations - -#### Performance Optimization -- Server tuning recommendations -- Operating system optimization -- Monitoring suggestions -- Backup strategies - -#### Security -- Firewall configuration -- Password best practices -- Regular updates -- Access control -- DDoS protection - -#### Resources -- External references (from knowledgepack) -- Community links -- Official documentation - -## Data Integration - -The generator intelligently combines data from multiple sources: - -1. **For games in YAML knowledgepack** (20 games like COD4, Dystopia, HLDM): - - Accurate port numbers and protocols - - Detailed port tables with multiple ports - - System requirements (RAM, CPU, dependencies) - - Startup command examples - - Specific troubleshooting tips - - External reference links - -2. **For games with XML configs** (all games): - - Configuration file paths - - Port configuration details - - Custom field documentation - -3. **For all games**: - - Consistent template structure - - Professional formatting - - Complete hosting guide sections - -## Games Documented (98 New + 45 Existing = 143 Total) - -### Newly Documented Games Include: - -**Action/FPS**: Aliens vs Predator, Battlefield series, Call of Duty variants, Counter-Strike variants, Half-Life variants, Insurgency, Medal of Honor series, Quake series, Sniper Elite - -**Source Engine**: Dystopia, Hidden: Source, Natural Selection 2, Nuclear Dawn, Pirates Vikings Knights II, Zombie Panic Source, Synergy, Brain Bread 2 - -**Open World/Survival**: Atlas, Hurtworld, Life is Feudal, Miscreated, Reign of Kings, The Forest, Space Engineers, Wurm Unlimited, PixArk - -**Racing/Sim**: Assetto Corsa, Euro Truck Simulator 2, Trackmania series, Wreckfest - -**Multiplayer Mods**: FiveM, Multi Theft Auto, IV:MP, JC:MP, Mafia II Online, Epoch Mod - -**Strategy/Building**: Avorion, Colony Survival, Eco, FreeCol, OpenTTD, Empyrion - -**Arena/Combat**: Jedi Knight series, Mount & Blade, Mordhau, Soldat, Smashball, Blood Frontier, Citadel, Red Orchestra 2, Rising Storm 2, Arma Reforger, Homefront - -**Voice/Communication**: TeamSpeak 2/3, Mumble, Ventrilo, SinusBot, Shoutcast - -**Classic/Retro**: Unreal Tournament series, Serious Sam HD, Roadkill, Wolfenstein RTCW, Enemy Territory, Warsow, Nexuiz, Xonotic, IL-2, Halo CE - -**Other**: Feed the Beast, Spigot MC, Rigs of Rods, Flight Gear, and more - -## Technical Details - -- **Script**: `tools/generate_game_docs.py` (968 lines) -- **Template Size**: ~370-420 lines of PHP per game -- **Files Modified**: 198 files (99 index.php + 99 metadata.json) -- **Total Documentation**: ~37,000 lines of comprehensive content -- **Syntax Validation**: All PHP files validated with `php -l` -- **Categories Updated**: All TODO → game -- **Remaining TODO**: 0 - -## Quality Assurance - -1. **Template Consistency**: All docs follow the Minecraft template structure -2. **PHP Syntax**: All files validated for syntax errors -3. **Data Accuracy**: Port info and configurations pulled from authoritative sources -4. **Formatting**: Professional styling with inline CSS matching site theme -5. **Navigation**: Quick navigation menu for easy access -6. **Completeness**: All required sections included - -## Files Created/Modified - -### New Files -- `tools/generate_game_docs.py` - Documentation generator script -- `modules/billing/docs/GENERATION_README.md` - Comprehensive documentation README -- `modules/billing/docs/IMPLEMENTATION_SUMMARY.md` - This file - -### Modified Files -- 98 × `index.php` files - Comprehensive game server documentation -- 98 × `metadata.json` files - Category updated from "todo" to "game" - -## Usage - -The documentation is accessible through: -- Main docs page: `/modules/billing/docs.php` -- Individual game: `/modules/billing/docs.php?action=view&doc=gamename` - -## Future Maintenance - -The generator script can be reused to: -1. Generate docs for new games (add folder with metadata.json set to "todo", run script) -2. Regenerate existing docs when data sources are updated -3. Maintain consistency across all documentation - -## Benefits - -1. **User Experience**: Comprehensive, professional documentation for 143 game servers -2. **SEO**: Rich content for search engine discovery -3. **Conversion**: Detailed guides drive awareness of hosting services -4. **Maintenance**: Automated generation ensures consistency -5. **Scalability**: Easy to add new games following the same process - -## Testing Recommendations - -To fully validate the implementation: -1. Start a PHP development server or configure Apache/Nginx -2. Navigate to `/modules/billing/docs.php` -3. Verify all 143 game servers appear in the list -4. Click through several game documentation pages -5. Test navigation menu functionality -6. Verify styling matches site theme -7. Check responsive design on mobile devices - -## Conclusion - -Successfully completed the task of generating comprehensive documentation for all games in the "TODO" category. The documentation follows the Minecraft template structure, includes all relevant details for VPS/dedicated server hosting, and is accessible through the existing docs.php page. The generator tool is saved for future use. diff --git a/Panel/modules/billing/docs/README.md b/Panel/modules/billing/docs/README.md deleted file mode 100644 index 949daa75..00000000 --- a/Panel/modules/billing/docs/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Documentation System - -## Overview - -The billing module now includes a flexible documentation browser that organizes documentation into categories with an easy-to-navigate interface. - -## Structure - -Documentation is organized in the `/modules/billing/docs/` folder with the following structure: - -``` -docs/ -├── category-name-1/ -│ ├── index.php (Required: Documentation content) -│ ├── metadata.json (Required: Category and ordering info) -│ └── icon.png or icon.jpg (Required: Category icon) -├── category-name-2/ -│ ├── index.php -│ ├── metadata.json -│ └── icon.png -└── ... -``` - -## Creating New Documentation - -### 1. Create a Folder - -Create a new folder in `/modules/billing/docs/` with a descriptive name (lowercase, hyphens for spaces): - -```bash -mkdir /modules/billing/docs/my-new-doc -``` - -### 2. Create metadata.json - -This file defines how the documentation appears in the list: - -```json -{ - "name": "My Documentation Title", - "description": "A brief description of this documentation", - "category": "game", - "order": 10 -} -``` - -**Fields:** -- `name`: Display name shown in the documentation list -- `description`: Brief description shown on the card -- `category`: One of: `game`, `panel`, `mods`, `troubleshooting`, `other` -- `order`: Sort order within the category (lower numbers appear first) - -### 3. Create index.php - -This file contains the actual documentation content. Use PHP and HTML: - -```php - -

My Documentation Title

- -

Section 1

-

Your content here...

- -

Subsection

-
    -
  • Item 1
  • -
  • Item 2
  • -
- -

Code Examples

-

-# Your code here
-command --option value
-
-``` - -The documentation system automatically styles: -- Headings (h1-h4) -- Links (styled with accent color) -- Code blocks (with dark background) -- Lists and other HTML elements - -### 4. Add an Icon - -Add either `icon.png` or `icon.jpg` to the folder. Recommended size: 60x60 pixels or larger (will be scaled down). - -If no icon is provided, a default document emoji (📄) will be shown. - -## Categories - -Documentation is organized into these categories: - -- **game** - Game-specific server guides -- **panel** - Panel usage and features -- **mods** - Mods and addon documentation -- **troubleshooting** - Problem-solving guides -- **other** - Miscellaneous documentation - -Categories are sorted and labeled automatically on the documentation page. - -## Example Documentation - -See the included examples: - -1. **minecraft** - Game server documentation example -2. **getting-started** - Panel documentation example -3. **common-issues** - Troubleshooting documentation example - -## Accessing Documentation - -Users can access documentation at: -- `/modules/billing/docs.php` - Main documentation list -- `/modules/billing/docs.php?action=view&doc=folder-name` - Specific doc - -A "Documentation" link is added to the main navigation menu. - -## Best Practices - -1. **Keep it Organized**: Use clear, descriptive folder names -2. **Consistent Naming**: Use lowercase and hyphens (e.g., `my-game-guide`) -3. **Good Descriptions**: Write helpful metadata descriptions -4. **Visual Icons**: Use recognizable icons for each category -5. **Test Content**: Preview documentation after creating it -6. **Regular Updates**: Keep documentation current with panel changes - -## Migration from Old System - -The old docs folder with game markdown files has been moved to `/modules/billing/docs_old/` for reference. The new system provides: - -- Better organization by category -- Consistent styling -- Easier navigation -- Extensible structure for any type of documentation - -To migrate old documentation: -1. Create a new folder for each document -2. Convert markdown to HTML in index.php -3. Add appropriate metadata.json -4. Add an icon image - -## Troubleshooting - -### Documentation not appearing -- Check that folder has all three required files (index.php, metadata.json, icon) -- Verify metadata.json is valid JSON -- Ensure file permissions allow reading - -### Styling issues -- The system uses inline styles from docs.php -- Custom styles in index.php may conflict -- Keep content semantic (use proper HTML tags) - -### Icons not showing -- Check file exists and is named exactly `icon.png` or `icon.jpg` -- Verify image file is not corrupted -- Try a smaller image size if very large diff --git a/Panel/modules/billing/docs/XML-Notes.md b/Panel/modules/billing/docs/XML-Notes.md deleted file mode 100644 index b823c898..00000000 --- a/Panel/modules/billing/docs/XML-Notes.md +++ /dev/null @@ -1,622 +0,0 @@ -## OGP XML Notes / still W.I.P. - -_The order of each XML element matters, and this guide presents them in their order of appearance!_ -___ -### Linux and Windows: - - -#### Game Config: -This is the first element. There can only be one `` element. -``` - -the whole XML file content here - -``` -All the following elements should be contained within `` element. - - - -___ -#### Game Key: -Comes after `` element (actually within `` element as said above). There can only be one `` element. Example: - -``` -space_engineers_win64 -``` -This is a unique key used to identify this specific game server in OGP. You should not use spaces, nor any special character in it, only alpha-numeric value and underscores. It should contain a suffix related to the compatible OS. Available suffixes are `_win32`, `_win64`, `_linux32`, `_linux64`, using one of these suffixes in the game_key will let OGP know which OS it is available on, making it visible or not when you install a new game server, depending on your OS architecture. `_win` and `_linux` work too, but we highly recommend to now use the previously listed suffixes. - - - -___ -#### Query Protocol: -Comes after `` element. There can only be one `` element. Example: - -``` -lgsl -``` -It defines the query protocol used by OGP. Available protocols are `lgsl`, `gameq`, `rcon` (`rcon2`? `lcon`?) - - - -___ -#### LGSL Query Name: -Comes after `` element. There can only be one `` element. Example: - -``` -killingfloor2 -``` -This is the unique key referencing this specific game server in the LGSL protocol file, used to query the game server. - - - -___ -#### GameQ Query Name: -Comes after `` element. There can only be one `` element. Example: - -``` -redorchestra2 -``` -This is the unique key referencing this specific game server in the GameQ protocol files, used to query the game server. - - - -___ -#### Installer: -Comes after `` element (or comes after `` or `` element when used). There can only be one `` element. Example: - -``` -steamcmd -``` -Defines the use of SteamCMD tool to install the game server. - - - -___ -#### Game Name: -Comes after `` element. There can only be one `` element. Example: - -``` -Killing Floor 2 -``` -This is the real game server name appearing in the list when installing a new game server. - - - -___ -#### Server Executable Name: -Comes after `` element. There can only be one `` element. Example: - -``` -SpaceEngineersDedicated.exe -``` -This is the server executable name used in the start command line. - - - -___ -#### Query Port: -Comes after `` element. There can only be one `` element. Example: - -``` -13 -``` -Difference between the server port (`%PORT%`) and the query port (`%QUERY_PORT%`). In this example the variable %QUERY_PORT% will be 13 added to the port value. - - - -___ -#### CLI Template: -Comes after `` element. There can only be one `` element. Example: - -``` --console %BASE_PATH% -ignorelastsession -``` -``` -%MAP%%GAMEMODE%%DIFFICULTY%%GAMELENGTH%%PLAYERS%%MUTATOR% %PORT% %IP% %WEB_ADMIN_PORT% %QUERY_PORT% -``` -This is the template that will generate the start command line placed after the server executable name. -You can use these variables which are known to OGP: -``` -GAME_TYPE -HOSTNAME -IP -MAP -PID_FILE -PLAYERS -PORT -QUERY_PORT -BASE_PATH -HOME_PATH -SAVE_PATH -OUTPUT_PATH -CONTROL_PASSWORD -``` -These variable should be between `%` characters, the Panel will then replace them with the proper value when generating the start command line. - -You can also use custom variables that you will define later in the XML. - - - -___ -#### CLI Parameters: -Comes after `` element. There can only be one `` element. Example: - -``` - - - - - - - - -``` -It defines the known variables used in ``. In this example we can imagine that for **%MAP%** it will generate the map name without space or quotes around it, because there is no `options`, for **%IP%** it will generate `-MultiHome="123.123.123.123"` using the `cli_string` and adding only quotes around the game server IP value because of `options="q"`, and **%PORT%** will generate `-Port= "27015"` using the `cli_string` and adding space and quotes around the game server port because of `options="sq"`. - - - -___ -#### Reserve Ports: -Comes after `` or `` element. There can only be one `` element. Example: - -``` - - 5 - 19238 - 666 - -``` -You can add reserved ports here to use in the generated start command line. These ports will also be managed by OGP if OGP is used to manage the Agent machine firewall. Type can be `add` or `subtract` which is self explanatory. In this example when using the %WEB_ADMIN_PORT% variable in the `` it will generate `-WebAdminPort=XXX`, XXX being 5 subtracted to the Port set for this game server, when using the %STEAM_PORT% variable in the `` it will generate `-SteamPort=XXX` where XXX will be 19238 added to the Port set for this game server. As you can see, the variable %MY_CUSTOM_PORT% have no `cli_string`, this can be used this way to simply open this port (which here would be 666 added to the game server port) in the Agent machine firewall, when OGP is set to control the machine firewall (note: the firewall management may actually not open anything else than the game server port, to be verified). - - - -___ -#### CLI Allowed Characters: -Comes after `` element. There can only be one `` element. Example: - -``` -; -``` -Used to allow some special characters in the command line. Escaped by default: ```\ " ' | & ; > < ` $ ( ) [ ]``` - - - -___ -#### Maps Location: -Comes after `` element. There can only be one `` element. Example: - -``` -folder/maps -``` -It sets the path of the map folder for this game server, which will be used to generate a selectable map list available before starting the game server. If folder contains map files it will use their name without the extension, if it contains sub folders with each map inside each own sub folder, it will generate the map list based on the folders names contained in the defined path. The selected map in the list will be used to replace the %MAP% variable in the ``. - - - -___ -#### Map List: -Comes after `` element. There can only be one `` element. Example: - -``` -maplist.txt -``` -The map list file path used to generate the selectable map list available before starting the game server. In this example it will look for a file called maplist.txt inside the root of the game server. Map list should have one map per line. The selected map in the list will be used to replace the %MAP% variable in the ``. - - - -___ -#### Console Log: -Comes after `` element. There can only be one `` element. Example: - -``` -KFGame/Logs/Launch.log -``` -It defines the path of the log file that will be shown in the LOG page of the game server. Most game servers, especially on Linux, will not need that, when in general, Windows game server will need it to properly show the output log. - - - -___ -#### Executable Location: -Comes after `` element. There can only be one `` element. Example: - -``` -Binaries/Win64 -``` -It defines the path of the game server executable when not in the root folder. - - - -___ -#### Max User Amount: -Comes after `` element. There can only be one `` element. Example: - -``` -64 -``` -It defines the maximum player number you will be able to set when creating the game server. - - - -___ -#### Control Protocol: -Comes after `` element. There can only be one `` element. Example: - -``` -rcon2 -``` -Can be `rcon`, `rcon2`, or `lcon` (legacy). Note that `rcon` can also have type option to define, which can be `old` or `new`. Example: -``` -rcon -old -``` - - - -___ -#### Mods: -Comes after `` element. There can only be one `` element. Example: - -``` - - - none - 237410 - - -``` - -Used to define different mods for the game server, in this example there is only one mod available which will be the default installed one (actually the game server itself here). The `` here is the Steam appID that will be used to install and update the game server. (note: case RSync to explain? Case multi mods to explain?) - - - -___ -#### Replace Texts -`` Comes after ``. - -Contains multiple `` entries like so: - -``` - - - ServerAdminPassword=.* - ServerAdminPassword= - ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini - sq - - - SessionName=.* - SessionName= - ShooterGame/Saved/Config/LinuxServer/GameUserSettings.ini - sq - - -``` - -`` within `` is what the line to replace starts with. - -`` within `` is the key for what should be kept when the line is replaced with the value entered by the user when replacing text occurs. - -`` within `` specifies the text file to make the replacement in. - -`` within `` specifies how to enter the user's value after the `` key. Possible options are: - -``` - nothing / no value = placed as is - s = space / separated - q = quoted - sq = space and quotes - sc = space and ends with a comma - sqc = space, quoted, and ends with a comma -``` -These replace text will be applied on server start and modify the specified config files with the values generated by the Panel. - - - -___ -#### Server Params: -`` Comes after ``. - -Contains multiple `` entries like so: - -``` - - - - Server Password - Players must know this password to connect. - - - - - - ns - Difficulty - This sets the server difficulty. Leave empty to configure this parameter in the config files or webadmin - - - Cheats - Enable the cheats to be used from the ingame Admin menu - - -``` - -`id` attribute on the `` specifies which variable to replace in the `` -`key` attribute specifies what will replace the variable defined by id attribute -type attribute will define what kind of parameter it is, possible values are `text` `select` `checkbox_key_value`: -- `text` will allow to write text value to be added during the replacement of the variables in startup command line. For example, `%SP%` in `` would be replaced with `?ServerPassword=XXX` where XXX would be the value entered by the user in the text box, if nothing is entered the variable is not replaced but removed from ``. The value entered can be modified to fit your needs by using the `