Files
red-valley/resources/[framework]/[addons]/jg-dealerships/framework/sv-currencies.lua
2026-03-29 21:41:17 +03:00

527 lines
20 KiB
Lua

--[[
Description:
Server-side currency configuration and handling.
This file defines all available currencies (payment methods) for the dealership system.
Users can add custom currencies like VIP Coins by extending the Currencies table.
Global Namespace:
Currencies.Server
Globals:
Currencies.Server.GetAll() - Returns all registered currencies
Currencies.Server.Get(id) - Returns a specific currency by ID
Currencies.Server.GetBalance(src, currencyId) - Get player's balance for a currency
Currencies.Server.AddBalance(src, amount, currencyId) - Add to player's balance
Currencies.Server.RemoveBalance(src, amount, currencyId) - Remove from player's balance
Currencies.Server.GetBalanceOffline(identifier, currencyId) - Get offline player's balance
Currencies.Server.RemoveBalanceOffline(identifier, amount, currencyId) - Remove from offline player's balance
Currencies.Server.ConvertToBase(amount, currencyId) - Convert currency amount to base value
Currencies.Server.ConvertFromBase(amount, currencyId) - Convert base value to currency amount (or flatCost if set)
Currencies.Server.FormatAmount(amount, currencyId) - Format an amount for display
Currencies.Server.AllowsFinance(currencyId) - Check if currency can be used for financing
Currencies.Server.HasFlatCost(currencyId) - Check if currency uses flat cost pricing
Currencies.Server.GetFlatCost(currencyId) - Get the flat cost amount for a currency
Exports:
getCurrencies - Returns all registered currencies
getCurrency - Returns a specific currency by ID
]]--
Currencies = Currencies or {}
Currencies.Server = Currencies.Server or {}
---@class CurrencyDefinition
---@field id string Unique identifier for the currency (e.g., "bank", "cash", "vip_coins")
---@field label string Display name (e.g., "Bank Account", "Cash", "VIP Coins")
---@field format string Format string for displaying amounts. Use %s for the amount (e.g., "$%s" -> "$1,000" or "%s coins" -> "1,000 coins")
---@field conversionRate? number How much 1 unit of this currency is worth in base currency. 1.0 = same as bank/cash, 10000 = 1 unit = $10,000. Ignored if flatCost is set.
---@field flatCost? number Optional: If set, this fixed amount is charged per transaction regardless of the item's base price. E.g., flatCost = 1 means 1 coin per purchase.
---@field allowFinance boolean Whether this currency can be used for financed purchases and recurring payments. Must be false if flatCost is set.
---@field fetchBalance fun(src: number): number Function to get player's balance
---@field addBalance fun(src: number, amount: number): boolean Function to add to player's balance
---@field removeBalance fun(src: number, amount: number): boolean Function to remove from player's balance
---@field fetchBalanceOffline? fun(identifier: string): number Optional: Function to get offline player's balance (for finance processing)
---@field removeBalanceOffline? fun(identifier: string, amount: number): boolean Optional: Function to remove from offline player's balance (for finance processing)
---@type table<string, CurrencyDefinition>
local registeredCurrencies = {}
-- ============================================================================
-- API
-- ============================================================================
---Get all registered currencies
---@return table<string, CurrencyDefinition>
function Currencies.Server.GetAll()
return registeredCurrencies
end
---Get a specific currency by ID
---@param currencyId string
---@return CurrencyDefinition|nil
function Currencies.Server.Get(currencyId)
return registeredCurrencies[currencyId]
end
---Register a currency in the system
---@param currency CurrencyDefinition
function Currencies.Server.Register(currency)
if not currency.id then
error("Currency must have an 'id' field")
end
registeredCurrencies[currency.id] = currency
DebugPrint(("Registered currency: %s"):format(currency.id), "debug")
end
---Get a list of currency IDs that are valid for purchases
---@return string[]
function Currencies.Server.GetPurchaseCurrencyIds()
local ids = {}
for id, _ in pairs(registeredCurrencies) do
table.insert(ids, id)
end
return ids
end
---Get player's balance for a specific currency
---@param src number Player source
---@param currencyId string Currency ID
---@return number balance
function Currencies.Server.GetBalance(src, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
DebugPrint(("Unknown currency: %s"):format(currencyId), "warning")
return 0
end
return currency.fetchBalance(src)
end
---Add to player's balance for a specific currency
---@param src number Player source
---@param amount number Amount to add (in the currency's units)
---@param currencyId string Currency ID
---@return boolean success
function Currencies.Server.AddBalance(src, amount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
DebugPrint(("Unknown currency: %s"):format(currencyId), "warning")
return false
end
return currency.addBalance(src, amount)
end
---Remove from player's balance for a specific currency
---@param src number Player source
---@param amount number Amount to remove (in the currency's units)
---@param currencyId string Currency ID
---@return boolean success
function Currencies.Server.RemoveBalance(src, amount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
DebugPrint(("Unknown currency: %s"):format(currencyId), "warning")
return false
end
return currency.removeBalance(src, amount)
end
---Get offline player's balance for a specific currency
---@param identifier string Player identifier
---@param currencyId string Currency ID
---@return number balance
function Currencies.Server.GetBalanceOffline(identifier, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
DebugPrint(("Unknown currency: %s"):format(currencyId), "warning")
return 0
end
if not currency.fetchBalanceOffline then
DebugPrint(("Currency %s does not support offline balance fetching"):format(currencyId), "warning")
return 0
end
return currency.fetchBalanceOffline(identifier)
end
---Remove from offline player's balance for a specific currency
---@param identifier string Player identifier
---@param amount number Amount to remove (in the currency's units)
---@param currencyId string Currency ID
---@return boolean success
function Currencies.Server.RemoveBalanceOffline(identifier, amount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
DebugPrint(("Unknown currency: %s"):format(currencyId), "warning")
return false
end
if not currency.removeBalanceOffline then
DebugPrint(("Currency %s does not support offline balance removal"):format(currencyId), "warning")
return false
end
return currency.removeBalanceOffline(identifier, amount)
end
---Convert an amount from a custom currency to base currency value
---@param amount number Amount in the custom currency
---@param currencyId string Currency ID
---@return number baseAmount Amount in base currency ($)
function Currencies.Server.ConvertToBase(amount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return amount end
-- flatCost currencies don't have a meaningful base conversion
-- Return 0 to indicate this conversion isn't applicable
if currency.flatCost then
return 0
end
return amount * currency.conversionRate
end
---Convert an amount from base currency to a custom currency
---@param baseAmount number Amount in base currency ($)
---@param currencyId string Currency ID
---@return number amount Amount in the custom currency
function Currencies.Server.ConvertFromBase(baseAmount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return baseAmount end
-- If flatCost is set, return that fixed amount regardless of base price
if currency.flatCost then
return currency.flatCost
end
return baseAmount / currency.conversionRate
end
---Format an amount for display using the currency's format string
---@param amount number Amount to format
---@param currencyId string Currency ID
---@return string formatted Formatted string (e.g., "$1,000" or "100 coins")
function Currencies.Server.FormatAmount(amount, currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then
return tostring(amount)
end
-- Note: The actual number formatting is handled by the UI
return string.format(currency.format, tostring(amount))
end
---Check if a currency allows financing
---@param currencyId string Currency ID
---@return boolean
function Currencies.Server.AllowsFinance(currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return false end
-- Flat cost currencies cannot be financed (you can't split 1 coin into payments)
if currency.flatCost then return false end
return currency.allowFinance == true
end
---Check if a currency uses flat cost pricing (fixed amount per transaction)
---@param currencyId string Currency ID
---@return boolean
function Currencies.Server.HasFlatCost(currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return false end
return currency.flatCost ~= nil
end
---Get the flat cost amount for a currency
---@param currencyId string Currency ID
---@return number|nil flatCost The flat cost amount, or nil if not a flat cost currency
function Currencies.Server.GetFlatCost(currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return nil end
return currency.flatCost
end
---Check if a currency supports offline operations (for finance processing)
---@param currencyId string Currency ID
---@return boolean
function Currencies.Server.SupportsOffline(currencyId)
local currency = registeredCurrencies[currencyId]
if not currency then return false end
return currency.fetchBalanceOffline ~= nil and currency.removeBalanceOffline ~= nil
end
---Get all currencies as a simplified table for sending to the client
---@return table[] currencies Array of {id, label, format, conversionRate, flatCost, allowFinance}
function Currencies.Server.GetAllForClient()
local result = {}
for id, currency in pairs(registeredCurrencies) do
table.insert(result, {
id = currency.id,
label = currency.label,
format = currency.format,
conversionRate = currency.conversionRate,
flatCost = currency.flatCost,
allowFinance = currency.allowFinance,
})
end
return result
end
-- ============================================================================
-- Callbacks
-- ============================================================================
lib.callback.register("jg-dealerships:server:get-currencies", function()
return Currencies.Server.GetAllForClient()
end)
lib.callback.register("jg-dealerships:server:get-player-balances", function(src, currencyIds)
local balances = {}
for _, currencyId in ipairs(currencyIds or Currencies.Server.GetPurchaseCurrencyIds()) do
balances[currencyId] = Currencies.Server.GetBalance(src, currencyId)
end
return balances
end)
-- ============================================================================
-- Exports
-- ============================================================================
exports("getCurrencies", Currencies.Server.GetAll)
exports("getCurrency", Currencies.Server.Get)
exports("registerCurrency", Currencies.Server.Register)
-- ============================================================================
-- Default Currencies (Bank and Cash)
-- These use the framework's built-in money handling
-- The format string comes from Config.Currency so you don't need to modify
-- this file to change the currency symbol!
-- ============================================================================
-- Bank Account
Currencies.Server.Register({
id = "bank",
label = "Bank Account",
format = Config.Currency or "$%s",
conversionRate = 1.0,
allowFinance = true,
fetchBalance = function(src)
local player = Framework.Server.GetPlayer(src)
if not player then return 0 end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
return player.PlayerData.money.bank or 0
elseif Config.Framework == "ESX" then
for _, acc in pairs(player.getAccounts()) do
if acc.name == "bank" then
return acc.money or 0
end
end
end
return 0
end,
addBalance = function(src, amount)
local player = Framework.Server.GetPlayer(src)
if not player then return false end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
player.Functions.AddMoney("bank", Round(amount, 0))
elseif Config.Framework == "ESX" then
player.addAccountMoney("bank", Round(amount, 0))
end
return true
end,
removeBalance = function(src, amount)
local player = Framework.Server.GetPlayer(src)
if not player then return false end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
player.Functions.RemoveMoney("bank", Round(amount, 0))
elseif Config.Framework == "ESX" then
player.removeAccountMoney("bank", Round(amount, 0))
end
return true
end,
fetchBalanceOffline = function(identifier)
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
local result = MySQL.scalar.await(
"SELECT JSON_EXTRACT(money, '$.bank') FROM " .. Framework.PlayersTable .. " WHERE " .. Framework.PlayersTableId .. " = ?",
{ identifier }
)
return tonumber(result) or 0
elseif Config.Framework == "ESX" then
local result = MySQL.scalar.await(
"SELECT accounts FROM " .. Framework.PlayersTable .. " WHERE " .. Framework.PlayersTableId .. " = ?",
{ identifier }
)
if not result then return 0 end
local accounts = json.decode(result)
if type(accounts) == "table" then
if accounts.bank then
return tonumber(accounts.bank) or 0
else
for _, acc in pairs(accounts) do
if acc.name == "bank" then
return tonumber(acc.money) or 0
end
end
end
end
end
return 0
end,
removeBalanceOffline = function(identifier, amount)
amount = Round(amount, 0)
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
local affectedRows = MySQL.update.await(
"UPDATE " .. Framework.PlayersTable .. " SET money = JSON_SET(money, '$.bank', JSON_EXTRACT(money, '$.bank') - ?) WHERE " .. Framework.PlayersTableId .. " = ?",
{ amount, identifier }
)
return affectedRows > 0
elseif Config.Framework == "ESX" then
local result = MySQL.single.await(
"SELECT accounts FROM " .. Framework.PlayersTable .. " WHERE " .. Framework.PlayersTableId .. " = ?",
{ identifier }
)
if not result or not result.accounts then return false end
local accounts = json.decode(result.accounts)
if type(accounts) == "table" then
if accounts.bank then
accounts.bank = (tonumber(accounts.bank) or 0) - amount
else
for i, acc in pairs(accounts) do
if acc.name == "bank" then
accounts[i].money = (tonumber(acc.money) or 0) - amount
break
end
end
end
MySQL.update.await(
"UPDATE " .. Framework.PlayersTable .. " SET accounts = ? WHERE " .. Framework.PlayersTableId .. " = ?",
{ json.encode(accounts), identifier }
)
return true
end
end
return false
end,
})
-- Cash
Currencies.Server.Register({
id = "cash",
label = "Cash",
format = Config.Currency or "$%s",
conversionRate = 1.0,
allowFinance = false, -- Cash typically not allowed for financing
fetchBalance = function(src)
local player = Framework.Server.GetPlayer(src)
if not player then return 0 end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
return player.PlayerData.money.cash or 0
elseif Config.Framework == "ESX" then
for _, acc in pairs(player.getAccounts()) do
if acc.name == "money" then
return acc.money or 0
end
end
end
return 0
end,
addBalance = function(src, amount)
local player = Framework.Server.GetPlayer(src)
if not player then return false end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
player.Functions.AddMoney("cash", Round(amount, 0))
elseif Config.Framework == "ESX" then
player.addAccountMoney("money", Round(amount, 0))
end
return true
end,
removeBalance = function(src, amount)
local player = Framework.Server.GetPlayer(src)
if not player then return false end
if Config.Framework == "QBCore" or Config.Framework == "Qbox" then
player.Functions.RemoveMoney("cash", Round(amount, 0))
elseif Config.Framework == "ESX" then
player.removeAccountMoney("money", Round(amount, 0))
end
return true
end,
-- Cash doesn't support offline operations (no financing)
fetchBalanceOffline = nil,
removeBalanceOffline = nil,
})
-- ============================================================================
-- EXAMPLE: VIP Coins Custom Currency
-- Uncomment and modify this to add your own custom currency!
-- ============================================================================
-- VIP Coins
-- Currencies.Server.Register({
-- id = "vip_coins",
-- label = "VIP Coins",
-- format = "%s coins", -- Displays as "1,000 coins" instead of "$1,000"
-- conversionRate = 10000, -- 1 VIP coin = $10,000 base currency
-- flatCost = 1, -- Use a flat rate of 1 coin = 1 transaction (if this is set, conversionRate is ignored)
-- allowFinance = false, -- Allow VIP coins for financed purchases (not compatible with flatCost = 1)
-- fetchBalance = function(src)
-- -- Replace with your own VIP coins balance system
-- -- Example using a database table:
-- -- local result = MySQL.scalar.await("SELECT coins FROM vip_coins WHERE identifier = ?", { Framework.Server.GetPlayerIdentifier(src) })
-- -- return tonumber(result) or 0
-- -- Example using an export from another resource:
-- -- return exports["your-vip-system"]:GetPlayerCoins(src) or 0
-- return 0 -- Default: no balance
-- end,
-- addBalance = function(src, amount)
-- -- Replace with your own VIP coins add system
-- -- Example:
-- -- MySQL.update.await("UPDATE vip_coins SET coins = coins + ? WHERE identifier = ?", { amount, Framework.Server.GetPlayerIdentifier(src) })
-- -- return true
-- -- Example using an export:
-- -- return exports["your-vip-system"]:AddPlayerCoins(src, amount)
-- return false -- Default: not implemented
-- end,
-- removeBalance = function(src, amount)
-- -- Replace with your own VIP coins remove system
-- -- Example:
-- -- MySQL.update.await("UPDATE vip_coins SET coins = coins - ? WHERE identifier = ?", { amount, Framework.Server.GetPlayerIdentifier(src) })
-- -- return true
-- -- Example using an export:
-- -- return exports["your-vip-system"]:RemovePlayerCoins(src, amount)
-- return false -- Default: not implemented
-- end,
-- -- Optional: Implement these for offline finance payment processing
-- fetchBalanceOffline = function(identifier)
-- -- Example:
-- -- local result = MySQL.scalar.await("SELECT coins FROM vip_coins WHERE identifier = ?", { identifier })
-- -- return tonumber(result) or 0
-- return 0
-- end,
-- removeBalanceOffline = function(identifier, amount)
-- -- Example:
-- -- MySQL.update.await("UPDATE vip_coins SET coins = coins - ? WHERE identifier = ?", { amount, identifier })
-- -- return true
-- return false
-- end,
-- })