Cadburry ScriptsCadburry Scripts

Framework, Utils & More

Quality of life scripts for your FiveM server. Performance optimized resources that install in minutes, with free updates and support that actually answers.

bridge/framework/server.lua

lua
local function getPhoneNumber(identifier)
    local result = MySQL.query.await('SELECT phone_number FROM users WHERE identifier = ?', { identifier })
    return result and result[1].phone_number
end

---Get Player Info
---@param playerId string | number | nil
---@return table | nil
function GetPlayerInfo(playerId)
    if not playerId then return nil end
    if GetResourceState('qbx_core') == 'started' then
        local player = (type(playerId) == 'string' and (exports.qbx_core:GetPlayerByCitizenId(playerId) or exports.qbx_core:GetOfflinePlayer(playerId))) or exports.qbx_core:GetPlayer(playerId)
        if not player then assert(player, "Player not found.") return nil end
        return {
            citizenId = player.PlayerData.citizenid,
            fullName = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname,
            phoneNumber = player.PlayerData.charinfo.phone,
            callsign = player.PlayerData.metadata.callsign or 'N/A',
            job = {
                name = player.PlayerData.job.name or 'N/A',
                grade = player.PlayerData.job.grade.level or 1
            },
            addMoney = function(type, amount, reason)
                return player.Functions.AddMoney(type, amount, reason)
            end,
            removeMoney = function(type, amount, reason)
                return player.Functions.RemoveMoney(type, amount, reason)
            end
        }
    elseif GetResourceState('qb-core') == 'started' then
        local QBCore = exports['qb-core']:GetCoreObject()
        local player = (type(playerId) == 'string' and (QBCore.Functions.GetPlayerByCitizenId(playerId) or QBCore.Functions.GetOfflinePlayerByCitizenId(playerId))) or QBCore.Functions.GetPlayer(playerId)
        if not player then assert(player, "Player not found.") return nil end
        return {
            citizenId = player.PlayerData.citizenid,
            fullName = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname,
            phoneNumber = player.PlayerData.charinfo.phone,
            callsign = player.PlayerData.metadata.callsign or 'N/A',
            job = {
                name = player.PlayerData.job.name or 'N/A',
                grade = player.PlayerData.job.grade.level or 1
            },
            addMoney = function(type, amount, reason)
                return player.Functions.AddMoney(type, amount, reason)
            end,
            removeMoney = function(type, amount, reason)
                return player.Functions.RemoveMoney(type, amount, reason)
            end
        }
    elseif GetResourceState('es_extended') == 'started' then
        local ESX = exports.es_extended:getSharedObject()
        local player = type(playerId) == 'string' and ESX.GetPlayerFromIdentifier(playerId) or ESX.GetPlayerFromId(playerId)
        if not player then print("Player not found.") return nil end
        return {
            citizenId = player.identifier,
            fullName = player.name,
            phoneNumber = getPhoneNumber(player.identifier),
            callsign = player.callsign or 'N/A', -- Not sure about this, check and replace this with your callsign function/export
            job = {
                name = player.getJob().name or 'N/A',
                grade = player.getJob().grade or 1
            },
            addMoney = function(type, amount, reason)
                if type == 'cash' then type = 'money' end
                player.addAccountMoney(type, amount, reason)
                return true
            end,
            removeMoney = function(type, amount, reason)
                if type == 'cash' then type = 'money' end
                player.removeAccountMoney(type, amount, reason)
                return true
            end
        }
    end

    return nil
end

---Get Player Info
---@param plateNumber string | number | nil
---@return table | nil
function GetPlayerInfoFromPlate(plateNumber)
    if not plateNumber then assert(plateNumber, "Plate number not found to get player info") return nil end
    if GetResourceState('qb-core') == 'started' or GetResourceState('qbx_core') == 'started' then
        local result = MySQL.single.await([[
            SELECT 
                players.citizenid
            FROM player_vehicles
            LEFT JOIN players ON players.citizenid = player_vehicles.citizenid
            WHERE plate = ? OR fakeplate = ? LIMIT 1
        ]], { plateNumber, plateNumber })
        assert(result, "Player not found by plate number.")
        return GetPlayerInfo(result.citizenid)
    elseif GetResourceState('es_extended') == 'started' then
        local result = MySQL.single.await([[
            SELECT 
                users.identifier
            FROM owned_vehicles
            LEFT JOIN users ON users.identifier = owned_vehicles.identifier
            WHERE plate = ? LIMIT 1
        ]], { plateNumber })
        assert(result, "Player not found by plate number.")
        return GetPlayerInfo(result.identifier)
    end

    return nil
