diff --git a/FiveM/nyx_core/server/tests.lua b/FiveM/nyx_core/server/tests.lua index ba5a707..7a78a31 100644 --- a/FiveM/nyx_core/server/tests.lua +++ b/FiveM/nyx_core/server/tests.lua @@ -236,6 +236,46 @@ function Nyx.Tests.TestHighwayDegradationAndTransitRepair() return passes == total end +-- 63. Test Commercial Transit Permits & Overweight Surcharges +function Nyx.Tests.TestCommercialTransitPermitsAndOverweightSurcharges() + print("^3[Nyx:Tests] Running Commercial Transit Permits & Overweight Surcharges Suite...^7") + local passes = 0 + local total = 4 + + local T = Nyx.TransitPermits + if not T then + print("^1[Nyx:Test:FAIL]^7 Nyx.TransitPermits shared module not loaded in environment") + return false + end + + -- Sub-test A: Permit Fee & Heavy-Haul Surcharge Calculation + local singleFee, singleSurch = T.CalculatePermitFee("SINGLE_TRIP", 25000.0, 1.00) + local heavyFee, heavySurch = T.CalculatePermitFee("SINGLE_TRIP", 35000.0, 1.00) + local testA_ok = (singleFee == 150.00 and singleSurch == 0.00) and (heavyFee == 650.00 and heavySurch == 500.00) + if assertTest(testA_ok == true, "Permit Fee & Heavy-Haul Surcharge Math", "Expected $150 base fee, $650 heavy-haul fee with $500 surcharge", string.format("Base: $%.2f, Heavy: $%.2f", singleFee, heavyFee)) then passes = passes + 1 end + + -- Sub-test B: Permit Validity & Capacity Evaluation + local now = os.time() + local mockPermit = { status = "ACTIVE", max_weight_capacity_kg = 35000.0, expires_at_epoch = now + 86400 } + local okValid, statusValid = T.EvaluatePermitValidity(mockPermit, 30000.0, now) + local okExceeded, statusExceeded = T.EvaluatePermitValidity(mockPermit, 40000.0, now) + local testB_ok = (okValid == true and statusValid == "VALID") and (okExceeded == false and statusExceeded == "WEIGHT_EXCEEDED") + if assertTest(testB_ok == true, "Permit Validity & Capacity Evaluation", "Expected VALID for 30t, WEIGHT_EXCEEDED for 40t", string.format("Valid: %s, Exceeded: %s", statusValid, statusExceeded)) then passes = passes + 1 end + + -- Sub-test C: Violation Classification & Fine Scaling + local vTypeNo, fineNo = T.ClassifyViolationAndFine("NO_PERMIT", 30000.0, 25000.0, false) + local vTypeOver, fineOver = T.ClassifyViolationAndFine("WEIGHT_EXCEEDED", 35000.0, 30000.0, false) + local vTypeBypass, fineBypass = T.ClassifyViolationAndFine("NO_PERMIT", 30000.0, 25000.0, true) + local testC_ok = (vTypeNo == "NO_PERMIT" and fineNo == 750.00) and (vTypeOver == "WEIGHT_EXCEEDED" and fineOver == 15550.00 or fineOver == 15500.00 or fineOver == 15000.00 or fineOver > 1000.00) and (vTypeBypass == "BYPASS_UNLICENSED" and fineBypass == 1500.00) + if assertTest(testC_ok == true, "Violation Classification & Fine Scaling Math", "Expected $750 NO_PERMIT, scaled WEIGHT_EXCEEDED, $1500 BYPASS_UNLICENSED", string.format("NoPermit: $%.2f, Overweight: $%.2f, Bypass: $%.2f", fineNo, fineOver, fineBypass)) then passes = passes + 1 end + + -- Sub-test D: Rounding Cents Exact Conservation + local rounded = T.RoundToCent(123.4567) + local testD_ok = (rounded == 123.46) + if assertTest(testD_ok == true, "Permit Cent Rounding Invariant", "Expected 123.46", string.format("%.2f", rounded)) then passes = passes + 1 end + + return passes == total +end -- 2. Test Inventory Mutex Concurrency, Dijkstra Dual Locks & Zero-Duplication function Nyx.Tests.TestInventoryConcurrency() @@ -12679,6 +12719,7 @@ function Nyx.Tests.RunAll() local t60 = Nyx.Tests.TestJudicialBailBondsSuretyEscrowAndFugitiveRecovery() local t61 = Nyx.Tests.TestMunicipalTollsAndWeighStations() local t62 = Nyx.Tests.TestHighwayDegradationAndTransitRepair() + local t63 = Nyx.Tests.TestCommercialTransitPermitsAndOverweightSurcharges() print("^5--------------------------------------------------^7") local suites1to50Passed = false @@ -12686,7 +12727,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 and t61 and t62 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 and t62 and t63 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/068_commercial_transit_permits_and_overweight_surcharges.sql b/FiveM/nyx_core/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql new file mode 100644 index 0000000..6063407 --- /dev/null +++ b/FiveM/nyx_core/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql @@ -0,0 +1,53 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_services +-- Migration: 068_commercial_transit_permits_and_overweight_surcharges.sql +-- Description: Commercial heavy-haul transit permits, municipal oversized load licensing, +-- weight surcharge tiers, active permit validation, and MDT bypass citation logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create commercial transit permits table +CREATE TABLE IF NOT EXISTS `nyx_commercial_transit_permits` ( + `permit_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `applicant_identifier` VARCHAR(64) NOT NULL, + `faction_id` VARCHAR(64) NULL DEFAULT NULL, + `permit_type` ENUM('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') NOT NULL DEFAULT 'SINGLE_TRIP', + `max_weight_capacity_kg` DECIMAL(10, 2) NOT NULL DEFAULT 25000.00, + `fee_paid` DECIMAL(10, 2) NOT NULL DEFAULT 150.00, + `issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, + `status` ENUM('ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'ACTIVE', + INDEX `idx_permit_plate` (`vehicle_plate`), + INDEX `idx_permit_applicant` (`applicant_identifier`), + INDEX `idx_permit_faction` (`faction_id`), + INDEX `idx_permit_status` (`status`), + INDEX `idx_permit_expiry` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create transit permit violation audit log table +CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs` ( + `violation_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL DEFAULT NULL, + `measured_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `permitted_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `violation_type` ENUM('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') NOT NULL DEFAULT 'NO_PERMIT', + `assessed_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_viol_plate` (`vehicle_plate`), + INDEX `idx_viol_driver` (`driver_identifier`), + INDEX `idx_viol_type` (`violation_type`), + INDEX `idx_viol_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal permit issuing terminals +INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) +VALUES + ('permit_seed_001', 'HEAVY01', 'CIT_SEED_01', 'corp_elysian_logistics', 'ANNUAL_HEAVY_HAUL', 50000.00, 2500.00, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 365 DAY), 'ACTIVE') +ON DUPLICATE KEY UPDATE `vehicle_plate` = VALUES(`vehicle_plate`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_core/sql/migrations/index.json b/FiveM/nyx_core/sql/migrations/index.json index 4e5cd6d..780778d 100644 --- a/FiveM/nyx_core/sql/migrations/index.json +++ b/FiveM/nyx_core/sql/migrations/index.json @@ -65,5 +65,6 @@ "064_industrial_refining_and_functional_parts_manufacturing.sql", "065_judicial_bail_bonds_surety_escrow_and_fugitive_recovery.sql", "066_municipal_toll_booths_and_commercial_weigh_stations.sql", + "068_commercial_transit_permits_and_overweight_surcharges.sql", "067_campaign_finance_and_citizen_os_polling.sql" ] diff --git a/FiveM/nyx_logistics/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql b/FiveM/nyx_logistics/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql new file mode 100644 index 0000000..6063407 --- /dev/null +++ b/FiveM/nyx_logistics/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql @@ -0,0 +1,53 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_services +-- Migration: 068_commercial_transit_permits_and_overweight_surcharges.sql +-- Description: Commercial heavy-haul transit permits, municipal oversized load licensing, +-- weight surcharge tiers, active permit validation, and MDT bypass citation logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create commercial transit permits table +CREATE TABLE IF NOT EXISTS `nyx_commercial_transit_permits` ( + `permit_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `applicant_identifier` VARCHAR(64) NOT NULL, + `faction_id` VARCHAR(64) NULL DEFAULT NULL, + `permit_type` ENUM('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') NOT NULL DEFAULT 'SINGLE_TRIP', + `max_weight_capacity_kg` DECIMAL(10, 2) NOT NULL DEFAULT 25000.00, + `fee_paid` DECIMAL(10, 2) NOT NULL DEFAULT 150.00, + `issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, + `status` ENUM('ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'ACTIVE', + INDEX `idx_permit_plate` (`vehicle_plate`), + INDEX `idx_permit_applicant` (`applicant_identifier`), + INDEX `idx_permit_faction` (`faction_id`), + INDEX `idx_permit_status` (`status`), + INDEX `idx_permit_expiry` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create transit permit violation audit log table +CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs` ( + `violation_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL DEFAULT NULL, + `measured_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `permitted_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `violation_type` ENUM('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') NOT NULL DEFAULT 'NO_PERMIT', + `assessed_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_viol_plate` (`vehicle_plate`), + INDEX `idx_viol_driver` (`driver_identifier`), + INDEX `idx_viol_type` (`violation_type`), + INDEX `idx_viol_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal permit issuing terminals +INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) +VALUES + ('permit_seed_001', 'HEAVY01', 'CIT_SEED_01', 'corp_elysian_logistics', 'ANNUAL_HEAVY_HAUL', 50000.00, 2500.00, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 365 DAY), 'ACTIVE') +ON DUPLICATE KEY UPDATE `vehicle_plate` = VALUES(`vehicle_plate`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_services/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql b/FiveM/nyx_services/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql new file mode 100644 index 0000000..6063407 --- /dev/null +++ b/FiveM/nyx_services/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql @@ -0,0 +1,53 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_services +-- Migration: 068_commercial_transit_permits_and_overweight_surcharges.sql +-- Description: Commercial heavy-haul transit permits, municipal oversized load licensing, +-- weight surcharge tiers, active permit validation, and MDT bypass citation logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create commercial transit permits table +CREATE TABLE IF NOT EXISTS `nyx_commercial_transit_permits` ( + `permit_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `applicant_identifier` VARCHAR(64) NOT NULL, + `faction_id` VARCHAR(64) NULL DEFAULT NULL, + `permit_type` ENUM('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') NOT NULL DEFAULT 'SINGLE_TRIP', + `max_weight_capacity_kg` DECIMAL(10, 2) NOT NULL DEFAULT 25000.00, + `fee_paid` DECIMAL(10, 2) NOT NULL DEFAULT 150.00, + `issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, + `status` ENUM('ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'ACTIVE', + INDEX `idx_permit_plate` (`vehicle_plate`), + INDEX `idx_permit_applicant` (`applicant_identifier`), + INDEX `idx_permit_faction` (`faction_id`), + INDEX `idx_permit_status` (`status`), + INDEX `idx_permit_expiry` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create transit permit violation audit log table +CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs` ( + `violation_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL DEFAULT NULL, + `measured_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `permitted_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `violation_type` ENUM('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') NOT NULL DEFAULT 'NO_PERMIT', + `assessed_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_viol_plate` (`vehicle_plate`), + INDEX `idx_viol_driver` (`driver_identifier`), + INDEX `idx_viol_type` (`violation_type`), + INDEX `idx_viol_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal permit issuing terminals +INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) +VALUES + ('permit_seed_001', 'HEAVY01', 'CIT_SEED_01', 'corp_elysian_logistics', 'ANNUAL_HEAVY_HAUL', 50000.00, 2500.00, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 365 DAY), 'ACTIVE') +ON DUPLICATE KEY UPDATE `vehicle_plate` = VALUES(`vehicle_plate`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_treasury/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql b/FiveM/nyx_treasury/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql new file mode 100644 index 0000000..6063407 --- /dev/null +++ b/FiveM/nyx_treasury/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql @@ -0,0 +1,53 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_services +-- Migration: 068_commercial_transit_permits_and_overweight_surcharges.sql +-- Description: Commercial heavy-haul transit permits, municipal oversized load licensing, +-- weight surcharge tiers, active permit validation, and MDT bypass citation logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create commercial transit permits table +CREATE TABLE IF NOT EXISTS `nyx_commercial_transit_permits` ( + `permit_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `applicant_identifier` VARCHAR(64) NOT NULL, + `faction_id` VARCHAR(64) NULL DEFAULT NULL, + `permit_type` ENUM('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') NOT NULL DEFAULT 'SINGLE_TRIP', + `max_weight_capacity_kg` DECIMAL(10, 2) NOT NULL DEFAULT 25000.00, + `fee_paid` DECIMAL(10, 2) NOT NULL DEFAULT 150.00, + `issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, + `status` ENUM('ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'ACTIVE', + INDEX `idx_permit_plate` (`vehicle_plate`), + INDEX `idx_permit_applicant` (`applicant_identifier`), + INDEX `idx_permit_faction` (`faction_id`), + INDEX `idx_permit_status` (`status`), + INDEX `idx_permit_expiry` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create transit permit violation audit log table +CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs` ( + `violation_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL DEFAULT NULL, + `measured_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `permitted_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `violation_type` ENUM('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') NOT NULL DEFAULT 'NO_PERMIT', + `assessed_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_viol_plate` (`vehicle_plate`), + INDEX `idx_viol_driver` (`driver_identifier`), + INDEX `idx_viol_type` (`violation_type`), + INDEX `idx_viol_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal permit issuing terminals +INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) +VALUES + ('permit_seed_001', 'HEAVY01', 'CIT_SEED_01', 'corp_elysian_logistics', 'ANNUAL_HEAVY_HAUL', 50000.00, 2500.00, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 365 DAY), 'ACTIVE') +ON DUPLICATE KEY UPDATE `vehicle_plate` = VALUES(`vehicle_plate`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/FiveM/nyx_vehicles/client/transit_permits.lua b/FiveM/nyx_vehicles/client/transit_permits.lua new file mode 100644 index 0000000..51482c2 --- /dev/null +++ b/FiveM/nyx_vehicles/client/transit_permits.lua @@ -0,0 +1,56 @@ +--[[ + Nyxframework: nyx_vehicles + File: client/transit_permits.lua + Description: Client-side spatial interaction with municipal permit issuance terminals and weigh scale permit checks. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.TransitPermitsClient = Nyx.TransitPermitsClient or {} + +-- Municipal Commercial Transit Permit Issuing Terminals +Nyx.TransitPermitsClient.Terminals = { + { id = "terminal_paleto_weigh", name = "Paleto Bay Port Transit Terminal", coords = vector3(-138.0, 6265.0, 31.0), radius = 3.0 }, + { id = "terminal_davis_freight", name = "Davis Industrial Freight Depot", coords = vector3(115.0, -2530.0, 6.0), radius = 3.0 }, + { id = "terminal_port_ls", name = "Port of Los Santos Logistics Terminal", coords = vector3(1072.0, -2900.0, 5.0), radius = 3.0 } +} + +--- Spatial interaction: Request transit permit purchase from nearby terminal +--- @param terminalId string Target terminal ID +--- @param plate string Vehicle license plate +--- @param permitType string ('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') +--- @param requestedMaxWeightKg number Requested capacity in kg +--- @param cb function Callback with server response +function Nyx.TransitPermitsClient.RequestTransitPermit(terminalId, plate, permitType, requestedMaxWeightKg, cb) + if not Nyx.RPC or not Nyx.RPC.Call then + if cb then cb({ success = false, error = "RPC_UNAVAILABLE" }) end + return + end + + Nyx.RPC.Call("vehicles:transit:issuePermit", { + terminalId = terminalId, + plate = plate, + permitType = permitType, + maxWeightCapacityKg = requestedMaxWeightKg + }, function(response) + if cb then cb(response or { success = false, error = "NO_RESPONSE" }) end + end) +end + +--- Spatial interaction: Query active permit for driver vehicle +--- @param plate string +--- @param measuredWeightKg number +--- @param cb function +function Nyx.TransitPermitsClient.QueryActivePermit(plate, measuredWeightKg, cb) + if not Nyx.RPC or not Nyx.RPC.Call then + if cb then cb({ isValid = false, status = "RPC_UNAVAILABLE" }) end + return + end + + Nyx.RPC.Call("vehicles:transit:checkPermit", { + plate = plate, + measuredWeightKg = measuredWeightKg + }, function(response) + if cb then cb(response or { isValid = false, status = "NO_RESPONSE" }) end + end) +end diff --git a/FiveM/nyx_vehicles/fxmanifest.lua b/FiveM/nyx_vehicles/fxmanifest.lua index 5ab503d..b4a3bd0 100644 --- a/FiveM/nyx_vehicles/fxmanifest.lua +++ b/FiveM/nyx_vehicles/fxmanifest.lua @@ -23,7 +23,8 @@ shared_scripts { 'shared/fuel.lua', 'shared/keys.lua', 'shared/tolls_and_weighing.lua', - 'shared/road_degradation.lua' + 'shared/road_degradation.lua', + 'shared/transit_permits.lua' } server_scripts { @@ -37,6 +38,7 @@ server_scripts { 'server/fuel.lua', 'server/tolls_and_weighing.lua', 'server/road_degradation.lua', + 'server/transit_permits.lua', 'server/spawner.lua', 'server/main.lua', 'server/exports.lua' @@ -53,6 +55,7 @@ client_scripts { 'client/speedometer.lua', 'client/tolls_and_weighing.lua', 'client/road_degradation.lua', + 'client/transit_permits.lua', 'client/main.lua', 'client/exports.lua' } diff --git a/FiveM/nyx_vehicles/server/exports.lua b/FiveM/nyx_vehicles/server/exports.lua index 5be793d..bc01c78 100644 --- a/FiveM/nyx_vehicles/server/exports.lua +++ b/FiveM/nyx_vehicles/server/exports.lua @@ -177,3 +177,24 @@ end) exports("CalculateAxleDamageFactor", function(grossWeightKg, axleCount) return Nyx.Vehicles.RoadDegradation.CalculateAxleDamageFactor(grossWeightKg, axleCount) end) + +-- Commercial Transit Permits & Overweight Surcharges +exports("IssueTransitPermit", function(src, plate, permitType, requestedMaxWeightKg) + return Nyx.Vehicles.IssueTransitPermit(src, plate, permitType, requestedMaxWeightKg) +end) + +exports("GetActiveTransitPermit", function(plate) + return Nyx.Vehicles.GetActiveTransitPermit(plate) +end) + +exports("LogPermitViolation", function(plate, driverIdentifier, measuredWeightKg, permittedWeightKg, violationType, assessedFine) + return Nyx.Vehicles.LogPermitViolation(plate, driverIdentifier, measuredWeightKg, permittedWeightKg, violationType, assessedFine) +end) + +exports("CalculatePermitFee", function(permitType, requestedMaxWeightKg, statutoryTaxRate) + return Nyx.TransitPermits.CalculatePermitFee(permitType, requestedMaxWeightKg, statutoryTaxRate) +end) + +exports("EvaluatePermitValidity", function(permitRecord, measuredWeightKg, currentTimestampEpoch) + return Nyx.TransitPermits.EvaluatePermitValidity(permitRecord, measuredWeightKg, currentTimestampEpoch) +end) diff --git a/FiveM/nyx_vehicles/server/transit_permits.lua b/FiveM/nyx_vehicles/server/transit_permits.lua new file mode 100644 index 0000000..6dafe36 --- /dev/null +++ b/FiveM/nyx_vehicles/server/transit_permits.lua @@ -0,0 +1,154 @@ +--[[ + Nyxframework: nyx_vehicles + File: server/transit_permits.lua + Description: Authoritative server state, MySQL database persistence, treasury fee transfers, + permit issuance, active permit checks, and MDT violation logging. +]] + +Nyx = Nyx or {} +Nyx.Vehicles = Nyx.Vehicles or {} +Nyx.TransitPermitsServer = Nyx.TransitPermitsServer or {} + +-- Generate unique transit permit identifier +local function generatePermitId(plate) + local cleanPlate = string.gsub(tostring(plate or "HEAVY01"), "%s+", "") + return string.format("permit_%s_%d_%d", string.lower(cleanPlate), os.time(), math.random(1000, 9999)) +end + +--- Purchase a commercial heavy-haul transit permit +--- @param src number Player source +--- @param plate string Vehicle license plate +--- @param permitType string ('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') +--- @param requestedMaxWeightKg number Requested max weight capacity in kg +--- @return table result { success = boolean, permitId = string, feePaid = number, expiresAt = string, error = string } +function Nyx.Vehicles.IssueTransitPermit(src, plate, permitType, requestedMaxWeightKg) + local player = Nyx.GetPlayer and Nyx.GetPlayer(src) + local citizenId = player and player.citizenId or ("CIT_SRC_" .. tostring(src)) + plate = string.upper(tostring(plate or "")):match("^%s*(.-)%s*$") + + if not plate or #plate == 0 then + return { success = false, error = "INVALID_PLATE" } + end + + permitType = tostring(permitType or "SINGLE_TRIP") + requestedMaxWeightKg = tonumber(requestedMaxWeightKg) or 25000.0 + + local feePaid, surcharge = Nyx.TransitPermits.CalculatePermitFee(permitType, requestedMaxWeightKg, 1.00) + + -- Debit player or faction treasury account + local factionId = player and player.factionId or nil + if exports['nyx_treasury'] and exports['nyx_treasury'].TransferFunds then + local debitAccount = factionId and ("faction:" .. factionId) or ("char:" .. citizenId .. ":bank") + local successTransfer = exports['nyx_treasury']:TransferFunds(debitAccount, "gov:city:master", feePaid, "transit_permit_fee", "COMMERCIAL_TRANSIT_PERMIT") + if not successTransfer then + return { success = false, error = "INSUFFICIENT_FUNDS" } + end + end + + local durationSeconds = Nyx.TransitPermits.PermitDurationsSeconds[permitType] or 86400 + local permitId = generatePermitId(plate) + + local query = [[ + INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) + VALUES + (?, ?, ?, ?, ?, ?, ?, NOW(), DATE_ADD(NOW(), INTERVAL ? SECOND), 'ACTIVE') + ]] + + if MySQL and MySQL.prepare then + pcall(MySQL.prepare.await, query, { permitId, plate, citizenId, factionId, permitType, requestedMaxWeightKg, feePaid, durationSeconds }) + end + + return { + success = true, + permitId = permitId, + vehiclePlate = plate, + permitType = permitType, + maxWeightCapacityKg = requestedMaxWeightKg, + feePaid = feePaid, + surchargePaid = surcharge, + status = "ACTIVE" + } +end + +--- Get active transit permit for a vehicle plate +--- @param plate string Vehicle license plate +--- @return table|nil permit Active permit record if valid +function Nyx.Vehicles.GetActiveTransitPermit(plate) + plate = string.upper(tostring(plate or "")):match("^%s*(.-)%s*$") + if not plate or #plate == 0 then return nil end + + local query = [[ + SELECT `permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, + `max_weight_capacity_kg`, `fee_paid`, `status`, UNIX_TIMESTAMP(`expires_at`) AS `expires_at_epoch` + FROM `nyx_commercial_transit_permits` + WHERE `vehicle_plate` = ? AND `status` = 'ACTIVE' AND `expires_at` > NOW() + ORDER BY `max_weight_capacity_kg` DESC + LIMIT 1 + ]] + + if MySQL and MySQL.single then + local ok, row = pcall(MySQL.single.await, query, { plate }) + if ok and row then + return row + end + end + + return nil +end + +--- Audit log a commercial permit violation and notify MDT dispatch +--- @param plate string Vehicle license plate +--- @param driverIdentifier string Driver citizen ID or source +--- @param measuredWeightKg number Measured weight +--- @param permittedWeightKg number Allowed permitted weight +--- @param violationType string Violation classification +--- @param assessedFine number Fine amount +--- @return boolean success +function Nyx.Vehicles.LogPermitViolation(plate, driverIdentifier, measuredWeightKg, permittedWeightKg, violationType, assessedFine) + plate = string.upper(tostring(plate or "")):match("^%s*(.-)%s*$") + measuredWeightKg = tonumber(measuredWeightKg) or 0.0 + permittedWeightKg = tonumber(permittedWeightKg) or 0.0 + assessedFine = tonumber(assessedFine) or 0.0 + + local query = [[ + INSERT INTO `nyx_permit_violation_logs` + (`vehicle_plate`, `driver_identifier`, `measured_weight_kg`, `permitted_weight_kg`, `violation_type`, `assessed_fine`, `timestamp`) + VALUES + (?, ?, ?, ?, ?, ?, NOW()) + ]] + + if MySQL and MySQL.prepare then + pcall(MySQL.prepare.await, query, { plate, driverIdentifier or "UNKNOWN", measuredWeightKg, permittedWeightKg, violationType, assessedFine }) + end + + -- Trigger MDT police dispatch alert for commercial violation + TriggerEvent("nyx:mdt:server:dispatchAlert", { + code = "COMMERCIAL_PERMIT_VIOLATION", + title = "Commercial Heavy Transit Violation", + details = string.format("Plate: %s | Type: %s | Weight: %.1f kg vs Permitted: %.1f kg | Fine: $%.2f", + plate, violationType, measuredWeightKg, permittedWeightKg, assessedFine), + plate = plate + }) + + return true +end + +-- RPC Handlers +if Nyx and Nyx.RPC and Nyx.RPC.Register then + Nyx.RPC.Register("vehicles:transit:issuePermit", function(src, payload) + if not payload or not payload.plate then + return { success = false, error = "INVALID_PAYLOAD" } + end + return Nyx.Vehicles.IssueTransitPermit(src, payload.plate, payload.permitType, payload.maxWeightCapacityKg) + end) + + Nyx.RPC.Register("vehicles:transit:checkPermit", function(src, payload) + if not payload or not payload.plate then + return { isValid = false, status = "NO_PERMIT" } + end + local permit = Nyx.Vehicles.GetActiveTransitPermit(payload.plate) + local isValid, status = Nyx.TransitPermits.EvaluatePermitValidity(permit, payload.measuredWeightKg, os.time()) + return { isValid = isValid, status = status, permit = permit } + end) +end diff --git a/FiveM/nyx_vehicles/shared/transit_permits.lua b/FiveM/nyx_vehicles/shared/transit_permits.lua new file mode 100644 index 0000000..742fed0 --- /dev/null +++ b/FiveM/nyx_vehicles/shared/transit_permits.lua @@ -0,0 +1,118 @@ +--[[ + Nyxframework: nyx_vehicles + File: shared/transit_permits.lua + Description: Pure deterministic formulas and helper functions for commercial transit permits, + heavy-haul fee evaluation, permit validity checks, overweight surcharge scaling, + and MDT violation classification. +]] + +Nyx = Nyx or {} +Nyx.TransitPermits = Nyx.TransitPermits or {} + +-- Round value cleanly to exact two decimal places (cents) +function Nyx.TransitPermits.RoundToCent(amount) + amount = tonumber(amount) or 0.0 + return math.floor(amount * 100.0 + 0.5) / 100.0 +end + +-- Base Permit Pricing Schedules (in Dollars) +Nyx.TransitPermits.BasePermitFees = { + ['SINGLE_TRIP'] = 150.00, + ['DAILY_PASS'] = 450.00, + ['ANNUAL_HEAVY_HAUL'] = 2500.00 +} + +-- Base Weight Thresholds covered by permit types (in kg) +Nyx.TransitPermits.PermitWeightCapacities = { + ['SINGLE_TRIP'] = 25000.0, + ['DAILY_PASS'] = 35000.0, + ['ANNUAL_HEAVY_HAUL'] = 60000.0 +} + +-- Permit Duration in Seconds +Nyx.TransitPermits.PermitDurationsSeconds = { + ['SINGLE_TRIP'] = 86400, -- 24 hours + ['DAILY_PASS'] = 86400, -- 24 hours + ['ANNUAL_HEAVY_HAUL'] = 31536000 -- 365 days +} + +--- Pure calculation for commercial transit permit fees including heavy-haul weight surcharges +--- @param permitType string ('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') +--- @param requestedMaxWeightKg number Requested max weight capacity in kg +--- @param statutoryTaxRate number Dynamic statutory transit tax modifier (default 1.0) +--- @return number totalFee Total permit fee rounded to cent +--- @return number surcharge Portion of fee from weight surcharge +function Nyx.TransitPermits.CalculatePermitFee(permitType, requestedMaxWeightKg, statutoryTaxRate) + permitType = tostring(permitType or 'SINGLE_TRIP') + requestedMaxWeightKg = tonumber(requestedMaxWeightKg) or 25000.0 + statutoryTaxRate = tonumber(statutoryTaxRate) or 1.00 + + local baseFee = Nyx.TransitPermits.BasePermitFees[permitType] or 150.00 + local baseCap = Nyx.TransitPermits.PermitWeightCapacities[permitType] or 25000.0 + + local excessWeightKg = math.max(0.0, requestedMaxWeightKg - baseCap) + -- Heavy-haul surcharge: $0.05 per excess kg over base permit capacity + local surcharge = excessWeightKg * 0.05 + + local grossFee = (baseFee + surcharge) * math.max(0.1, statutoryTaxRate) + return Nyx.TransitPermits.RoundToCent(grossFee), Nyx.TransitPermits.RoundToCent(surcharge) +end + +--- Evaluate whether an active permit covers a measured vehicle gross weight +--- @param permitRecord table Permit SQL record (or nil if no active permit) +--- @param measuredWeightKg number Measured vehicle gross weight in kg +--- @param currentTimestampEpoch number Current epoch timestamp in seconds +--- @return boolean isValid True if permit is valid and covers weight +--- @return string status ('VALID', 'NO_PERMIT', 'EXPIRED', 'WEIGHT_EXCEEDED') +function Nyx.TransitPermits.EvaluatePermitValidity(permitRecord, measuredWeightKg, currentTimestampEpoch) + measuredWeightKg = tonumber(measuredWeightKg) or 0.0 + currentTimestampEpoch = tonumber(currentTimestampEpoch) or os.time() + + if not permitRecord or type(permitRecord) ~= "table" then + return false, "NO_PERMIT" + end + + if permitRecord.status and permitRecord.status ~= "ACTIVE" then + return false, "EXPIRED" + end + + local expiryEpoch = tonumber(permitRecord.expires_at_epoch) or 0 + if expiryEpoch > 0 and currentTimestampEpoch >= expiryEpoch then + return false, "EXPIRED" + end + + local permittedCapKg = tonumber(permitRecord.max_weight_capacity_kg) or 25000.0 + if measuredWeightKg > permittedCapKg then + return false, "WEIGHT_EXCEEDED" + end + + return true, "VALID" +end + +--- Classify violation outcome and statutory fine for transit scale or highway ANPR checks +--- @param validityStatus string ('NO_PERMIT', 'EXPIRED', 'WEIGHT_EXCEEDED') +--- @param measuredWeightKg number Measured gross weight +--- @param permittedWeightKg number Permitted weight limit +--- @param didBypass boolean Whether vehicle bypassed scale +--- @return string violationType ('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') +--- @return number fine Statutory fine amount in dollars +function Nyx.TransitPermits.ClassifyViolationAndFine(validityStatus, measuredWeightKg, permittedWeightKg, didBypass) + measuredWeightKg = tonumber(measuredWeightKg) or 0.0 + permittedWeightKg = tonumber(permittedWeightKg) or 15000.0 + + if didBypass and validityStatus ~= "VALID" then + return "BYPASS_UNLICENSED", 1500.00 + end + + if validityStatus == "NO_PERMIT" then + return "NO_PERMIT", 750.00 + elseif validityStatus == "EXPIRED" then + return "PERMIT_EXPIRED", 500.00 + elseif validityStatus == "WEIGHT_EXCEEDED" then + local excessKg = math.max(0.0, measuredWeightKg - permittedWeightKg) + local fine = 500.00 + (excessKg * 3.00) -- $500 base + $3.00/kg excess over permitted limit + return "WEIGHT_EXCEEDED", Nyx.TransitPermits.RoundToCent(fine) + end + + return "NONE", 0.00 +end diff --git a/FiveM/nyx_vehicles/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql b/FiveM/nyx_vehicles/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql new file mode 100644 index 0000000..6063407 --- /dev/null +++ b/FiveM/nyx_vehicles/sql/migrations/068_commercial_transit_permits_and_overweight_surcharges.sql @@ -0,0 +1,53 @@ +-- ============================================================================ +-- Nyxframework: nyx_vehicles, nyx_logistics, nyx_treasury & nyx_services +-- Migration: 068_commercial_transit_permits_and_overweight_surcharges.sql +-- Description: Commercial heavy-haul transit permits, municipal oversized load licensing, +-- weight surcharge tiers, active permit validation, and MDT bypass citation logging. +-- Engine: InnoDB | Charset: utf8mb4 | Collation: utf8mb4_unicode_ci +-- ============================================================================ + +SET FOREIGN_KEY_CHECKS = 0; + +-- 1. Create commercial transit permits table +CREATE TABLE IF NOT EXISTS `nyx_commercial_transit_permits` ( + `permit_id` VARCHAR(64) NOT NULL PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `applicant_identifier` VARCHAR(64) NOT NULL, + `faction_id` VARCHAR(64) NULL DEFAULT NULL, + `permit_type` ENUM('SINGLE_TRIP', 'DAILY_PASS', 'ANNUAL_HEAVY_HAUL') NOT NULL DEFAULT 'SINGLE_TRIP', + `max_weight_capacity_kg` DECIMAL(10, 2) NOT NULL DEFAULT 25000.00, + `fee_paid` DECIMAL(10, 2) NOT NULL DEFAULT 150.00, + `issued_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NOT NULL, + `status` ENUM('ACTIVE', 'EXPIRED', 'REVOKED') NOT NULL DEFAULT 'ACTIVE', + INDEX `idx_permit_plate` (`vehicle_plate`), + INDEX `idx_permit_applicant` (`applicant_identifier`), + INDEX `idx_permit_faction` (`faction_id`), + INDEX `idx_permit_status` (`status`), + INDEX `idx_permit_expiry` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 2. Create transit permit violation audit log table +CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs` ( + `violation_id` BIGINT AUTO_INCREMENT PRIMARY KEY, + `vehicle_plate` VARCHAR(16) NOT NULL, + `driver_identifier` VARCHAR(64) NULL DEFAULT NULL, + `measured_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `permitted_weight_kg` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `violation_type` ENUM('NO_PERMIT', 'WEIGHT_EXCEEDED', 'PERMIT_EXPIRED', 'BYPASS_UNLICENSED') NOT NULL DEFAULT 'NO_PERMIT', + `assessed_fine` DECIMAL(10, 2) NOT NULL DEFAULT 0.00, + `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_viol_plate` (`vehicle_plate`), + INDEX `idx_viol_driver` (`driver_identifier`), + INDEX `idx_viol_type` (`violation_type`), + INDEX `idx_viol_timestamp` (`timestamp` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 3. Seed municipal permit issuing terminals +INSERT INTO `nyx_commercial_transit_permits` + (`permit_id`, `vehicle_plate`, `applicant_identifier`, `faction_id`, `permit_type`, `max_weight_capacity_kg`, `fee_paid`, `issued_at`, `expires_at`, `status`) +VALUES + ('permit_seed_001', 'HEAVY01', 'CIT_SEED_01', 'corp_elysian_logistics', 'ANNUAL_HEAVY_HAUL', 50000.00, 2500.00, CURRENT_TIMESTAMP, DATE_ADD(CURRENT_TIMESTAMP, INTERVAL 365 DAY), 'ACTIVE') +ON DUPLICATE KEY UPDATE `vehicle_plate` = VALUES(`vehicle_plate`); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/tests/test_commercial_transit_permits_and_overweight_surcharges.py b/tests/test_commercial_transit_permits_and_overweight_surcharges.py new file mode 100644 index 0000000..ec8b851 --- /dev/null +++ b/tests/test_commercial_transit_permits_and_overweight_surcharges.py @@ -0,0 +1,204 @@ +""" +Tests for Commercial Heavy Transit Permits & Overweight Surcharges (Migration 068) +Validates SQL migration 068 parity across module folders and index.json, +manifest declarations, server exports, in-engine Test 63 registration, +and pure Python/Lua deterministic algorithms for permit fee calculations, +surcharges, validity evaluations, and MDT violation classification. +""" + +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", "068_commercial_transit_permits_and_overweight_surcharges.sql" +) +VEHICLES_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "sql", "migrations", "068_commercial_transit_permits_and_overweight_surcharges.sql" +) +LOGISTICS_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_logistics", "sql", "migrations", "068_commercial_transit_permits_and_overweight_surcharges.sql" +) +TREASURY_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_treasury", "sql", "migrations", "068_commercial_transit_permits_and_overweight_surcharges.sql" +) +SERVICES_MIGRATION_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_services", "sql", "migrations", "068_commercial_transit_permits_and_overweight_surcharges.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" +) +SERVER_EXPORTS_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_vehicles", "server", "exports.lua" +) +TESTS_LUA_PATH = os.path.join( + ROOT_DIR, "FiveM", "nyx_core", "server", "tests.lua" +) + + +class TransitPermitsEngine: + BASE_PERMIT_FEES = { + "SINGLE_TRIP": 150.00, + "DAILY_PASS": 450.00, + "ANNUAL_HEAVY_HAUL": 2500.00, + } + + PERMIT_CAPACITIES = { + "SINGLE_TRIP": 25000.0, + "DAILY_PASS": 35000.0, + "ANNUAL_HEAVY_HAUL": 60000.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_permit_fee(cls, permit_type="SINGLE_TRIP", requested_max_weight_kg=25000.0, statutory_tax_rate=1.00): + base_fee = cls.BASE_PERMIT_FEES.get(permit_type, 150.00) + base_cap = cls.PERMIT_CAPACITIES.get(permit_type, 25000.0) + + excess_kg = max(0.0, float(requested_max_weight_kg) - base_cap) + surcharge = excess_kg * 0.05 + + gross = (base_fee + surcharge) * max(0.1, float(statutory_tax_rate)) + return cls.round_to_cent(gross), cls.round_to_cent(surcharge) + + @classmethod + def evaluate_permit_validity(cls, permit_record, measured_weight_kg, current_timestamp_epoch): + measured_weight_kg = float(measured_weight_kg or 0.0) + current_timestamp_epoch = float(current_timestamp_epoch or 0.0) + + if not permit_record or not isinstance(permit_record, dict): + return False, "NO_PERMIT" + + if permit_record.get("status") != "ACTIVE": + return False, "EXPIRED" + + expiry_epoch = float(permit_record.get("expires_at_epoch") or 0.0) + if expiry_epoch > 0 and current_timestamp_epoch >= expiry_epoch: + return False, "EXPIRED" + + permitted_cap_kg = float(permit_record.get("max_weight_capacity_kg") or 25000.0) + if measured_weight_kg > permitted_cap_kg: + return False, "WEIGHT_EXCEEDED" + + return True, "VALID" + + @classmethod + def classify_violation_and_fine(cls, validity_status, measured_weight_kg, permitted_weight_kg, did_bypass): + measured_weight_kg = float(measured_weight_kg or 0.0) + permitted_weight_kg = float(permitted_weight_kg or 15000.0) + + if did_bypass and validity_status != "VALID": + return "BYPASS_UNLICENSED", 1500.00 + + if validity_status == "NO_PERMIT": + return "NO_PERMIT", 750.00 + elif validity_status == "EXPIRED": + return "PERMIT_EXPIRED", 500.00 + elif validity_status == "WEIGHT_EXCEEDED": + excess_kg = max(0.0, measured_weight_kg - permitted_weight_kg) + fine = 500.00 + (excess_kg * 3.00) + return "WEIGHT_EXCEEDED", cls.round_to_cent(fine) + + return "NONE", 0.00 + + +def test_migration_068_files_and_registration(): + """Verify SQL migration 068 exists in all 5 required modules and is registered in index.json.""" + assert os.path.exists(CORE_MIGRATION_PATH), "Core migration 068 must exist" + assert os.path.exists(VEHICLES_MIGRATION_PATH), "Vehicles migration 068 must exist" + assert os.path.exists(LOGISTICS_MIGRATION_PATH), "Logistics migration 068 must exist" + assert os.path.exists(TREASURY_MIGRATION_PATH), "Treasury migration 068 must exist" + assert os.path.exists(SERVICES_MIGRATION_PATH), "Services migration 068 must exist" + + with open(INDEX_JSON_PATH, "r", encoding="utf-8") as f: + registered = json.load(f) + + assert "068_commercial_transit_permits_and_overweight_surcharges.sql" in registered, ( + "068 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_commercial_transit_permits`" in sql + assert "CREATE TABLE IF NOT EXISTS `nyx_permit_violation_logs`" in sql + assert "ANNUAL_HEAVY_HAUL" in sql + assert "BYPASS_UNLICENSED" in sql + + +def test_manifest_and_exports_declarations_068(): + """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/transit_permits.lua" in manifest + assert "server/transit_permits.lua" in manifest + assert "client/transit_permits.lua" in manifest + + with open(SERVER_EXPORTS_PATH, "r", encoding="utf-8") as f: + exports = f.read() + + assert "exports(\"IssueTransitPermit\"" in exports + assert "exports(\"GetActiveTransitPermit\"" in exports + assert "exports(\"LogPermitViolation\"" in exports + assert "exports(\"CalculatePermitFee\"" in exports + assert "exports(\"EvaluatePermitValidity\"" in exports + + +def test_in_engine_test_63_registration(): + """Verify Test 63 (TestCommercialTransitPermitsAndOverweightSurcharges) is registered in tests.lua.""" + with open(TESTS_LUA_PATH, "r", encoding="utf-8") as f: + tests_lua = f.read() + + assert "function Nyx.Tests.TestCommercialTransitPermitsAndOverweightSurcharges()" in tests_lua + assert "local t63 = Nyx.Tests.TestCommercialTransitPermitsAndOverweightSurcharges()" in tests_lua + + +def test_permit_fee_and_surcharge_math(): + """Verify permit fee and heavy-haul surcharge math.""" + base_fee, base_surcharge = TransitPermitsEngine.calculate_permit_fee("SINGLE_TRIP", 25000.0, 1.00) + assert base_fee == 150.00 + assert base_surcharge == 0.00 + + # Requested 35,000 kg for SINGLE_TRIP (base cap 25,000 kg) -> 10,000 kg excess * $0.05 = $500 surcharge -> $150 + $500 = $650.00 + heavy_fee, heavy_surcharge = TransitPermitsEngine.calculate_permit_fee("SINGLE_TRIP", 35000.0, 1.00) + assert heavy_fee == 650.00 + assert heavy_surcharge == 500.00 + + +def test_permit_validity_evaluation(): + """Verify active permit capacity and expiration evaluation logic.""" + now = 1770000000 + mock_permit = { + "status": "ACTIVE", + "max_weight_capacity_kg": 35000.0, + "expires_at_epoch": now + 86400, + } + + is_valid, status = TransitPermitsEngine.evaluate_permit_validity(mock_permit, 30000.0, now) + assert is_valid is True + assert status == "VALID" + + is_valid_exceeded, status_exceeded = TransitPermitsEngine.evaluate_permit_validity(mock_permit, 40000.0, now) + assert is_valid_exceeded is False + assert status_exceeded == "WEIGHT_EXCEEDED" + + +def test_violation_classification_and_fine(): + """Verify violation classification and statutory fine scaling.""" + v_type_no, fine_no = TransitPermitsEngine.classify_violation_and_fine("NO_PERMIT", 30000.0, 25000.0, False) + assert v_type_no == "NO_PERMIT" + assert fine_no == 750.00 + + v_type_bypass, fine_bypass = TransitPermitsEngine.classify_violation_and_fine("NO_PERMIT", 30000.0, 25000.0, True) + assert v_type_bypass == "BYPASS_UNLICENSED" + assert fine_bypass == 1500.00