Skip to main content
return {

    -- Use only if needed, directed by support or know what you're doing
    -- Notice: enabling debug features will significantly increase resmon
    -- And should always be disabled in production
    debug = false,

    -- Set your interaction system below
    -- Available options are: 'auto', 'ox_target', 'qb-target', 'interact' & 'custom'
    -- 'auto' will automatically detect and use the available interaction system
    -- 'custom' needs to be added to client/utils/interact.lua
    interact = 'auto',

    -- Set your notification system below
    -- Available options are: 'lation_ui', 'ox_lib', 'esx', 'qb', 'okok', 'sd-notify', 'wasabi_notify' & 'custom'
    -- 'custom' needs to be added to client/utils/notify.lua
    notify = 'ox_lib',

    -- Do you want to be notified via server console if an update is available?
    -- True if yes, false if no
    version = true,

    -- Set your society banking system below
    -- Available options are: 'auto', 'qb-banking', 'qb-management', 'esx_addonaccount', 'Renewed-Banking',
    -- 'okokBanking', 'fd_banking', 'tgg-banking' or 'custom'
    -- 'auto' will automatically detect based on your framework and available banking resources
    -- 'custom' needs to be added to server/utils/banking.lua
    banking = 'auto',
}
Framework, Inventory = nil, nil
PlayerLoaded = false

-- Handle player login
function OnPlayerLoaded()
    PlayerLoaded = true
    TriggerEvent('lation_shops:playerLoaded')
end

-- Handle player logout
function OnPlayerLogout()
    PlayerLoaded = false
    TriggerEvent('lation_shops:playerDropped')
end

-- Initialize framework
local function InitFramework()
    if GetResourceState('es_extended') == 'started' then
        ESX = exports['es_extended']:getSharedObject()
        Framework = 'esx'

        RegisterNetEvent('esx:playerLoaded', function() OnPlayerLoaded() end)
        RegisterNetEvent('esx:onPlayerLogout', function() OnPlayerLogout() end)
    elseif GetResourceState('qbx_core') == 'started' then
        Framework = 'qbx'

        AddEventHandler('QBCore:Client:OnPlayerLoaded', function() OnPlayerLoaded() end)
        RegisterNetEvent('qbx_core:client:playerLoggedOut', function() OnPlayerLogout() end)
    elseif GetResourceState('qb-core') == 'started' then
        QBCore = exports['qb-core']:GetCoreObject()
        Framework = 'qb'

        RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() OnPlayerLoaded() end)
        RegisterNetEvent('QBCore:Client:OnPlayerUnload', function() OnPlayerLogout() end)
    elseif GetResourceState('ox_core') == 'started' then
        Ox = require '@ox_core.lib.init'
        Framework = 'ox'

        AddEventHandler('ox:playerLoaded', function() OnPlayerLoaded() end)
        AddEventHandler('ox:playerLogout', function() OnPlayerLogout() end)
    else
        -- Add custom framework here
    end

    AddEventHandler('onResourceStart', function(resourceName)
        if GetCurrentResourceName() == resourceName then
            OnPlayerLoaded()
        end
    end)

    AddEventHandler('onResourceStop', function(resourceName)
        if GetCurrentResourceName() == resourceName then
            OnPlayerLogout()
        end
    end)
end

-- Initialize inventory
local function InitInventory()
    if GetResourceState('ox_inventory') == 'started' then
        Inventory = 'ox_inventory'
    elseif GetResourceState('qb-inventory') == 'started' then
        Inventory = 'qb-inventory'
    elseif GetResourceState('qs-inventory') == 'started' then
        Inventory = 'qs-inventory'
    elseif GetResourceState('ps-inventory') == 'started' then
        Inventory = 'ps-inventory'
    elseif GetResourceState('origen_inventory') == 'started' then
        Inventory = 'origen_inventory'
    elseif GetResourceState('codem-inventory') == 'started' then
        Inventory = 'codem-inventory'
    elseif GetResourceState('core_inventory') == 'started' then
        Inventory = 'core_inventory'
    elseif GetResourceState('tgiann-inventory') == 'started' then
        Inventory = 'tgiann-inventory'
    elseif GetResourceState('jaksam_inventory') == 'started' then
        Inventory = 'jaksam_inventory'
    else
        -- Add custom inventory here
    end
end

-- Returns player data
function GetPlayerData()
    if Framework == 'esx' then
        return ESX.GetPlayerData()
    elseif Framework == 'qb' then
        return QBCore.Functions.GetPlayerData()
    elseif Framework == 'qbx' then
        return exports.qbx_core:GetPlayerData()
    elseif Framework == 'ox' then
        return Ox.GetPlayer()
    else
        -- Add custom framework here
    end