end

---Get Vehicle Name
---@param vehicleId number | integer
---@return string
function GetVehicleName(vehicleId)
    if not vehicleId then return nil, nil end
    local model = GetEntityModel(vehicleId)
    if GetResourceState('qbx_core') == 'started' then
        local vehicle = exports.qbx_core:GetVehiclesByHash(model)
        return (vehicle.brand and (vehicle.brand .. ' ' .. vehicle.name)) or vehicle.name
    elseif GetResourceState('qb-core') == 'started' then
        local QBCore = exports['qb-core']:GetCoreObject()
        if QBCore.Shared.VehicleHashes then
            local vehicle = QBCore.Shared.VehicleHashes[model]
            return (vehicle.brand and (vehicle.brand .. ' ' .. vehicle.name)) or vehicle.name
        else
            return GetVehicleNumberPlateText(vehicleId)
        end
    else
        return GetVehicleNumberPlateText(vehicleId)
    end
end

bridge/inventory/client.lua

lua
---Display metadata for the citation item, only for ox_inventory
CreateThread(function()
    if GetResourceState('ox_inventory') == 'missing' then
        return
    end
    while true do
        Wait(1000)
        if GetResourceState('ox_inventory') == 'started' then
            exports.ox_inventory:displayMetadata({
                fullName = 'Name',
                citizenId = 'Citizen ID',
                fineAmount = 'Fine Amount',
                -- violations = 'Violations',
                -- vehicleModel = 'Vehicle Model',
                -- plateNumber = 'Plate Number',
                -- currentDate = 'Current Date',
                -- streetName = 'Street Name',
                -- officerCitizenId = 'AX12344',
                -- officerName = 'Officer Name',
                -- officerCallsign = 'Officer Callsign',
                -- comments = 'Comments',
                -- evidence = 'Evidence'
            })
            break
        end
    end
end)

bridge/inventory/server.lua

lua
---Can Carry an item
---@param source number | nil
---@param item string
---@param amount number
function CanCarryItem(source, item, amount)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:CanCarryItem(source, item, amount)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:CanAddItem(source, item, amount)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:CanAddItem(source, item, amount)
    else
        assert(false, "No inventory resource found.")
        return false
    end
end

---Get item by slot number
---@param source number | nil
---@param slot number
function GetItemBySlot(source, slot)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:GetSlot(source, slot)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:GetItemBySlot(source, slot)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:GetItemBySlot(source, slot)
    else
        assert(false, "No inventory resource found.")
        return nil
    end
end

---Add item to inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function AddItem(source, item, amount, metadata)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:AddItem(source, item, amount, metadata)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
    else
        assert(false, "No inventory resource found.")
        return false
    end
end

---Remove item from inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function RemoveItem(source, item, amount, metadata)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:AddItem(source, item, amount, metadata)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
    else
        assert(false, "No inventory resource found.")
        return false
    end
end


---Handles Item Usage
CreateThread(function()
    if GetResourceState('qb-inventory') ~= 'missing' then
        local timeout = 10
        while not GetResourceState('qb-inventory') ~= 'started' and timeout > 0 do
            timeout = timeout - 1
            Wait(1000)
        end
        local QBCore = exports['qb-core']:GetCoreObject()
        for _, itemName in pairs(Config.Items) do
            QBCore.Functions.CreateUseableItem(itemName, function(source, item)
                if item.name == Config.Items.show_receipt then
                    exports['cad-citations']:showCitation(source, item.metadata)
                elseif item.name == Config.Items.create_citation then
                    local citation = GetAutomatedInfo(source)
                    exports['cad-citations']:createCitation(source, citation)
                end
            end)
        end
    end

    if GetResourceState('ps-inventory') ~= 'missing' then
        local timeout = 10
        while not GetResourceState('ps-inventory') ~= 'started' and timeout > 0 do
            timeout = timeout - 1
            Wait(1000)
        end
        for _, itemName in pairs(Config.Items) do
            exports['ps-inventory']:CreateUsableItem(itemName, function(source, item)
                if item.name == Config.Items.show_receipt then
                    exports['cad-citations']:showCitation(source, item.metadata)
                elseif item.name == Config.Items.create_citation then
                    local citation = GetAutomatedInfo(source)
                    exports['cad-citations']:createCitation(source, citation)
                end
            end)
        end
    end
end)

