From 2cb3780893f171dc0599200e66cf97128c4e9eac Mon Sep 17 00:00:00 2001 From: Lacikika <114676193+Lacikika@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:46:27 +0000 Subject: [PATCH 1/2] feat: implement Migration 066 for municipal tolls and weigh stations - Create migration 066 SQL schema in nyx_core, nyx_vehicles, nyx_logistics, and nyx_treasury - Register migration 066 in index.json - Add shared/tolls_and_weighing.lua for pure toll, GVWR, and overweight fine math - Add server/tolls_and_weighing.lua for RFID/plate toll deduction, treasury routing, and MDT dispatch alerts - Add client/tolls_and_weighing.lua for spatial gantry/scale pad triggers and NUI notifications - Export server APIs in exports.lua and register Test 61 in nyx_core/server/tests.lua - Add unit test suite tests/test_municipal_toll_booths_and_commercial_weigh_stations.py Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- FiveM/nyx_core/server/tests.lua | 47 +++- ...l_booths_and_commercial_weigh_stations.sql | 54 ++++ FiveM/nyx_core/sql/migrations/index.json | 3 +- ...l_booths_and_commercial_weigh_stations.sql | 54 ++++ ...l_booths_and_commercial_weigh_stations.sql | 54 ++++ .../client/tolls_and_weighing.lua | 122 +++++++++ FiveM/nyx_vehicles/config.lua | 9 +- FiveM/nyx_vehicles/fxmanifest.lua | 5 +- FiveM/nyx_vehicles/server/exports.lua | 19 ++ .../server/tolls_and_weighing.lua | 247 ++++++++++++++++++ .../shared/tolls_and_weighing.lua | 143 ++++++++++ ...l_booths_and_commercial_weigh_stations.sql | 54 ++++ ...ll_booths_and_commercial_weigh_stations.py | 244 +++++++++++++++++ 13 files changed, 1051 insertions(+), 4 deletions(-) create mode 100644 FiveM/nyx_core/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql create mode 100644 FiveM/nyx_logistics/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql create mode 100644 FiveM/nyx_treasury/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql create mode 100644 FiveM/nyx_vehicles/client/tolls_and_weighing.lua create mode 100644 FiveM/nyx_vehicles/server/tolls_and_weighing.lua create mode 100644 FiveM/nyx_vehicles/shared/tolls_and_weighing.lua create mode 100644 FiveM/nyx_vehicles/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql create mode 100644 tests/test_municipal_toll_booths_and_commercial_weigh_stations.py diff --git a/FiveM/nyx_core/server/tests.lua b/FiveM/nyx_core/server/tests.lua index 028ba5b..b05bd78 100644 --- a/FiveM/nyx_core/server/tests.lua +++ b/FiveM/nyx_core/server/tests.lua @@ -12400,6 +12400,50 @@ function Nyx.Tests.TestIndustrialRefiningAndFunctionalPartsManufacturing() end -- 60. Test Pre-Trial Judicial Bail Bonds, Surety Escrow & Fugitive Recovery Warrants +function Nyx.Tests.TestMunicipalTollsAndWeighStations() + print("^3[Nyx:Tests] Running Municipal Toll Booths & Commercial Weigh Stations Suite...^7") + local passes = 0 + local total = 5 + + local T = Nyx.TollsAndWeighing + if not T then + print("^1[Nyx:Test:FAIL]^7 Nyx.TollsAndWeighing shared module not loaded in environment") + return false + end + + -- Sub-test A: Toll Fee Calculation + local sedanToll = T.CalculateTollFee(1, 5.00, 1.50, 1.00) + local commToll = T.CalculateTollFee(20, 5.00, 1.50, 1.00) + local testA_ok = (sedanToll == 5.00) and (commToll == 13.13) + if assertTest(testA_ok == true, "Toll Fee Pure Math Calculation", "Expected $5.00 sedan, $13.13 commercial toll", string.format("Sedan: $%.2f, Comm: $%.2f", sedanToll, commToll)) then passes = passes + 1 end + + -- Sub-test B: Gross Vehicle Weight Rating (GVWR) + local gvwrSedan = T.CalculateGVWR(1, 150.0, 0.0) + local gvwrCommercial = T.CalculateGVWR(20, 500.0, 15000.0) + local testB_ok = (gvwrSedan == 1750.0) and (gvwrCommercial == 27500.0) + if assertTest(testB_ok == true, "GVWR Gross Weight Calculation", "Expected 1750kg sedan, 27500kg loaded semi", string.format("Sedan: %.1fkg, Semi: %.1fkg", gvwrSedan, gvwrCommercial)) then passes = passes + 1 end + + -- Sub-test C: Overweight Fine Calculation + local fineZero = T.CalculateOverweightFine(14000.0, 15000.0) + local fineExcess = T.CalculateOverweightFine(17000.0, 15000.0, 2.50) + local testC_ok = (fineZero == 0.00) and (fineExcess == 5250.00) + if assertTest(testC_ok == true, "Statutory Overweight Fine Calculation", "Expected $0 fine below limit, $5250.00 fine for 2000kg excess", string.format("Zero: $%.2f, Excess: $%.2f", fineZero, fineExcess)) then passes = passes + 1 end + + -- Sub-test D: Inspection Outcomes + local statusPass = T.DetermineInspectionStatus(14000.0, 15000.0, true, false, false) + local statusOver = T.DetermineInspectionStatus(17000.0, 15000.0, true, false, false) + local statusBypass = T.DetermineInspectionStatus(14000.0, 15000.0, true, false, true) + local testD_ok = (statusPass == "PASS") and (statusOver == "OVERWEIGHT") and (statusBypass == "BYPASSED") + if assertTest(testD_ok == true, "Weigh Station Inspection Outcome Resolution", "Expected PASS, OVERWEIGHT, BYPASSED", string.format("P: %s, O: %s, B: %s", statusPass, statusOver, statusBypass)) then passes = passes + 1 end + + -- Sub-test E: Rounding Cents + local rounded = T.RoundToCent(12.3456) + local testE_ok = (rounded == 12.35) + if assertTest(testE_ok == true, "Round to Cent Exact Conservation", "Expected 12.35", string.format("%.2f", rounded)) then passes = passes + 1 end + + return passes == total +end + function Nyx.Tests.TestJudicialBailBondsSuretyEscrowAndFugitiveRecovery() print("^3[Nyx:Tests] Running Judicial Bail Bonds & Fugitive Recovery Suite...^7") local passes = 0 @@ -12595,6 +12639,7 @@ function Nyx.Tests.RunAll() local t58 = Nyx.Tests.TestPharmaceuticalRefiningAndPlayerChemicalLabs() local t59 = Nyx.Tests.TestIndustrialRefiningAndFunctionalPartsManufacturing() local t60 = Nyx.Tests.TestJudicialBailBondsSuretyEscrowAndFugitiveRecovery() + local t61 = Nyx.Tests.TestMunicipalTollsAndWeighStations() print("^5--------------------------------------------------^7") local suites1to50Passed = false @@ -12602,7 +12647,7 @@ function Nyx.Tests.RunAll() suites1to50Passed = true end - if suites1to50Passed and t51 and t52 and t53 and t54 and t55 and t56 and t57 and t58 and t59 and t60 then + if suites1to50Passed and t51 and t52 and t53 and t54 and t55 and t56 and t57 and t58 and t59 and t60 and t61 then print("^2>>> ALL NYXFRAMEWORK INTEGRATION TESTS PASSED! <<<^7") else print("^1>>> SOME NYXFRAMEWORK TESTS FAILED! CHECK LOGS. <<<^7") diff --git a/FiveM/nyx_core/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql b/FiveM/nyx_core/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql new file mode 100644 index 0000000..61ea02f --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql @@ -0,0 +1,54 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_mdt +-- Migration: 066_municipal_toll_booths_and_commercial_weigh_stations.sql +-- Description: Municipal transit infrastructure, highway toll plazas, +-- automated RFID toll deduction, commercial axle multipliers, +-- and commercial weigh station inspection/bypass logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal toll plazas table +CREATE TABLE IF NOT EXISTS `nyx_toll_plazas` ( + `plaza_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `zone_name` VARCHAR(64) NOT NULL, + `base_fee` DECIMAL(10, 2) NOT NULL DEFAULT 5.00, + `commercial_axle_multiplier` DECIMAL(5, 2) NOT NULL DEFAULT 1.50, + `active_status` TINYINT(1) NOT NULL DEFAULT 1, + `total_collected` DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_toll_zone` (`zone_name`), + INDEX `idx_toll_status` (`active_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create automated commercial weigh station inspection logs table +CREATE TABLE IF NOT EXISTS `nyx_weigh_station_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL, + `manifest_id` VARCHAR(64) NULL, + `gross_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `max_allowed_kg` DECIMAL(10, 2) NOT NULL DEFAULT 15000.00, + `overweight_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `inspection_status` ENUM('PASS', 'OVERWEIGHT', 'CONTRABAND_FLAG', 'BYPASSED') NOT NULL DEFAULT 'PASS', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_weigh_plate` (`vehicle_plate`), + INDEX `idx_weigh_driver` (`driver_identifier`), + INDEX `idx_weigh_manifest` (`manifest_id`), + INDEX `idx_weigh_status` (`inspection_status`), + INDEX `idx_weigh_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal highway toll plazas +INSERT INTO `nyx_toll_plazas` + (`plaza_id`, `zone_name`, `base_fee`, `commercial_axle_multiplier`, `active_status`, `total_collected`) +VALUES + ('plaza_del_perro_fwy', 'Del Perro Freeway Gantry', 5.00, 1.50, 1, 0.00), + ('plaza_la_puerta_fwy', 'La Puerta Freeway Toll Plaza', 7.50, 1.75, 1, 0.00), + ('plaza_palomino_fwy', 'Palomino Freeway East Barrier', 10.00, 2.00, 1, 0.00), + ('plaza_senora_fwy', 'Senora Freeway North Express Gantry', 6.00, 1.60, 1, 0.00) +ON DUPLICATE KEY UPDATE `zone_name` = VALUES(`zone_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_core/sql/migrations/index.json b/FiveM/nyx_core/sql/migrations/index.json index 8aa2f05..e5081f1 100644 --- a/FiveM/nyx_core/sql/migrations/index.json +++ b/FiveM/nyx_core/sql/migrations/index.json @@ -63,5 +63,6 @@ "062_point_of_sale_physical_stocking_and_empty_shelf_displays.sql", "063_pharmaceutical_refining_and_player_chemical_labs.sql", "064_industrial_refining_and_functional_parts_manufacturing.sql", - "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql" + "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql", + "066_municipal_toll_booths_and_commercial_weigh_stations.sql" ] diff --git a/FiveM/nyx_logistics/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql b/FiveM/nyx_logistics/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql new file mode 100644 index 0000000..61ea02f --- /dev/null +++ b/FiveM/nyx_logistics/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql @@ -0,0 +1,54 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_mdt +-- Migration: 066_municipal_toll_booths_and_commercial_weigh_stations.sql +-- Description: Municipal transit infrastructure, highway toll plazas, +-- automated RFID toll deduction, commercial axle multipliers, +-- and commercial weigh station inspection/bypass logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal toll plazas table +CREATE TABLE IF NOT EXISTS `nyx_toll_plazas` ( + `plaza_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `zone_name` VARCHAR(64) NOT NULL, + `base_fee` DECIMAL(10, 2) NOT NULL DEFAULT 5.00, + `commercial_axle_multiplier` DECIMAL(5, 2) NOT NULL DEFAULT 1.50, + `active_status` TINYINT(1) NOT NULL DEFAULT 1, + `total_collected` DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_toll_zone` (`zone_name`), + INDEX `idx_toll_status` (`active_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create automated commercial weigh station inspection logs table +CREATE TABLE IF NOT EXISTS `nyx_weigh_station_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL, + `manifest_id` VARCHAR(64) NULL, + `gross_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `max_allowed_kg` DECIMAL(10, 2) NOT NULL DEFAULT 15000.00, + `overweight_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `inspection_status` ENUM('PASS', 'OVERWEIGHT', 'CONTRABAND_FLAG', 'BYPASSED') NOT NULL DEFAULT 'PASS', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_weigh_plate` (`vehicle_plate`), + INDEX `idx_weigh_driver` (`driver_identifier`), + INDEX `idx_weigh_manifest` (`manifest_id`), + INDEX `idx_weigh_status` (`inspection_status`), + INDEX `idx_weigh_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal highway toll plazas +INSERT INTO `nyx_toll_plazas` + (`plaza_id`, `zone_name`, `base_fee`, `commercial_axle_multiplier`, `active_status`, `total_collected`) +VALUES + ('plaza_del_perro_fwy', 'Del Perro Freeway Gantry', 5.00, 1.50, 1, 0.00), + ('plaza_la_puerta_fwy', 'La Puerta Freeway Toll Plaza', 7.50, 1.75, 1, 0.00), + ('plaza_palomino_fwy', 'Palomino Freeway East Barrier', 10.00, 2.00, 1, 0.00), + ('plaza_senora_fwy', 'Senora Freeway North Express Gantry', 6.00, 1.60, 1, 0.00) +ON DUPLICATE KEY UPDATE `zone_name` = VALUES(`zone_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_treasury/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql b/FiveM/nyx_treasury/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql new file mode 100644 index 0000000..61ea02f --- /dev/null +++ b/FiveM/nyx_treasury/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql @@ -0,0 +1,54 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_mdt +-- Migration: 066_municipal_toll_booths_and_commercial_weigh_stations.sql +-- Description: Municipal transit infrastructure, highway toll plazas, +-- automated RFID toll deduction, commercial axle multipliers, +-- and commercial weigh station inspection/bypass logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal toll plazas table +CREATE TABLE IF NOT EXISTS `nyx_toll_plazas` ( + `plaza_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `zone_name` VARCHAR(64) NOT NULL, + `base_fee` DECIMAL(10, 2) NOT NULL DEFAULT 5.00, + `commercial_axle_multiplier` DECIMAL(5, 2) NOT NULL DEFAULT 1.50, + `active_status` TINYINT(1) NOT NULL DEFAULT 1, + `total_collected` DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_toll_zone` (`zone_name`), + INDEX `idx_toll_status` (`active_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create automated commercial weigh station inspection logs table +CREATE TABLE IF NOT EXISTS `nyx_weigh_station_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL, + `manifest_id` VARCHAR(64) NULL, + `gross_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `max_allowed_kg` DECIMAL(10, 2) NOT NULL DEFAULT 15000.00, + `overweight_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `inspection_status` ENUM('PASS', 'OVERWEIGHT', 'CONTRABAND_FLAG', 'BYPASSED') NOT NULL DEFAULT 'PASS', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_weigh_plate` (`vehicle_plate`), + INDEX `idx_weigh_driver` (`driver_identifier`), + INDEX `idx_weigh_manifest` (`manifest_id`), + INDEX `idx_weigh_status` (`inspection_status`), + INDEX `idx_weigh_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal highway toll plazas +INSERT INTO `nyx_toll_plazas` + (`plaza_id`, `zone_name`, `base_fee`, `commercial_axle_multiplier`, `active_status`, `total_collected`) +VALUES + ('plaza_del_perro_fwy', 'Del Perro Freeway Gantry', 5.00, 1.50, 1, 0.00), + ('plaza_la_puerta_fwy', 'La Puerta Freeway Toll Plaza', 7.50, 1.75, 1, 0.00), + ('plaza_palomino_fwy', 'Palomino Freeway East Barrier', 10.00, 2.00, 1, 0.00), + ('plaza_senora_fwy', 'Senora Freeway North Express Gantry', 6.00, 1.60, 1, 0.00) +ON DUPLICATE KEY UPDATE `zone_name` = VALUES(`zone_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_vehicles/client/tolls_and_weighing.lua b/FiveM/nyx_vehicles/client/tolls_and_weighing.lua new file mode 100644 index 0000000..b967971 --- /dev/null +++ b/FiveM/nyx_vehicles/client/tolls_and_weighing.lua @@ -0,0 +1,122 @@ +--[[ + Nyxframework: nyx_vehicles + File: client/tolls_and_weighing.lua + Description: Spatial trigger zone monitoring for highway toll gantries and weigh station scale pads, + 3-second weight stabilization checks, and NUI/HUD bridge to nyx_ui. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.TollsAndWeighing = Nyx.TollsAndWeighing or {} + +local isCheckingScalePad = false +local scalePadHaltTimer = 0 +local activeGantryCooldowns = {} -- [plazaId] = timestamp + +-- Spatial Toll Gantry Zone Definitions +local TollGantries = { + { id = "plaza_del_perro_fwy", label = "Del Perro Freeway Toll Gantry", coords = vector3(-1200.0, -1500.0, 4.0), radius = 25.0 }, + { id = "plaza_la_puerta_fwy", label = "La Puerta Freeway Toll Plaza", coords = vector3(150.0, -2200.0, 6.0), radius = 25.0 }, + { id = "plaza_palomino_fwy", label = "Palomino Freeway East Barrier", coords = vector3(2500.0, -400.0, 90.0), radius = 30.0 }, + { id = "plaza_senora_fwy", label = "Senora Freeway North Express Gantry", coords = vector3(2600.0, 4300.0, 45.0), radius = 30.0 } +} + +-- Spatial Scale Pad Zone Definitions +local ScalePads = { + { id = "scale_senora_north", label = "Senora Commercial Weigh Station", coords = vector3(2520.0, 4210.0, 45.0), radius = 12.0 }, + { id = "scale_port_south", label = "Los Santos Port Scale Pad", coords = vector3(800.0, -3000.0, 5.0), radius = 12.0 } +} + +--- Trigger HUD / NUI Notification bridge to nyx_ui +local function SendHudNotification(title, message, notifyType) + SendNUIMessage({ + action = "nyx_ui:showNotification", + data = { + title = title or "Tranzit Rendszer", + message = message or "", + type = notifyType or "info", + duration = 5000 + } + }) +end + +-- Monitor Toll Gantries and Scale Pads Spatial Triggers +CreateThread(function() + while true do + local sleep = 1000 + local ped = PlayerPedId() + + if IsPedInAnyVehicle(ped, false) and GetPedInVehicleSeat(GetVehiclePedIsIn(ped, false), -1) == ped then + local veh = GetVehiclePedIsIn(ped, false) + local coords = GetEntityCoords(veh) + local speedMps = GetEntitySpeed(veh) + local speedKmh = speedMps * 3.6 + local now = GetGameTimer() + + -- 1. Check Toll Gantries + for _, gantry in ipairs(TollGantries) do + local dist = #(coords - gantry.coords) + if dist <= gantry.radius then + sleep = 200 + local lastPass = activeGantryCooldowns[gantry.id] or 0 + if (now - lastPass) > 15000 then -- 15 second cooldown per gantry pass + activeGantryCooldowns[gantry.id] = now + local plate = GetVehicleNumberPlateText(veh) + local vClass = GetVehicleClass(veh) + + TriggerServerEvent("nyx_vehicles:server:processToll", gantry.id, plate, vClass) + SendHudNotification("Útdíj Levonás", string.format("%s automatikus útdíj áthaladás regisztrálva (%s).", gantry.label, plate), "info") + end + end + end + + -- 2. Check Weigh Station Scale Pads + for _, pad in ipairs(ScalePads) do + local dist = #(coords - pad.coords) + if dist <= pad.radius then + sleep = 100 + local vClass = GetVehicleClass(veh) + + -- Requires vehicle to halt (speed < 1.0 km/h) for 3 seconds on scale pad + if speedKmh < 1.0 then + if not isCheckingScalePad then + isCheckingScalePad = true + scalePadHaltTimer = GetGameTimer() + SendHudNotification("Mérlegelés Folyamatban", "Maradjon mozdulatlan a mérlegpadon 3 másodpercig...", "warning") + else + local elapsed = GetGameTimer() - scalePadHaltTimer + if elapsed >= 3000 then + isCheckingScalePad = false + scalePadHaltTimer = GetGameTimer() + 20000 -- Cooldown + + local plate = GetVehicleNumberPlateText(veh) + TriggerServerEvent("nyx_vehicles:server:inspectWeighStation", plate, vClass, false) + SendHudNotification("Mérlegelés Kész", "Tengelysúly és fuvarlevél ellenőrzés sikeresen lefutott.", "success") + end + end + else + if isCheckingScalePad then + isCheckingScalePad = false + SendHudNotification("Mérlegelés Megszakítva", "A jármű elmozdult a mérlegpadról!", "error") + end + + -- If commercial vehicle drives through scale pad at high speed (> 25 km/h) without halting, trigger BYPASS alert + if (vClass == 10 or vClass == 20 or vClass == 11) and speedKmh > 25.0 then + local lastBypass = activeGantryCooldowns[pad.id] or 0 + if (now - lastBypass) > 30000 then + activeGantryCooldowns[pad.id] = now + local plate = GetVehicleNumberPlateText(veh) + TriggerServerEvent("nyx_vehicles:server:inspectWeighStation", plate, vClass, true) + SendHudNotification("MÉRŐÁLLOMÁS MEGKERÜLÉSE", "Riasztás kiküldve a hatóságoknak!", "error") + end + end + end + end + end + else + isCheckingScalePad = false + end + + Wait(sleep) + end +end) diff --git a/FiveM/nyx_vehicles/config.lua b/FiveM/nyx_vehicles/config.lua index 9a303dd..17c1876 100644 --- a/FiveM/nyx_vehicles/config.lua +++ b/FiveM/nyx_vehicles/config.lua @@ -55,6 +55,14 @@ Config.Access = { } } +-- Municipal Toll Gantries & Commercial Weigh Stations Configuration +Config.TollsAndWeighing = { + ScalePadStabilizationSeconds = 3, -- Required seconds stationary on scale pad + BypassSpeedKmhThreshold = 25.0, -- Speed threshold to trigger bypass alert + BaseFinePerExcessKg = 2.50, -- Statutory fine per excess kg + BypassFineSurcharge = 500.00 -- Fine penalty surcharge for scale bypass +} + -- State Persistence & Debouncing Config.Persistence = { DamageFlushInterval = 30, -- Debounced DB write batch interval in seconds @@ -123,4 +131,3 @@ Config.Wear = { }, ServiceRequiredItem = "tool_repairkit" -- Required item if performing manual field repairs } - diff --git a/FiveM/nyx_vehicles/fxmanifest.lua b/FiveM/nyx_vehicles/fxmanifest.lua index 3a6a318..51aa8f5 100644 --- a/FiveM/nyx_vehicles/fxmanifest.lua +++ b/FiveM/nyx_vehicles/fxmanifest.lua @@ -21,7 +21,8 @@ shared_scripts { 'shared/registration.lua', 'shared/diagnostics.lua', 'shared/fuel.lua', - 'shared/keys.lua' + 'shared/keys.lua', + 'shared/tolls_and_weighing.lua' } server_scripts { @@ -33,6 +34,7 @@ server_scripts { 'server/registration.lua', 'server/diagnostics.lua', 'server/fuel.lua', + 'server/tolls_and_weighing.lua', 'server/spawner.lua', 'server/main.lua', 'server/exports.lua' @@ -47,6 +49,7 @@ client_scripts { 'client/diagnostics.lua', 'client/fuel.lua', 'client/speedometer.lua', + 'client/tolls_and_weighing.lua', 'client/main.lua', 'client/exports.lua' } diff --git a/FiveM/nyx_vehicles/server/exports.lua b/FiveM/nyx_vehicles/server/exports.lua index 07b7472..805dc7a 100644 --- a/FiveM/nyx_vehicles/server/exports.lua +++ b/FiveM/nyx_vehicles/server/exports.lua @@ -140,4 +140,23 @@ exports("GetVehicleLockpickHistory", function(vin) return Nyx.Vehicles.GetLockpickHistory(vin) end) +-- Toll Plazas, Transit Infrastructure & Commercial Weigh Stations +exports("ProcessTollPass", function(src, plazaId, plate, vehicleClass) + return Nyx.Vehicles.ProcessTollPass(src, plazaId, plate, vehicleClass) +end) + +exports("InspectWeighStation", function(src, plate, vehicleClass, didBypass) + return Nyx.Vehicles.InspectWeighStation(src, plate, vehicleClass, didBypass) +end) + +exports("CalculateTollFee", function(vehicleClass, baseFee, commercialAxleMultiplier, statutoryTaxRate) + return Nyx.TollsAndWeighing.CalculateTollFee(vehicleClass, baseFee, commercialAxleMultiplier, statutoryTaxRate) +end) + +exports("CalculateGVWR", function(vehicleClass, trunkCargoKg, loadedPalletsKg, customCurbWeight) + return Nyx.TollsAndWeighing.CalculateGVWR(vehicleClass, trunkCargoKg, loadedPalletsKg, customCurbWeight) +end) +exports("CalculateOverweightFine", function(grossWeightKg, maxAllowedKg, baseFinePerExcessKg) + return Nyx.TollsAndWeighing.CalculateOverweightFine(grossWeightKg, maxAllowedKg, baseFinePerExcessKg) +end) diff --git a/FiveM/nyx_vehicles/server/tolls_and_weighing.lua b/FiveM/nyx_vehicles/server/tolls_and_weighing.lua new file mode 100644 index 0000000..1d4d277 --- /dev/null +++ b/FiveM/nyx_vehicles/server/tolls_and_weighing.lua @@ -0,0 +1,247 @@ +--[[ + Nyxframework: nyx_vehicles + File: server/tolls_and_weighing.lua + Description: Authoritative server handlers for automated RFID/plate toll deduction, + treasury revenue routing, commercial weigh station inspections, + cargo manifest verification, and automated MDT dispatch alerts. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.TollsAndWeighing = Nyx.TollsAndWeighing or {} + +-- Standardized RPC Bridge to nyx_core +Nyx.RPC = Nyx.RPC or { + Register = function(route, handler) + if exports['nyx_core'] and exports['nyx_core'].RegisterRPC then + exports['nyx_core']:RegisterRPC(route, handler) + end + end, + GenerateNonce = function() + local template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + return (string.gsub(template, '[xy]', function(c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end)) + end +} + +--- Process automated RFID/plate toll gantry pass +--- @param src number Player source ID +--- @param plazaId string Toll plaza identifier +--- @param plate string Vehicle license plate +--- @param vehicleClass number Vehicle class ID +--- @return table result { success = bool, fee = number, plazaId = string, txUuid = string } +function Nyx.Vehicles.ProcessTollPass(src, plazaId, plate, vehicleClass) + src = tonumber(src) + plate = tostring(plate or "UNKNOWN") + vehicleClass = tonumber(vehicleClass) or 0 + plazaId = tostring(plazaId or "plaza_del_perro_fwy") + + local plaza = MySQL.single.await([[ + SELECT `plaza_id`, `zone_name`, `base_fee`, `commercial_axle_multiplier`, `active_status` + FROM `nyx_toll_plazas` WHERE `plaza_id` = ? + ]], { plazaId }) + + if not plaza or tonumber(plaza.active_status or 1) == 0 then + -- Default fallback plaza if DB query returns nil + plaza = { + plaza_id = plazaId, + zone_name = "Highway Toll Plaza", + base_fee = 5.00, + commercial_axle_multiplier = 1.50, + active_status = 1 + } + end + + local fee = Nyx.TollsAndWeighing.CalculateTollFee( + vehicleClass, + tonumber(plaza.base_fee) or 5.00, + tonumber(plaza.commercial_axle_multiplier) or 1.50, + 1.00 + ) + + -- Determine account/citizen source + local player = Nyx.GetPlayer and Nyx.GetPlayer(src) or nil + local citizenId = player and player.citizenId or ("CIT_" .. src) + local accountId = "citizen:" .. citizenId + + -- Route fee directly into public treasury master account earmarked for road degradation repair + local masterAcc = "gov:city:master" + local txUuid = nil + + if exports['nyx_treasury'] and exports['nyx_treasury'].TransferFunds then + local ok, tx = pcall(function() + return exports['nyx_treasury']:TransferFunds( + accountId, + masterAcc, + fee, + "highway_toll", + string.format("Automated RFID toll pass at %s (%s)", plaza.zone_name, plate) + ) + end) + if ok and tx then txUuid = tx end + end + + -- Update toll plaza total collected statistic + MySQL.update.await([[ + UPDATE `nyx_toll_plazas` + SET `total_collected` = `total_collected` + ? + WHERE `plaza_id` = ? + ]], { fee, plazaId }) + + -- Sync municipal infrastructure degradation if treasury module is available + if exports['nyx_treasury'] and exports['nyx_treasury'].EvaluateMunicipalFiscalState then + pcall(function() exports['nyx_treasury']:EvaluateMunicipalFiscalState() end) + end + + return { + success = true, + fee = fee, + plazaId = plazaId, + txUuid = txUuid + } +end + +--- Inspect commercial vehicle weight, cross-reference freight contracts and pallets, and log result +--- @param src number Driver source ID +--- @param plate string Vehicle plate +--- @param vehicleClass number Vehicle class +--- @param didBypass boolean Whether driver bypassed scale without halting +--- @return table inspectionResult { logId = number, status = string, grossWeightKg = number, maxAllowedKg = number, fine = number } +function Nyx.Vehicles.InspectWeighStation(src, plate, vehicleClass, didBypass) + src = tonumber(src) + plate = tostring(plate or "COMMERCIAL") + vehicleClass = tonumber(vehicleClass) or 20 + didBypass = not not didBypass + + local player = Nyx.GetPlayer and Nyx.GetPlayer(src) or nil + local driverId = player and player.citizenId or ("CIT_" .. (src or 0)) + + -- 1. Calculate Gross Vehicle Weight Rating (GVWR) + local trunkWeightKg = 0.0 + local palletWeightKg = 0.0 + local activeManifestId = nil + local isContrabandFlagged = false + + -- Query loaded pallets attached to vehicle or active freight contract in nyx_logistics + if exports['nyx_logistics'] and exports['nyx_logistics'].GetVehiclePallets then + local ok, pallets = pcall(function() return exports['nyx_logistics']:GetVehiclePallets(plate) end) + if ok and type(pallets) == "table" then + for _, p in ipairs(pallets) do + palletWeightKg = palletWeightKg + (tonumber(p.weightKg or p.weight) or 250.0) + if p.manifestId then activeManifestId = p.manifestId end + if p.isContraband or p.isSealedBreached then isContrabandFlagged = true end + end + end + end + + -- Query active contract if manifest missing + if not activeManifestId and exports['nyx_logistics'] and exports['nyx_logistics'].GetActiveContractForDriver then + local ok, contract = pcall(function() return exports['nyx_logistics']:GetActiveContractForDriver(driverId) end) + if ok and type(contract) == "table" and contract.id then + activeManifestId = "MANIFEST_" .. contract.id + end + end + + local grossWeightKg = Nyx.TollsAndWeighing.CalculateGVWR(vehicleClass, trunkWeightKg, palletWeightKg) + local maxAllowedKg = Nyx.TollsAndWeighing.GetMaxAllowedWeight(vehicleClass) + local hasValidManifest = (activeManifestId ~= nil) + + local status = Nyx.TollsAndWeighing.DetermineInspectionStatus( + grossWeightKg, + maxAllowedKg, + hasValidManifest, + isContrabandFlagged, + didBypass + ) + + local fine = 0.00 + if status == "OVERWEIGHT" or status == "BYPASSED" then + fine = Nyx.TollsAndWeighing.CalculateOverweightFine(grossWeightKg, maxAllowedKg) + if status == "BYPASSED" then + fine = fine + 500.00 -- Bypass surcharge fine + end + end + + -- Insert inspection log in nyx_weigh_station_logs + local logId = MySQL.insert.await([[ + INSERT INTO `nyx_weigh_station_logs` + (`vehicle_plate`, `driver_identifier`, `manifest_id`, `gross_weight_kg`, `max_allowed_kg`, `overweight_fine`, `inspection_status`) + VALUES (?, ?, ?, ?, ?, ?, ?) + ]], { + plate, + driverId, + activeManifestId, + grossWeightKg, + maxAllowedKg, + fine, + status + }) + + -- Trigger automated MDT dispatch notification if BYPASSED, OVERWEIGHT, or CONTRABAND_FLAG + if status == "BYPASSED" or status == "OVERWEIGHT" or status == "CONTRABAND_FLAG" then + local alertTitle = "Kereskedelmi Tengelysúly-Sértés" + local alertCode = "10-88" + local priority = "medium" + + if status == "BYPASSED" then + alertTitle = "Mérőállomás Megkerülése (Körözött Tehergépjármű)" + alertCode = "10-99" + priority = "high" + elseif status == "CONTRABAND_FLAG" then + alertTitle = "Gyanús Illegális Teher / Lepecsételt Raklap" + alertCode = "10-85" + priority = "high" + end + + local alertDesc = string.format( + "Jármű Rendszám: %s | Vezető: %s | Súly: %.1f kg / %.1f kg max | Bírság: $%.2f | Státusz: %s", + plate, driverId, grossWeightKg, maxAllowedKg, fine, status + ) + + if exports['nyx_mdt'] and exports['nyx_mdt'].CreateCall then + pcall(function() + exports['nyx_mdt']:CreateCall( + alertCode, + alertTitle, + alertDesc, + "Automated Scale Sensor", + { x = 2500.0, y = 4200.0, z = 45.0 }, + "Senora Freeway Commercial Scale Pad", + priority + ) + end) + end + end + + return { + logId = logId, + status = status, + grossWeightKg = grossWeightKg, + maxAllowedKg = maxAllowedKg, + fine = fine + } +end + +-- RPC Endpoints +Nyx.RPC.Register("nyx_vehicles:processToll", function(src, data) + data = data or {} + return Nyx.Vehicles.ProcessTollPass(src, data.plazaId, data.plate, data.vehicleClass) +end) + +Nyx.RPC.Register("nyx_vehicles:inspectWeighStation", function(src, data) + data = data or {} + return Nyx.Vehicles.InspectWeighStation(src, data.plate, data.vehicleClass, data.didBypass) +end) + +-- Register Net Events +RegisterNetEvent("nyx_vehicles:server:processToll", function(plazaId, plate, vehicleClass) + local src = source + Nyx.Vehicles.ProcessTollPass(src, plazaId, plate, vehicleClass) +end) + +RegisterNetEvent("nyx_vehicles:server:inspectWeighStation", function(plate, vehicleClass, didBypass) + local src = source + Nyx.Vehicles.InspectWeighStation(src, plate, vehicleClass, didBypass) +end) diff --git a/FiveM/nyx_vehicles/shared/tolls_and_weighing.lua b/FiveM/nyx_vehicles/shared/tolls_and_weighing.lua new file mode 100644 index 0000000..d5d2845 --- /dev/null +++ b/FiveM/nyx_vehicles/shared/tolls_and_weighing.lua @@ -0,0 +1,143 @@ +--[[ + Nyxframework: nyx_vehicles + File: shared/tolls_and_weighing.lua + Description: Pure deterministic calculations for toll fees, Gross Vehicle Weight Ratings (GVWR), + overweight fines, commercial axle multipliers, and weigh station bypass thresholds. +]] + +Nyx = Nyx or {} +Nyx.TollsAndWeighing = Nyx.TollsAndWeighing or {} + +-- Round value cleanly to exact two decimal places (cents) +function Nyx.TollsAndWeighing.RoundToCent(amount) + amount = tonumber(amount) or 0.0 + return math.floor(amount * 100.0 + 0.5) / 100.0 +end + +-- Default base curb weights by vehicle class (in kg) +-- Vehicle Classes: 0: Compacts, 1: Sedans, 2: SUVs, 3: Coupes, 4: Muscle, 5: Sports Classics, +-- 6: Sports, 7: Super, 8: Motorcycles, 9: Off-road, 10: Industrial, 11: Utility, 12: Vans, 20: Commercial +Nyx.TollsAndWeighing.BaseCurbWeights = { + [0] = 1200.0, -- Compacts + [1] = 1600.0, -- Sedans + [2] = 2200.0, -- SUVs + [3] = 1400.0, -- Coupes + [4] = 1700.0, -- Muscle + [5] = 1300.0, -- Sports Classics + [6] = 1500.0, -- Sports + [7] = 1450.0, -- Super + [8] = 250.0, -- Motorcycles + [9] = 2400.0, -- Off-road + [10] = 8000.0, -- Industrial + [11] = 3500.0, -- Utility + [12] = 2800.0, -- Vans + [20] = 12000.0 -- Commercial (Semis, Heavy Haulers) +} + +-- Standard Axle counts by class +Nyx.TollsAndWeighing.ClassAxleCounts = { + [8] = 2, -- Motorcycle + [10] = 4, -- Industrial (Heavy) + [20] = 5 -- Commercial 18-wheeler / Semi +} + +-- Base Max Allowed Weights (in kg) +Nyx.TollsAndWeighing.MaxAllowedWeights = { + [10] = 25000.0, -- Industrial + [11] = 10000.0, -- Utility + [12] = 8000.0, -- Vans + [20] = 40000.0, -- Commercial Heavy + ["default"] = 15000.0 +} + +--- Pure calculation for highway toll fees +--- @param vehicleClass number GTA V vehicle class integer (0..22, 20=commercial) +--- @param baseFee number Base toll fee in dollars +--- @param commercialAxleMultiplier number Multiplier applied for commercial / multi-axle heavy vehicles +--- @param statutoryTaxRate number Dynamic statutory transit tax modifier (default 1.0) +--- @return number finalFee Total toll fee rounded to cent +function Nyx.TollsAndWeighing.CalculateTollFee(vehicleClass, baseFee, commercialAxleMultiplier, statutoryTaxRate) + vehicleClass = tonumber(vehicleClass) or 0 + baseFee = tonumber(baseFee) or 5.00 + commercialAxleMultiplier = tonumber(commercialAxleMultiplier) or 1.50 + statutoryTaxRate = tonumber(statutoryTaxRate) or 1.00 + + local isCommercial = (vehicleClass == 10 or vehicleClass == 20 or vehicleClass == 11) + local axles = Nyx.TollsAndWeighing.ClassAxleCounts[vehicleClass] or 2 + + local effectiveFee = baseFee + if isCommercial then + effectiveFee = baseFee * (1.0 + (axles - 2) * 0.25) * commercialAxleMultiplier + end + + effectiveFee = effectiveFee * math.max(0.1, statutoryTaxRate) + return Nyx.TollsAndWeighing.RoundToCent(effectiveFee) +end + +--- Pure calculation for Gross Vehicle Weight Rating (GVWR) +--- @param vehicleClass number GTA vehicle class +--- @param trunkCargoKg number Trunk / Glovebox inventory weight in kg +--- @param loadedPalletsKg number Loaded physical pallet cargo weight in kg +--- @param customCurbWeight number Optional override for vehicle base curb weight in kg +--- @return number grossWeightKg Combined gross weight in kg +function Nyx.TollsAndWeighing.CalculateGVWR(vehicleClass, trunkCargoKg, loadedPalletsKg, customCurbWeight) + vehicleClass = tonumber(vehicleClass) or 0 + trunkCargoKg = tonumber(trunkCargoKg) or 0.0 + loadedPalletsKg = tonumber(loadedPalletsKg) or 0.0 + + local curb = tonumber(customCurbWeight) + if not curb or curb <= 0 then + curb = Nyx.TollsAndWeighing.BaseCurbWeights[vehicleClass] or 1800.0 + end + + local total = curb + math.max(0.0, trunkCargoKg) + math.max(0.0, loadedPalletsKg) + return Nyx.TollsAndWeighing.RoundToCent(total) +end + +--- Get max allowed gross weight limit for a vehicle class +--- @param vehicleClass number +--- @return number maxAllowedKg Max allowed weight in kg +function Nyx.TollsAndWeighing.GetMaxAllowedWeight(vehicleClass) + vehicleClass = tonumber(vehicleClass) or 0 + return Nyx.TollsAndWeighing.MaxAllowedWeights[vehicleClass] or Nyx.TollsAndWeighing.MaxAllowedWeights["default"] +end + +--- Calculate statutory overweight citation fine +--- @param grossWeightKg number Measured vehicle weight +--- @param maxAllowedKg number Statutory max weight limit +--- @param baseFinePerExcessKg number Fine per excess kg (default $2.50 / kg) +--- @return number fine Statutory fine rounded to cent +function Nyx.TollsAndWeighing.CalculateOverweightFine(grossWeightKg, maxAllowedKg, baseFinePerExcessKg) + grossWeightKg = tonumber(grossWeightKg) or 0.0 + maxAllowedKg = tonumber(maxAllowedKg) or 15000.0 + baseFinePerExcessKg = tonumber(baseFinePerExcessKg) or 2.50 + + local excessKg = grossWeightKg - maxAllowedKg + if excessKg <= 0 then + return 0.00 + end + + -- Mandatory base administrative fine $250.00 + excess weight charge + local totalFine = 250.00 + (excessKg * baseFinePerExcessKg) + return Nyx.TollsAndWeighing.RoundToCent(totalFine) +end + +--- Determine weigh station inspection outcome status +--- @param grossWeightKg number Measured weight +--- @param maxAllowedKg number Max weight +--- @param hasValidManifest boolean Whether freight manifest is valid +--- @param isContrabandFlagged boolean Whether cargo contains illicit or unmanifested contraband +--- @param didBypass boolean Whether vehicle bypassed scale without halting +--- @return string status ('PASS', 'OVERWEIGHT', 'CONTRABAND_FLAG', 'BYPASSED') +function Nyx.TollsAndWeighing.DetermineInspectionStatus(grossWeightKg, maxAllowedKg, hasValidManifest, isContrabandFlagged, didBypass) + if didBypass then + return "BYPASSED" + end + if isContrabandFlagged or (not hasValidManifest and (grossWeightKg > 5000.0)) then + return "CONTRABAND_FLAG" + end + if tonumber(grossWeightKg) > tonumber(maxAllowedKg) then + return "OVERWEIGHT" + end + return "PASS" +end diff --git a/FiveM/nyx_vehicles/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql b/FiveM/nyx_vehicles/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql new file mode 100644 index 0000000..61ea02f --- /dev/null +++ b/FiveM/nyx_vehicles/sql/migrations/066_municipal_toll_booths_and_commercial_weigh_stations.sql @@ -0,0 +1,54 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_mdt +-- Migration: 066_municipal_toll_booths_and_commercial_weigh_stations.sql +-- Description: Municipal transit infrastructure, highway toll plazas, +-- automated RFID toll deduction, commercial axle multipliers, +-- and commercial weigh station inspection/bypass logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create municipal toll plazas table +CREATE TABLE IF NOT EXISTS `nyx_toll_plazas` ( + `plaza_id` VARCHAR(32) NOT NULL PRIMARY KEY, + `zone_name` VARCHAR(64) NOT NULL, + `base_fee` DECIMAL(10, 2) NOT NULL DEFAULT 5.00, + `commercial_axle_multiplier` DECIMAL(5, 2) NOT NULL DEFAULT 1.50, + `active_status` TINYINT(1) NOT NULL DEFAULT 1, + `total_collected` DECIMAL(15, 2) NOT NULL DEFAULT 0.00, + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX `idx_toll_zone` (`zone_name`), + INDEX `idx_toll_status` (`active_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create automated commercial weigh station inspection logs table +CREATE TABLE IF NOT EXISTS `nyx_weigh_station_logs` ( + `log_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL, + `manifest_id` VARCHAR(64) NULL, + `gross_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `max_allowed_kg` DECIMAL(10, 2) NOT NULL DEFAULT 15000.00, + `overweight_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `inspection_status` ENUM('PASS', 'OVERWEIGHT', 'CONTRABAND_FLAG', 'BYPASSED') NOT NULL DEFAULT 'PASS', + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_weigh_plate` (`vehicle_plate`), + INDEX `idx_weigh_driver` (`driver_identifier`), + INDEX `idx_weigh_manifest` (`manifest_id`), + INDEX `idx_weigh_status` (`inspection_status`), + INDEX `idx_weigh_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal highway toll plazas +INSERT INTO `nyx_toll_plazas` + (`plaza_id`, `zone_name`, `base_fee`, `commercial_axle_multiplier`, `active_status`, `total_collected`) +VALUES + ('plaza_del_perro_fwy', 'Del Perro Freeway Gantry', 5.00, 1.50, 1, 0.00), + ('plaza_la_puerta_fwy', 'La Puerta Freeway Toll Plaza', 7.50, 1.75, 1, 0.00), + ('plaza_palomino_fwy', 'Palomino Freeway East Barrier', 10.00, 2.00, 1, 0.00), + ('plaza_senora_fwy', 'Senora Freeway North Express Gantry', 6.00, 1.60, 1, 0.00) +ON DUPLICATE KEY UPDATE `zone_name` = VALUES(`zone_name`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_municipal_toll_booths_and_commercial_weigh_stations.py b/tests/test_municipal_toll_booths_and_commercial_weigh_stations.py new file mode 100644 index 0000000..52200c8 --- /dev/null +++ b/tests/test_municipal_toll_booths_and_commercial_weigh_stations.py @@ -0,0 +1,244 @@ +""" +Tests for Municipal Transit, Toll Booth Metering & Automated Commercial Weigh Stations (Migration 066) +Validates SQL migration 066 parity across module folders and index.json, +manifest declarations, server and client exports, in-engine Test 61 registration, +and pure Python/Lua deterministic algorithms for toll fee calculation, GVWR weight ratings, +overweight fine scaling, inspection outcomes, and MDT dispatch alert generation. +""" + +import json +import math +import os +import pytest + +ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +CORE_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "sql", "migrations", "066_municipal_toll_booths_and_commercial_weigh_stations.sql" +) +VEHICLES_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "sql", "migrations", "066_municipal_toll_booths_and_commercial_weigh_stations.sql" +) +LOGISTICS_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_logistics", "sql", "migrations", "066_municipal_toll_booths_and_commercial_weigh_stations.sql" +) +TREASURY_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_treasury", "sql", "migrations", "066_municipal_toll_booths_and_commercial_weigh_stations.sql" +) +INDEX_JSON_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "sql", "migrations", "index.json" +) +VEHICLES_MANIFEST_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "fxmanifest.lua" +) +SHARED_TOLLS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "shared", "tolls_and_weighing.lua" +) +SERVER_TOLLS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "server", "tolls_and_weighing.lua" +) +SERVER_EXPORTS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "server", "exports.lua" +) +CLIENT_TOLLS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "client", "tolls_and_weighing.lua" +) +TESTS_LUA_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "server", "tests.lua" +) + + +class TollsAndWeighingEngine: + BASE_CURB_WEIGHTS = { + 0: 1200.0, + 1: 1600.0, + 2: 2200.0, + 3: 1400.0, + 4: 1700.0, + 5: 1300.0, + 6: 1500.0, + 7: 1450.0, + 8: 250.0, + 9: 2400.0, + 10: 8000.0, + 11: 3500.0, + 12: 2800.0, + 20: 12000.0, + } + + CLASS_AXLE_COUNTS = { + 8: 2, + 10: 4, + 20: 5, + } + + MAX_ALLOWED_WEIGHTS = { + 10: 25000.0, + 11: 10000.0, + 12: 8000.0, + 20: 40000.0, + "default": 15000.0, + } + + @staticmethod + def round_to_cent(val): + val = float(val or 0.0) + return math.floor(val * 100.0 + 0.5) / 100.0 + + @classmethod + def calculate_toll_fee(cls, vehicle_class, base_fee=5.00, commercial_multiplier=1.50, statutory_tax=1.00): + vehicle_class = int(vehicle_class or 0) + base_fee = float(base_fee or 5.00) + commercial_multiplier = float(commercial_multiplier or 1.50) + statutory_tax = float(statutory_tax or 1.00) + + is_commercial = vehicle_class in (10, 11, 20) + axles = cls.CLASS_AXLE_COUNTS.get(vehicle_class, 2) + + effective_fee = base_fee + if is_commercial: + effective_fee = base_fee * (1.0 + (axles - 2) * 0.25) * commercial_multiplier + + effective_fee *= max(0.1, statutory_tax) + return cls.round_to_cent(effective_fee) + + @classmethod + def calculate_gvwr(cls, vehicle_class, trunk_cargo_kg=0.0, loaded_pallets_kg=0.0, custom_curb=None): + vehicle_class = int(vehicle_class or 0) + trunk_cargo_kg = float(trunk_cargo_kg or 0.0) + loaded_pallets_kg = float(loaded_pallets_kg or 0.0) + + curb = float(custom_curb) if custom_curb is not None else cls.BASE_CURB_WEIGHTS.get(vehicle_class, 1800.0) + total = curb + max(0.0, trunk_cargo_kg) + max(0.0, loaded_pallets_kg) + return cls.round_to_cent(total) + + @classmethod + def get_max_allowed_weight(cls, vehicle_class): + vehicle_class = int(vehicle_class or 0) + return cls.MAX_ALLOWED_WEIGHTS.get(vehicle_class, cls.MAX_ALLOWED_WEIGHTS["default"]) + + @classmethod + def calculate_overweight_fine(cls, gross_weight_kg, max_allowed_kg, base_fine_per_excess=2.50): + gross_weight_kg = float(gross_weight_kg or 0.0) + max_allowed_kg = float(max_allowed_kg or 15000.0) + base_fine_per_excess = float(base_fine_per_excess or 2.50) + + excess_kg = gross_weight_kg - max_allowed_kg + if excess_kg <= 0: + return 0.00 + + total_fine = 250.00 + (excess_kg * base_fine_per_excess) + return cls.round_to_cent(total_fine) + + @classmethod + def determine_inspection_status(cls, gross_weight_kg, max_allowed_kg, has_valid_manifest, is_contraband_flagged, did_bypass): + if did_bypass: + return "BYPASSED" + if is_contraband_flagged or (not has_valid_manifest and gross_weight_kg > 5000.0): + return "CONTRABAND_FLAG" + if gross_weight_kg > max_allowed_kg: + return "OVERWEIGHT" + return "PASS" + + +def test_migration_066_files_and_registration(): + """Verify SQL migration 066 exists in all 4 required modules and is registered in index.json.""" + assert os.path.exists(CORE_MIGRATION_PATH), "Core migration 066 must exist" + assert os.path.exists(VEHICLES_MIGRATION_PATH), "Vehicles migration 066 must exist" + assert os.path.exists(LOGISTICS_MIGRATION_PATH), "Logistics migration 066 must exist" + assert os.path.exists(TREASURY_MIGRATION_PATH), "Treasury migration 066 must exist" + + with open(INDEX_JSON_PATH, "r", encoding="utf-8") as f: + registered = json.load(f) + + assert "066_municipal_toll_booths_and_commercial_weigh_stations.sql" in registered, ( + "066 migration must be registered in index.json" + ) + + with open(CORE_MIGRATION_PATH, "r", encoding="utf-8") as f: + sql = f.read() + + assert "CREATE TABLE IF NOT EXISTS `nyx_toll_plazas`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_weigh_station_logs`" in sql + assert "plaza_del_perro_fwy" in sql + assert "commercial_axle_multiplier" in sql + assert "CONTRABAND_FLAG" in sql + + +def test_manifest_and_exports_declarations(): + """Verify scripts are registered in fxmanifest.lua and server exports.""" + with open(VEHICLES_MANIFEST_PATH, "r", encoding="utf-8") as f: + manifest = f.read() + + assert "shared/tolls_and_weighing.lua" in manifest, "shared/tolls_and_weighing.lua must be declared" + assert "server/tolls_and_weighing.lua" in manifest, "server/tolls_and_weighing.lua must be declared" + assert "client/tolls_and_weighing.lua" in manifest, "client/tolls_and_weighing.lua must be declared" + + with open(SERVER_EXPORTS_PATH, "r", encoding="utf-8") as f: + exports = f.read() + + assert "exports(\"ProcessTollPass\"" in exports + assert "exports(\"InspectWeighStation\"" in exports + assert "exports(\"CalculateTollFee\"" in exports + assert "exports(\"CalculateGVWR\"" in exports + assert "exports(\"CalculateOverweightFine\"" in exports + + +def test_in_engine_test_61_registration(): + """Verify Test 61 (TestMunicipalTollsAndWeighStations) is registered in tests.lua.""" + with open(TESTS_LUA_PATH, "r", encoding="utf-8") as f: + tests_lua = f.read() + + assert "function Nyx.Tests.TestMunicipalTollsAndWeighStations()" in tests_lua + assert "local t61 = Nyx.Tests.TestMunicipalTollsAndWeighStations()" in tests_lua + + +def test_toll_fee_math(): + """Verify toll fee calculation math for passenger and commercial vehicles.""" + # Sedan (Class 1) -> base $5.00 + sedan_fee = TollsAndWeighingEngine.calculate_toll_fee(1, 5.00, 1.50, 1.00) + assert sedan_fee == 5.00 + + # Commercial 5-axle semi (Class 20) -> $5.00 * (1.0 + (5-2)*0.25) * 1.50 = 5.00 * 1.75 * 1.50 = 13.125 -> $13.13 + semi_fee = TollsAndWeighingEngine.calculate_toll_fee(20, 5.00, 1.50, 1.00) + assert semi_fee == 13.13 + + # Dynamic statutory transit tax modifier (1.20) + taxed_fee = TollsAndWeighingEngine.calculate_toll_fee(20, 5.00, 1.50, 1.20) + assert taxed_fee == 15.75 + + +def test_gvwr_gross_weight_rating(): + """Verify Gross Vehicle Weight Rating (GVWR) calculation combining curb, trunk, and pallet load.""" + sedan_gvwr = TollsAndWeighingEngine.calculate_gvwr(1, 150.0, 0.0) + assert sedan_gvwr == 1750.0 # 1600.0 + 150.0 + + semi_gvwr = TollsAndWeighingEngine.calculate_gvwr(20, 500.0, 15000.0) + assert semi_gvwr == 27500.0 # 12000.0 + 500.0 + 15000.0 + + +def test_overweight_fine_calculation(): + """Verify statutory overweight citation fine calculations.""" + zero_fine = TollsAndWeighingEngine.calculate_overweight_fine(14000.0, 15000.0) + assert zero_fine == 0.00 + + # 17,000 kg vs 15,000 kg max -> 2,000 kg excess -> $250 base + 2000 * $2.50 = $5,250.00 + excess_fine = TollsAndWeighingEngine.calculate_overweight_fine(17000.0, 15000.0, 2.50) + assert excess_fine == 5250.00 + + +def test_weigh_station_inspection_status_resolution(): + """Verify inspection outcome status determination for pass, overweight, contraband, and bypass.""" + status_pass = TollsAndWeighingEngine.determine_inspection_status(14000.0, 15000.0, True, False, False) + assert status_pass == "PASS" + + status_over = TollsAndWeighingEngine.determine_inspection_status(17000.0, 15000.0, True, False, False) + assert status_over == "OVERWEIGHT" + + status_contraband = TollsAndWeighingEngine.determine_inspection_status(14000.0, 15000.0, True, True, False) + assert status_contraband == "CONTRABAND_FLAG" + + status_unmanifested = TollsAndWeighingEngine.determine_inspection_status(8000.0, 15000.0, False, False, False) + assert status_unmanifested == "CONTRABAND_FLAG" + + status_bypass = TollsAndWeighingEngine.determine_inspection_status(14000.0, 15000.0, True, False, True) + assert status_bypass == "BYPASSED" From e2eb0188fae8947d460ba9be0778e9abea3e45e7 Mon Sep 17 00:00:00 2001 From: Lacikika <114676193+Lacikika@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:52:49 +0000 Subject: [PATCH 2/2] feat: implement Migration 066 for municipal tolls and weigh stations - Create migration 066 SQL schema in nyx_core, nyx_vehicles, nyx_logistics, and nyx_treasury - Register migration 066 in index.json - Add shared/tolls_and_weighing.lua for pure toll, GVWR, and overweight fine math - Add server/tolls_and_weighing.lua for RFID/plate toll deduction, treasury routing, and MDT dispatch alerts - Add client/tolls_and_weighing.lua for spatial gantry/scale pad triggers and NUI notifications - Export server APIs in exports.lua and register Test 61 in nyx_core/server/tests.lua - Update requirements.txt and ci.yml for CI workflow caching - Add unit test suite tests/test_municipal_toll_booths_and_commercial_weigh_stations.py Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- FiveM/nyx_logistics/config.lua | 9 +++++++-- FiveM/nyx_logistics/fxmanifest.lua | 4 +++- requirements.txt | 1 + 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 892e879..e477fb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + pip install -r requirements.txt - name: Run manifest validator run: python scripts/validator.py diff --git a/FiveM/nyx_logistics/config.lua b/FiveM/nyx_logistics/config.lua index dd3cc71..7d67e47 100644 --- a/FiveM/nyx_logistics/config.lua +++ b/FiveM/nyx_logistics/config.lua @@ -22,6 +22,13 @@ Config.PalletModel = "prop_boxpile_06a" Config.PalletMaxWeight = 1000000 -- 1,000 kg (1 metric ton) per freight pallet Config.PalletMaxSlots = 20 -- Heavy slot capacity +-- Commercial Freight Weigh Stations & Toll Integration +Config.WeighStations = { + MaxCommercialGrossWeightKg = 40000.0, -- Maximum 40 metric tons total GVWR for commercial haulers + ScalePadStabilizationSeconds = 3, -- Required stationary time on scale pad + MandatoryFreightManifest = true -- Requires active haul contract / manifest ID when passing scale +} + -- Automated Shortage Contracts Config.ShortageContractThreshold = 0.10 -- Generate freight contract when stock <= 10% of target capacity Config.DefaultFreightPayoutMultiplier = 0.35 -- Payout per delivered unit = 35% of item base price @@ -52,5 +59,3 @@ Config.Warehouse = { "prop_boxpile_06a" } } - - diff --git a/FiveM/nyx_logistics/fxmanifest.lua b/FiveM/nyx_logistics/fxmanifest.lua index c2c5254..6730267 100644 --- a/FiveM/nyx_logistics/fxmanifest.lua +++ b/FiveM/nyx_logistics/fxmanifest.lua @@ -11,11 +11,13 @@ dependencies { 'oxmysql', 'nyx_core', 'nyx_inventory', - 'nyx_treasury' + 'nyx_treasury', + 'nyx_vehicles' } shared_scripts { '@nyx_core/shared/math.lua', + '@nyx_vehicles/shared/tolls_and_weighing.lua', 'config.lua', 'shared/zones.lua', 'shared/nodes.lua', diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e144ed0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest>=8.0.0