end

-- Returns player licenses
--- @param license string|nil Specific licence to get
function GetPlayerLicense(license)
    local player = GetPlayerData()
    if not player then return end
    if Framework == 'esx' then
        return lib.callback.await('lation_shops:getPlayerLicense', false, license)
    elseif Framework == 'qb' then
        return player.metadata.licences[license] or false
    elseif Framework == 'qbx' then
        return player.metadata.licences[license] or false
    elseif Framework == 'ox' then
        -- TODO
    else
        -- Add custom framework here
    end
end

-- Returns player job
function GetPlayerJob()
    local player = GetPlayerData()
    if not player then return {} end

    if Framework == 'esx' then
        return {
            name = player.job.name,
            label = player.job.label,
            grade = player.job.grade
        }
    elseif Framework == 'qb' then
        return {
            name = player.job.name,
            label = player.job.label,
            grade = player.job.grade.level
        }
    elseif Framework == 'qbx' then
        return {
            name = player.job.name,
            label = player.job.label,
            grade = player.job.grade.level
        }
    else
        -- Add custom framework here
    end

    return {}
end

-- Return all jobs
function GetAllJobs()
    if Framework == 'esx' then
        return lib.callback.await('lation_shops:getAllJobs', false)
    elseif Framework == 'qb' then
        return QBCore.Shared.Jobs
    elseif Framework == 'qbx' then
        return exports.qbx_core:GetJobs()
    elseif Framework == 'ox' then
        return Ox.GetGroupsByType('job')
    else
        -- Add custom framework here
    end
    return {}
end

-- Sanitize item data for certain inventories
local function sanitize(items)
    local sanitized = {}
    for itemName, itemData in pairs(items or {}) do
        if type(itemData) == 'table' then
            sanitized[itemName] = {}
            for key, value in pairs(itemData) do
                if type(key) == 'string' and not tonumber(key) and type(value) ~= 'function' then
                    if (key == 'label' or key == 'description') and type(value) == 'table' then
                        sanitized[itemName][key] = select(2, next(value)) or "INVALID"
                    elseif type(value) == 'table' then
                        local clean = {}
                        for k, v in pairs(value) do
                            if type(v) ~= 'function' then clean[k] = v end
                        end
                        if next(clean) then sanitized[itemName][key] = clean end
                    else
                        sanitized[itemName][key] = value
                    end
                end
            end
        end
    end
    return sanitized
end

-- Return data for item and image path
function GetAllItems()
    local items
    if Inventory then
        if Inventory == 'ox_inventory' then
            items = exports[Inventory]:Items()
        elseif Inventory == 'qb-inventory' then
            items = QBCore.Shared.Items
        elseif Inventory == 'ps-inventory' then
            items = QBCore.Shared.Items
        elseif Inventory == 'qs-inventory' then
            items = exports[Inventory]:GetItemList()
        elseif Inventory == 'origen_inventory' then
            items = exports[Inventory]:GetItems()
        elseif Inventory == 'codem-inventory' then
            items = exports[Inventory]:GetItemList()
        elseif Inventory == 'core_inventory' then
            -- No available client-side export to get item list
            if Framework == 'qb' then
                items = QBCore.Shared.Items
            else
                print('^1[ERROR]: An issue has occured, please contact support at https://discord.gg/9EbY4nM5uu^0')
                return nil
            end
        elseif Inventory == 'tgiann-inventory' then
            items = exports[Inventory]:GetItemList()
        elseif Inventory == 'jaksam_inventory' then
            items = exports[Inventory]:getStaticItemsList()
        else
            -- Add custom inventory here
            return nil
        end
    else
        if Framework == 'esx' then
            -- Unlikely to need anything here but.. just in case..
            print('^1[ERROR]: An error has occured with lation_shops - please contact support^0')
            return nil
        elseif Framework == 'qb' then
            items = QBCore.Shared.Items
        elseif Framework == 'qbx' then
            -- Unlikely to need anything here but.. just in case..
            print('^1[ERROR]: Are you really not using ox_inventory? Contact support please lul.^0')
            return nil
        end
    end
    return sanitize(items)
end