---This function is used to handle item usage, specifically for ox_inventory.
---@param event string
---@param item table<any>
---@param inv table<any>
---@param slot number
---@param data table<any>
exports('useItem', function(event, item, inv, slot, data)
    if event == 'usingItem' then
        if item.name == Config.Items.show_receipt then
            local _item = GetItemBySlot(inv.id, slot)
            assert(_item, "Item data not found for slot: " .. tostring(slot))
            exports['cad-citations']:showCitation(inv.id, _item.metadata)
            return false
        end

        if item.name == Config.Items.create_citation then
            local citation = GetAutomatedInfo(inv.id)
            exports['cad-citations']:createCitation(inv.id, citation)
            return false
        end

        return
    end
end)

bridge/utils/client.lua

lua
---Take Photo
---@param data { method: string, authKey: string } Upload Config
---@param cb function Callback function returning the captured image url
function TakeScreenPhoto(data, cb)
    if data.method == 'discord' then
        exports['screenshot-basic']:requestScreenshotUpload(data.authKey, 'files[]', function(response)
            local image = json.decode(response)
            if not data or not image then return cb('') end
            cb((image.attachments[1] and image.attachments[1]?.proxy_url) or 'Invalid Url')
        end)
    elseif data.method == 'fivemanage' then
        exports['screenshot-basic']:requestScreenshotUpload('https://fmapi.net/api/v2/image', 'file', {
            headers = { Authorization = data.authKey },
            encoding = 'png'
        }, function(response)
            local resp = json.decode(response)
            cb((resp and resp?.data and resp.data?.url) or '')
        end)
    elseif data.method == 'fivemerr' then
        exports['screenshot-basic']:requestScreenshotUpload('https://api.fivemerr.com/v1/media/images', 'file', {
            headers = { Authorization = data.authKey },
            encoding = 'png'
        }, function(response)
            local resp = json.decode(response)
            cb((resp and resp.url) or '')
        end)
    end
end

---Notification
---@param data { title: string, description: string, type?: string, duration?: number }
RegisterNetEvent('cad-citations:notify', function(data)
    if not data then return end
    lib.notify({
        title = data.title,
        description = data.description,
        type = data.type,
        duration = 5000
    })
end)

---On use event listener
---@param type string 'new' | 'existing'
RegisterNetEvent('cad-citations:onUse', function(type)
    local emote = (type == 'existing') and 'clipboard' or 'notepad'
    if GetResourceState('scully_emotemenu') == 'started' then
        exports.scully_emotemenu:PlayEmoteByCommand(emote)
    elseif GetResourceState('rpemotes') == 'started' then
        exports.rpemotes:EmoteCommandStart(emote)
    elseif GetResourceState('dpemotes') == 'started' then
        ExecuteCommand('e '..emote)
    end
end)

---On close event listener
---@param type string 'new' | 'existing'
RegisterNetEvent('cad-citations:onClose', function(type)
    if GetResourceState('scully_emotemenu') == 'started' then
        exports.scully_emotemenu:cancelEmote()
    elseif GetResourceState('rpemotes') == 'started' then
        exports.rpemotes:EmoteCancel(true)
    elseif GetResourceState('dpemotes') == 'started' then
        ExecuteCommand('e c')
    end
end)

---Interactions (QbTarget, OxTarget, OxLibTextUI, etc)
CreateThread(function()
    local function createBlip(data)
        local blip = AddBlipForCoord(data.coords.x, data.coords.y, data.coords.z)
		SetBlipAsShortRange(blip, true)
		SetBlipSprite(blip, data.blip.sprite)
		SetBlipColour(blip, data.blip.color)
		SetBlipScale(blip, data.blip.scale)
		SetBlipDisplay(blip, 6)
		BeginTextCommandSetBlipName('STRING')
		AddTextComponentSubstringPlayerName(data.blip.label)
		EndTextCommandSetBlipName(blip)
    end
    for name, data in pairs(Config.CitationLocations) do
        -- Creates blip for the location
        if data.blip and data.blip?.enabled then createBlip(data) end
        if (Config.Target == 'auto' and GetResourceState('ox_target') == 'started') or Config.Target == 'ox_target' then
            -- Ox Target
            exports.ox_target:addSphereZone({
                name = name,
                coords = data.coords,
                radius = data.radius or 1.0,
                debug = Config.Debug,
                drawSprite = true,
                options = {
                    {
                        label = data.label or 'Public Citations',
                        icon = data.icon or 'fa-solid fa-file-invoice',
                        distance = data.distance or data.radius,
                        onSelect = function()
                            exports['cad-citations']:showDashboard()
                        end
                    }
                }
            })
        elseif (Config.Target == 'auto' and GetResourceState('qb-target') == 'started') or Config.Target == 'qb-target' then
            -- QB Target
            exports['qb-target']:AddCircleZone(name, data.coords, data.radius, {
                name = name,
                debugPoly = Config.Debug
            }, {
                options = {
                    {
                        num = 1,
                        label = data.label or 'Public Citations',
                        icon = data.icon or 'fa-solid fa-file-invoice',
                        action = function()
                            exports['cad-citations']:showDashboard()
                        end
                    }
                },
                distance = data.distance or data.radius,
            })
        else
            -- Ox Lib TextUI
            local point = lib.points.new({
                coords = data.coords,
                distance = data.radius,
            })

            function point:onEnter()
                lib.showTextUI(('[E] %s'):format(data.label), {
                    -- position = "top-center",
                    icon = data.icon,
                })
            end

            function point:onExit()
                lib.hideTextUI()
            end

            function point:nearby()
                DrawMarker(20, self.coords.x, self.coords.y, self.coords.z + 0.5, 0.0, 0.0, 0.0, 0.0, 180.0, 0.0, 0.3, 0.3, 0.2, 100, 100, 200, 100, true, true, 2, false, nil, nil, false)

                if self.currentDistance < (data.distance or data.radius) and IsControlJustReleased(0, 38) then
                    exports['cad-citations']:showDashboard()
                end
            end
        end
    end
end)

RegisterCommand('reset_citation', function()
    exports['cad-citations']:resetUI()
end, false)

bridge/utils/server.lua

lua
---Get Automated Data For: Player, Vehicle, Officer
---@param source string | integer
---@return table
function GetAutomatedInfo(source)
    local data = {}
    local ped = GetPlayerPed(source)
    local coords = GetEntityCoords(ped)
    local playerId = lib.getClosestPlayer(coords, 5.0, false)
    local vehicleId = lib.getClosestVehicle(coords, 5.0, false)

    if vehicleId and Config.AutoFetchData.vehicle then
        data.vehicleModel, data.plateNumber = GetVehicleName(vehicleId), GetVehicleNumberPlateText(vehicleId)
    end

    if playerId and (playerId ~= source) and Config.AutoFetchData.player then
        local player = GetPlayerInfo(playerId)
        data.citizenId = player?.citizenId
        data.fullName = player?.fullName
    end

    if source and Config.AutoFetchData.officer then
        local player = GetPlayerInfo(source)
        data.officerCitizenId = player?.citizenId
        data.officerName = player?.fullName
        data.officerCallsign = player?.callsign
    end

    return data
end

---Send Phone Mail
---@param data any
function SendMail(data)
    if GetResourceState('lb-phone') == 'started' then
        local result = MySQL.query.await("SELECT lia.username FROM phone_phones pp LEFT JOIN phone_logged_in_accounts lia ON lia.phone_number = pp.phone_number AND lia.app = 'Mail' where pp.owner_id = ?", { data.citizenId })
		if not result or not result[1] then return false end
        exports["lb-phone"]:SendMail({
            to = result and result[1].username,
            sender = 'Traffic Department',
            subject = data.subject,
            message = data.message
        })
    elseif GetResourceState('yseries') == 'started' then
        local phoneNumber = exports.yseries:GetPhoneNumberByIdentifier(data.citizenId)
        exports.yseries:SendMail({
            title = data.subject,
            sender = 'trafficdept@yflip.com',
            senderDisplayName = 'Traffic Department',
            content = data.message
        }, 'phoneNumber', phoneNumber)
    elseif GetResourceState('snappy-phone') == 'started' then
        exports['snappy-phone']:sendCustomMail({
            sender = 'Traffic Department',
            subject = data.subject,
            body = data.message,
            recipients = {
                { identifier = data.citizenId }
            }
        })
    else
        assert(false, "No phone resource found.")
        return false
    end