-- Return image path for inventory items
function GetImagePath()
    if Inventory then
        if Inventory == 'ox_inventory' then
            return 'nui://ox_inventory/web/images/'
        elseif Inventory == 'qb-inventory' then
            return 'nui://qb-inventory/html/images/'
        elseif Inventory == 'ps-inventory' then
            return 'nui://ps-inventory/html/images/'
        elseif Inventory == 'qs-inventory' then
            return 'nui://qs-inventory/html/images/'
        elseif Inventory == 'origen_inventory' then
            return 'nui://origen_inventory/html/images/'
        elseif Inventory == 'codem-inventory' then
            return 'nui://codem-inventory/html/itemimages/'
        elseif Inventory == 'core_inventory' then
            return 'nui://core_inventory/html/images/'
        elseif Inventory == 'tgiann-inventory' then
            return 'nui://inventory_images/images/'
        elseif Inventory == 'jaksam_inventory' then
            return 'nui://jaksam_inventory/_images/'
        else
            -- Add custom inventory here
        end
    end
end

-- Check if player has item
--- @param item string
--- @param amount number
--- @return boolean
function HasItem(item, amount)
    if not item or not amount then return false end
    if Inventory then
        if Inventory == 'ox_inventory' then
            return exports[Inventory]:Search('count', item) >= amount
        elseif Inventory == 'core_inventory' then
            return exports[Inventory]:hasItem(item, amount)
        elseif Inventory == 'jaksam_inventory' then
            return exports[Inventory]:getTotalItemAmount(item) >= amount
        else
            return exports[Inventory]:HasItem(item, amount)
        end
    else
        local playerData = GetPlayerData()
        if not playerData then return false end
        local inventory = Framework == 'esx' and playerData.inventory or playerData.items
        if not inventory then return false end
        for _, itemData in pairs(inventory) do
            if itemData and itemData.name == item then
                local count = itemData.amount or itemData.count or 0
                if count >= amount then
                    return true
                end
            end
        end
        return false
    end
end

-- Disables access to open/view inventory
function DisableInventory()
    if Inventory == 'ox_inventory' then
        LocalPlayer.state.invBusy = true
    elseif Inventory == 'qb-inventory' then
        LocalPlayer.state.inv_busy = true
    elseif Inventory == 'qs-inventory' then
        exports[Inventory]:setInventoryDisabled(true)
    elseif Inventory == 'core_inventory' then
        exports[Inventory]:lockInventory()
    elseif Inventory == 'tgiann-inventory' then
        exports[Inventory]:SetInventoryActive(false)
    else
        -- Add custom inventory here
    end
end

-- Enables access to open/view inventory
function EnableInventory()
    if Inventory == 'ox_inventory' then
        LocalPlayer.state.invBusy = false
    elseif Inventory == 'qb-inventory' then
        LocalPlayer.state.inv_busy = false
    elseif Inventory == 'qs-inventory' then
        exports[Inventory]:setInventoryDisabled(false)
    elseif Inventory == 'core_inventory' then
        exports[Inventory]:unlockInventory()
    elseif Inventory == 'tgiann-inventory' then
        exports[Inventory]:SetInventoryActive(true)
    else
        -- Add custom inventory here
    end
end

InitFramework()
InitInventory()
local setup = require 'config.setup'

-- Auto-detect interaction system
local function DetectInteractSystem()
    if GetResourceState('ox_target') == 'started' then
        return 'ox_target'
    elseif GetResourceState('qb-target') == 'started' then
        return 'qb-target'
    elseif GetResourceState('interact') == 'started' then
        return 'interact'
    else
        print('^1[ERROR]: No interaction system was auto-detected.^0')
        return nil
    end
end

-- Get the interaction system to use
local interact = setup.interact == 'auto' and DetectInteractSystem() or setup.interact

-- Add interaction
--- @param data table
local function AddInteraction(data)
    if interact == 'ox_target' then
        exports.ox_target:addSphereZone(data)
    elseif interact == 'qb-target' then
        exports['qb-target']:AddCircleZone(data.name, data.coords, data.radius, {
            name = data.name,
            debugPoly = setup.debug}, {
            options = data.options,
            distance = 2,
        })
    elseif interact == 'interact' then
        exports.interact:AddInteraction({
            coords = data.coords,
            interactDst = 2.0,
            id = data.name,
            options = data.options
        })
    elseif interact == 'custom' then
        -- Add support for a custom target system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

-- Add entity interaction
--- @param entity number Entity number
--- @param data table Options table
local function AddEntityInteraction(entity, data)
    if interact == 'ox_target' then
        exports.ox_target:addLocalEntity(entity, data)
    elseif interact == 'qb-target' then
        exports['qb-target']:AddTargetEntity(entity, {options = data, distance = 1.75})
    elseif interact == 'interact' then
        exports.interact:AddLocalEntityInteraction({
            entity = entity,
            interactDst = 1.75,
            options = data
        })
    elseif interact == 'custom' then
        -- Add support for a custom interaction system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

-- Add model interaction
--- @param model string Model
--- @param data table Options table
local function AddModelInteraction(model, data)
    if interact == 'ox_target' then
        exports.ox_target:addModel(model, data)
    elseif interact == 'qb-target' then
        exports['qb-target']:AddTargetModel(model, {options = data, distance = 1.75})
    elseif interact == 'interact' then
        exports.interact:AddModelInteraction({
            model = model,
            id = model,
            interactDst = 1.75,
            options = data
        })
    elseif interact == 'custom' then
        -- Add support for a custom interaction system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

-- Remove interaction
--- @param name string
local function RemoveInteraction(name)
    if interact == 'ox_target' then
        exports.ox_target:removeZone(name)
    elseif interact == 'qb-target' then
        exports['qb-target']:RemoveZone(name)
    elseif interact == 'interact' then
        exports.interact:RemoveInteraction(name)
    elseif interact == 'custom' then
        -- Add support for a custom target system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

-- Remove entity interaction
--- @param entity any|number
--- @param data table|string
local function RemoveEntityInteraction(entity, data)
    if interact == 'ox_target' then
        exports.ox_target:removeLocalEntity(entity, data)
    elseif interact == 'qb-target' then
        exports['qb-target']:RemoveTargetEntity(entity, data)
    elseif interact == 'interact' then
        exports.interact:RemoveLocalEntityInteraction(entity, data)
    elseif interact == 'custom' then
        -- Add support for a custom interaction system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

-- Remove model interaction
--- @param model string Model
local function RemoveModelInteraction(model)
    if interact == 'ox_target' then
        exports.ox_target:removeModel(model, nil)
    elseif interact == 'qb-target' then
        exports['qb-target']:RemoveTargetModel(model, nil)
    elseif interact == 'interact' then
        exports.interact:RemoveModelInteraction(model, model)
    elseif interact == 'custom' then
        -- Add support for a custom interaction system here
    else
        print('^1[ERROR]: No interaction system defined in the config/setup.lua file^0')
    end
end

return {
    AddInteraction = AddInteraction,
    AddEntityInteraction = AddEntityInteraction,
    AddModelInteraction = AddModelInteraction,
    RemoveInteraction = RemoveInteraction,
    RemoveEntityInteraction = RemoveEntityInteraction,
    RemoveModelInteraction = RemoveModelInteraction
}
local setup = require 'config.setup'

-- Auto-detect banking system
local function DetectBankingSystem()
    if GetResourceState('okokBanking') == 'started' then
        return 'okokBanking'
    elseif GetResourceState('Renewed-Banking') == 'started' then
        return 'Renewed-Banking'
    elseif GetResourceState('fd_banking') == 'started' then
        return 'fd_banking'
    elseif GetResourceState('tgg-banking') == 'started' then
        return 'tgg-banking'
    elseif GetResourceState('wasabi_banking') == 'started' then
        return 'wasabi_banking'
    end

    if Framework == 'qb' then
        if GetResourceState('qb-banking') == 'started' then
            local version = GetResourceMetadata('qb-banking', 'version', 0)
            if version and tonumber(string.sub(version, 1, 3)) >= 2 then
                return 'qb-banking'
            end
        end
        if GetResourceState('qb-management') == 'started' then
            return 'qb-management'
        end
    elseif Framework == 'esx' then
        if GetResourceState('esx_society') == 'started' then
            return 'esx_addonaccount'
        end
    end

    print('^1[ERROR]: No banking system auto-detected.^0')
    return nil
end

-- Get the banking system to use
local banking = setup.banking == 'auto' and DetectBankingSystem() or setup.banking

-- Get society account balance
--- @param society string Society name
--- @param shopId string|nil Shop ID for tracking
--- @return number
local function GetSocietyBalance(society, shopId)
    if banking == 'qb-banking' then
        return exports['qb-banking']:GetAccountBalance(society) or 0
    elseif banking == 'qb-management' then
        return exports['qb-management']:GetAccount(society) or 0
    elseif banking == 'esx_addonaccount' then
        local balance = promise.new()
        TriggerEvent("esx_society:getSociety", society, function(data)
            if not data then return balance:resolve(0) end

            TriggerEvent("esx_addonaccount:getSharedAccount", data.account, function(account)
                return balance:resolve(account.money)
            end)
        end)
        return Citizen.Await(balance)
    elseif banking == 'Renewed-Banking' then
        return exports['Renewed-Banking']:getAccountMoney(society) or 0
    elseif banking == 'okokBanking' then
        return exports.okokBanking:GetAccount(society) or 0
    elseif banking == 'fd_banking' then
        return exports.fd_banking:GetAccount(society) or 0
    elseif banking == 'tgg-banking' then
        return exports['tgg-banking']:GetSocietyAccountMoney(society) or 0
    elseif banking == 'wasabi_banking' then
        return exports.wasabi_banking:GetAccountBalance(society, 'society') or 0
    else
        -- Add custom banking here
    end
    return 0