end

---Create Warrant/BOLO
---@param data ICitation
function CreateWarrantBOLO(data)
end

---Add Fine Money to Account
---@param player table
---@param data ICitation
function AddMoneyToAccount(player, data)
    if not Config.CitationAccountEnabled then return end
    if not data?.officerJob or not data?.fineAmount then return end
    local accountName, amount = data?.officerJob, data?.fineAmount
    local commission = Config.CitationCommisions.commisions and Config.CitationCommisions.commisions[data?.officerJob]
    if commission and commission > 0 then amount = amount - (amount * (commission/100)) end
    local reason = 'Citation payment by ' .. player?.fullName .. ' for ' .. data?.plateNumber
    if GetResourceState('snipe-banking') == 'started' then
        exports['snipe-banking']:AddMoneyToAccount(accountName, amount)
    elseif GetResourceState('Renewed-Banking') == 'started' then
        return exports['Renewed-Banking']:addAccountMoney(accountName, amount)
    elseif GetResourceState('qb-banking') == 'started' then
        local status, business = pcall(function() return exports['qb-banking']:business(accountName, Config.CitationAccount.accountId) end)
        if status and business then
            return business.addBalance(amount, reason)
        else
            return exports['qb-banking']:AddMoney(accountName, amount, reason) or false
        end
        return business or false
    elseif GetResourceState('okokBanking') == 'started' then
        return exports['okokBanking']:AddMoney(accountName, amount)
    elseif GetResourceState('tgg-banking') == 'started' then
        return exports['tgg-banking']:AddSocietyMoney(accountName, amount)
    elseif GetResourceState('fd_banking') == 'started' then
        return exports['tgg-banking']:AddMoney(accountName, amount, reason)
    elseif GetResourceState('kartik-banking') == 'started' then
        return exports['kartik-banking']:AddAccountMoney(accountName, amount, reason)
    else
        assert(false, "No banking resource found.")
        return false
    end
end

---Provide Commision to officer
---@param player table
---@param data ICitation
function AddCommision(player, data)
    if not Config.CitationCommisions or not Config.CitationCommisions?.enabled then return end
    local commission = Config.CitationCommisions.commisions[data?.officerJob]
    local fineAmount = data?.fineAmount and tonumber(data?.fineAmount) or 0
    if commission and fineAmount > 0 then
        local officer = GetPlayerInfo(data?.officerCitizenId)
        if not officer then return end
        local commissionAmount = lib.math.round(fineAmount*(commission/100), 2)
        local reason = 'Citation commission from ' .. player?.fullName .. ' for ' .. data?.plateNumber
        officer.addMoney(Config.CitationPaymentMethod, commissionAmount, reason)
    end
end

---Logger wrapper
---@param source number | nil
---@param name string
---@param message string
---@param data table<any>
function Logger(source, name, message, data)
    local webhook = ""
    local title = ('[%d] - %s - %s'):format(source, GetPlayerName(source), name)
    local headers = { ['Content-Type'] = 'application/json' }
    local _data = json.encode({
        username = "Cad Citations",
        embeds = {
            {
                title = title,
                color = 10650848,
                author = {
                    name = "cadburry",
                },
                description = "\n** Message :** "..message.." \n **Data : ** "..json.encode(data).." ",
                footer = {
                    text = os.date('%c'),
                },
            }
        }
    })
    PerformHttpRequest(webhook, function() end, 'POST', _data, headers)
    lib.logger(source, name, message, data)
end


---On Citation Event Triggered (Listener)
---@param event string "createCitation" | "updateCitation" | "payCitation" | "deleteCitation" | "warrantBolo"
---@param eventData IEventData
function OnEventAction(event, eventData)
    if event == 'createCitation' then
        if not eventData?.data?.citizenId or not eventData?.data?.plateNumber then return end
        local player = GetPlayerInfoFromPlate(eventData?.data?.plateNumber)
        if not player then return end
        SendMail({
            citizenId = player.citizenId,
            phoneNumber = player.phoneNumber,
            subject = 'New Citation',
            message = 'You have been issued citation on '.. eventData.data.vehicleModel ..' ('.. eventData.data.plateNumber .. ') for $'.. eventData.data.fineAmount
        })
    end

    if event == 'warrantBolo' then
        if not eventData?.citizenId or not eventData?.data then return end
        CreateWarrantBOLO(eventData?.data)
    end

    if event == 'payCitation' then
        if not eventData?.citizenId or not eventData?.data then return end
        local player = GetPlayerInfo(eventData?.citizenId)
        if not player then return end
        AddMoneyToAccount(player, eventData?.data)
        AddCommision(player, eventData?.data)
    end