end

-- Add money to society account
--- @param society string Society name
--- @param amount number Amount to add
--- @param shopId string|nil Shop ID for tracking
local function AddSocietyMoney(society, amount, shopId)
    if banking == 'qb-banking' then
        exports['qb-banking']:AddMoney(society, amount)
    elseif banking == 'qb-management' then
        exports['qb-management']:AddMoney(society, amount)
    elseif banking == 'esx_addonaccount' then
        TriggerEvent("esx_society:getSociety", society, function(data)
            TriggerEvent("esx_addonaccount:getSharedAccount", data.account, function(account)
                account.addMoney(amount)
            end)
        end)
    elseif banking == 'Renewed-Banking' then
        exports['Renewed-Banking']:addAccountMoney(society, amount)
    elseif banking == 'okokBanking' then
        exports.okokBanking:AddMoney(society, amount)
    elseif banking == 'fd_banking' then
        exports.fd_banking:AddMoney(society, amount)
    elseif banking == 'tgg-banking' then
        exports['tgg-banking']:AddSocietyMoney(society, amount)
    elseif banking == 'wasabi_banking' then
        exports.wasabi_banking:AddMoney('society', society, amount)
    else
        -- Add custom banking here
    end
end

-- Remove money from society account
--- @param society string Society name
--- @param amount number Amount to remove
--- @param shopId string|nil Shop ID for tracking
local function RemoveSocietyMoney(society, amount, shopId)
    if banking == 'qb-banking' then
        exports['qb-banking']:RemoveMoney(society, amount)
    elseif banking == 'qb-management' then
        exports['qb-management']:RemoveMoney(society, amount)
    elseif banking == 'esx_addonaccount' then
        TriggerEvent("esx_society:getSociety", society, function(data)
            TriggerEvent("esx_addonaccount:getSharedAccount", data.account, function(account)
                account.removeMoney(amount)
            end)
        end)
    elseif banking == 'Renewed-Banking' then
        exports['Renewed-Banking']:removeAccountMoney(society, amount)
    elseif banking == 'okokBanking' then
        exports.okokBanking:RemoveMoney(society, amount)
    elseif banking == 'fd_banking' then
        exports.fd_banking:RemoveMoney(society, amount)
    elseif banking == 'tgg-banking' then
        exports['tgg-banking']:RemoveSocietyMoney(society, amount)
    elseif banking == 'wasabi_banking' then
        exports.wasabi_banking:RemoveMoney('society', society, amount)
    else
        -- Add custom banking here
    end
end

-- Get custom payment method balance
--- @param source number Player ID
--- @param paymentId string Payment method ID
--- @param shopId string|nil Shop ID for tracking
--- @return number Balance
local function GetCustomBalance(source, paymentId, shopId)
    -- Example: Handle special custom payment methods here
    -- By default, paymentId is treated as an item name

    if paymentId == 'example_special_currency' then
        -- return exports.your_resource:GetBalance(source) or 0
    else
        -- Use paymentId as item name (e.g., 'black_money', 'crypto', 'gold')
        return GetItemCount(source, paymentId) or 0
    end

    return 0
end

-- Remove custom payment method
--- @param source number Player ID
--- @param paymentId string Payment method ID
--- @param amount number Amount to remove
--- @param shopId string|nil Shop ID for tracking
local function RemoveCustomMoney(source, paymentId, amount, shopId)
    -- Example: Handle special custom payment methods here
    -- By default, paymentId is treated as an item name

    if paymentId == 'example_special_currency' then
        -- exports.your_resource:RemoveMoney(source, amount)
    else
        -- Use paymentId as item name (e.g., 'black_money', 'crypto', 'gold')
        RemoveItem(source, paymentId, amount)
    end
end

return {
    GetSocietyBalance = GetSocietyBalance,
    AddSocietyMoney = AddSocietyMoney,
    RemoveSocietyMoney = RemoveSocietyMoney,
    GetCustomBalance = GetCustomBalance,
    RemoveCustomMoney = RemoveCustomMoney
}