end
table<any>
function AddItem(source, item, amount, metadata)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:AddItem(source, item, amount, metadata)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('lj-inventory') == 'started' then
        return exports['lj-inventory']:AddItem(source, item, amount, nil, metadata)
    else
        assert(false, "No inventory resource found.")
        return false
    end
end

---Remove item from inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function RemoveItem(source, item, amount, metadata)
    if GetResourceState('ox_inventory') == 'started' then
        return exports.ox_inventory:AddItem(source, item, amount, metadata)
    elseif GetResourceState('qb-inventory') == 'started' then
        return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('ps-inventory') == 'started' then
        return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
    elseif GetResourceState('lj-inventory') == 'started' then
        return exports['lj-inventory']:AddItem(source, item, amount, nil, metadata)
    else
        assert(false, "No inventory resource found.")
        return false
    end
end

---Get Automated Data For: Player, Vehicle, Officer
---@param source string | integer
---@return table
function GetAutomatedInfo(source)
    local data = {}
    local ped = GetPlayerPed(source)
    local coords = GetEntityCoords(ped)
    local playerId = lib.getClosestPlayer(coords, 5.0, false)
    local vehicleId = lib.getClosestVehicle(coords, 5.0, false)

    if vehicleId and Config.AutoFetchData.vehicle then
        data.vehicleModel, data.plateNumber = GetVehicleName(vehicleId), GetVehicleNumberPlateText(vehicleId)
    end

    if playerId and (playerId ~= source) and Config.AutoFetchData.player then
        local player = GetPlayerInfo(playerId)
        data.citizenId = player?.citizenId
        data.fullName = player?.fullName
    end

    if source and Config.AutoFetchData.officer then
        local player = GetPlayerInfo(source)
        data.officerCitizenId = player?.citizenId
        data.officerName = player?.fullName
        data.officerCallsign = player?.callsign
    end

    return data
end

---Send Phone Mail
---@param data any
function SendMail(data)
    if GetResourceState('lb-phone') == 'started' then
        local email = exports["lb-phone"]:GetEmailAddress(data.phoneNumber)
        exports["lb-phone"]:SendMail({
            to = email,
            sender = 'Traffic Department',
            subject = data.subject,
            message = data.message
        })
    elseif GetResourceState('yseries') == 'started' then
        local phoneNumber = exports.yseries:GetPhoneNumberByIdentifier(data.citizenId)
        exports.yseries:SendMail({
            title = data.subject,
            sender = 'trafficdept@yflip.com',
            senderDisplayName = 'Traffic Department',
            content = data.message
        }, 'phoneNumber', phoneNumber)
    elseif GetResourceState('snappy-phone') == 'started' then
        exports['snappy-phone']:sendCustomMail({
            sender = 'Traffic Department',
            subject = data.subject,
            body = data.message,
            recipients = {
                { identifier = data.citizenId }
            }
        })
    else
        assert(false, "No phone resource found.")
        return false
    end
end

---Create Warrant/BOLO
---@param data ICitation
function CreateWarrantBOLO(data)
end

---Add Fine Money to Account
---@param player table
---@param data ICitation
function AddMoneyToAccount(player, data)
    if not Config.CitationAccountEnabled then return end
    if not data?.officerJob or not data?.fineAmount then return end
    local accountName, amount = data?.officerJob, data?.fineAmount
    local reason = 'Citation payment by ' .. player?.fullName .. ' for ' .. data?.plateNumber
    if GetResourceState('snipe-banking') == 'started' then
        exports['snipe-banking']:AddMoneyToAccount(accountName, amount)
    elseif GetResourceState('Renewed-Banking') == 'started' then
        return exports['Renewed-Banking']:addAccountMoney(accountName, amount)
    elseif GetResourceState('qb-banking') == 'started' then
        local status, business = pcall(function() return exports['qb-banking']:business(accountName, Config.CitationAccount.accountId) end)
        if status and business then
            return business.addBalance(amount, reason)
        else
            return exports['qb-banking']:AddMoney(accountName, amount, reason) or false
        end
        return business or false
    elseif GetResourceState('okokBanking') == 'started' then
        return exports['okokBanking']:AddMoney(accountName, amount)
    elseif GetResourceState('tgg-banking') == 'started' then
        return exports['tgg-banking']:AddSocietyMoney(accountName, amount)
    elseif GetResourceState('fd_banking') == 'started' then
        return exports['tgg-banking']:AddMoney(accountName, amount, reason)
    elseif GetResourceState('kartik-banking') == 'started' then
        return exports['kartik-banking']:AddAccountMoney(accountName, amount, reason)
    else
        assert(false, "No banking resource found.")
        return false
    end
end

---Provide Commision to officer
---@param player table
---@param data ICitation
function AddCommision(player, data)
    if not Config.CitationCommisions or not Config.CitationCommisions?.enabled then return end
    local commission = Config.CitationCommisions.commisions[data?.officerJob]
    local fineAmount = data?.fineAmount and tonumber(data?.fineAmount) or 0
    if commission and fineAmount > 0 then
        local officer = GetPlayerInfo(data?.officerCitizenId)
        if not officer then return end
        local commissionAmount = lib.math.round(fineAmount*commission, 2)
        local reason = 'Citation commission from ' .. player?.fullName .. ' for ' .. data?.plateNumber
        officer.addMoney(Config.CitationPaymentMethod, commissionAmount, reason)
    end
end

---Logger wrapper
---@param source number | nil
---@param name string
---@param message string
---@param data table<any>
function Logger(source, name, message, data)
    local webhook = ""
    local title = ('[%d] - %s - %s'):format(source, GetPlayerName(source), name)
    local headers = { ['Content-Type'] = 'application/json' }
    local _data = json.encode({
        username = "Cad Citations",
        embeds = {
            {
                title = title,
                color = 10650848,
                author = {
                    name = "cadburry",
                },
                description = "\n** Message :** "..message.." \n **Data : ** "..json.encode(data).." ",
                footer = {
                    text = os.date('%c'),
                },
            }
        }
    })
    PerformHttpRequest(webhook, function() end, 'POST', _data, headers)
    lib.logger(source, name, message, data)
end

---This function is used to handle item usage, specifically for ox_inventory.
---@param event string
---@param item table<any>
---@param inv table<any>
---@param slot number
---@param data table<any>
exports('useItem', function(event, item, inv, slot, data)
    if event == 'usingItem' then
        if item.name == Config.Items.show_receipt then
            local _item = GetItemBySlot(inv.id, slot)
            assert(_item, "Item not found in inventory: " .. tostring(slot))
            exports['cad-citations']:showCitation(inv.id, _item.metadata)
            return false
        end

        if item.name == Config.Items.create_citation then
            local citation = GetAutomatedInfo(inv.id)
            exports['cad-citations']:createCitation(inv.id, citation)
            return false
        end

        return
    end
end)

---On Citation Event Triggered (Listener)
---@param event string "createCitation" | "updateCitation" | "payCitation" | "deleteCitation" | "warrantBolo"
---@param eventData IEventData
function OnEventAction(event, eventData)
    if event == 'createCitation' then
        if not eventData?.data?.citizenId or not eventData?.data?.plateNumber then return end
        local player = GetPlayerInfoFromPlate(eventData?.data?.plateNumber)
        if not player then return end
        SendMail({
            citizenId = player.citizenId,
            phoneNumber = player.phoneNumber,
            subject = 'New Citation',
            message = 'You have been issued citation on '.. eventData.data.vehicleModel ..' ('.. eventData.data.plateNumber .. ') for $'.. eventData.data.fineAmount
        })
    end

    if event == 'warrantBolo' then
        if not eventData?.citizenId or not eventData?.data then return end
        CreateWarrantBOLO(eventData?.data)
    end

    if event == 'payCitation' then
        if not eventData?.citizenId or not eventData?.data then return end
        local player = GetPlayerInfo(eventData?.citizenId)
        if not player then return end
        AddMoneyToAccount(player, eventData?.data)
        AddCommision(player, eventData?.data)
    end
end
Last updated Cadburry Scripts