RE:CORD
REC_Economy

Stracture

Welcome to REC_Economy documentation.
Added
Updated
config/sv_config.lua

---[[
---     REC_Economy server config
---     What each setting does, and why the defaults are what they are:
---     https://docs.re-cord.dev/en/resources/rec_economy
---]]

---@type REC_Library.Server.Class.Web.WebConfig
local webConfig = require "@REC_Library.server.class.web.sv_webConfig"

---@type REC_Library.Server.Class.Web.WebConfigStore
local WebConfigStore = require "@REC_Library.server.class.web.sv_webConfigStore"

---@type REC_Library.Server.Class.Staff.StaffConfig
local staffConfig = require "@REC_Library.server.class.staff.sv_staffConfig"

---[[
---     Settings the admin panel owns
---     They are declared in server/sv_settings.lua (value, range and description) and
---     changed from the panel, so they are not repeated here.
---]]
---@type REC_Economy.Server.Settings
local settings = require "@REC_Economy.server.sv_settings"

---@type REC_Economy.Shared.Enum
local shEnums = require "@REC_Economy.shared.sh_enum"
local moneyTypes = shEnums.moneyTypes

---@class REC_Economy.Server.Config
local config = {}

---[[
---    Debug Mode
---]]
---@type boolean
config.debugMode = true

---[[
---     What the figures count
---     Both of these decide what lands in the money supply. Who holds it is a
---     separate question, answered by config.staff below.
---]]
config.counting = {

    ---[[
    ---     Add the ItemManager money item circulation to the supply
    ---     true makes this resource wait for REC_ItemManager at boot, so leave it off
    ---     on a server with no physical money items.
    ---]]
    ---@type boolean
    moneyItems = false,

    ---[[
    ---     Money kinds never read from a player balance
    ---     Set cash = true when cash is a physical item, so it is not counted twice,
    ---     once as a balance and once through config.moneyItems.
    ---]]
    ---@type table<REC_Economy.Shared.Enum.MoneyTypes, true>
    skipMoneyTypes = {
        -- cash = true,
    },
}

---[[
---     Money flow bucket names
---     The thresholds and intervals belong to the admin panel, only the literals the
---     stored rows are keyed by stay here: changing one renames the bucket, so the
---     figures already recorded under the old name stop lining up.
---     Requires the framework adapter to implement setOnMoneyChange (qb / qbx are
---     wired up, ESX depends on the build, ox_core has no event to hook).
---]]
config.flows = {

    ---[[
    ---     Exact reason labels mapped to faucet / sink / transfer / internal.
    ---     Unknown reasons stay unclassified; classification applies to new records.
    ---     Use faucet only for newly issued money, sink only for destroyed money.
    ---]]
    classifications = {
        -- ["paycheck"] = "faucet",
        -- ["bank-deposit"] = "internal",
        -- ["player-transfer"] = "transfer",
    },

    jobs = {

        ---@type string
        unknownJob = "unknown", -- bucket for a mover whose job could not be read
    },

    ---@type string
    otherReason = "other", -- anything past the cap the panel sets

    ---[[
    ---     Reason recorded when the caller passed none
    ---     Only change it for a custom framework adapter that uses another literal.
    ---]]
    ---@type string
    unknownReason = "unknown",
}

---[[
---     Alert webhook
---     Every threshold belongs to the admin panel now. The url stays here because a
---     read only token can see the panel, and a webhook url is a credential.
---]]
config.alerts = {

    webhook = {

        ---@type string
        url = "",

        ---@type string
        userName = "REC_Economy",
    },
}

---[[
---     Money items
---     multiplier is the currency value of one item, type is the kind it counts as.
---]]
---@type table<string, REC_Economy.Server.Config.MoneyItem>
config.moneyItems = {

    ["black_money"] = {

        type = moneyTypes.black_money,

        multiplier = 1,
    },
}

---[[
---     Scheduled report webhook
---     The schedule and the target ratio belong to the admin panel.
---]]
---[[
---     Analytics uses one settlement currency; no implicit exchange rate is applied.
---     Price basket quantities stay fixed when comparing the two periods.
---     Growth cohorts start when this resource first observes a character.
---]]
config.analytics = {
    moneyTypes = { "cash", "bank_money", },
    targetWealth = 100000,
    priceBasket = {
        -- ["water"] = 10,
        -- ["bread"] = 10,
    },
    maxActivityHours = 12,
    maxActiveSessions = 500,
    maxTradeKeys = 500,
}

config.report = {

    webhook = {

        ---@type string
        url = "", -- empty reuses config.alerts.webhook.url

        ---@type string
        userName = "REC_Economy",
    },
}

---[[
---     Characters left out of every figure
---     Their balances stay out of the supply, the distribution, the dormancy tiers,
---     the player list and the alerts, and their movements are not recorded as a
---     money flow. An admin may hold whatever they like without the economy noticing.
---
---     The shared list lives in REC_Library server/sv_config.lua and covers every
---     REC_* resource at once. A convar does the same without touching a file:
---         set REC:staffCitizenIds "ABCDEFGH, K3PQ81ZR"
---         set REC_Economy:staffCitizenIds "ABCDEFGH"   # this panel alone
---]]
config.staff = staffConfig:build({

    ---[[
    ---     Added on top of the shared list, never replacing it
    ---     An empty one changes nothing, so the shared list still applies.
    ---]]
    ---@type string[]
    -- citizenIds = {
    --     "ABCDEFGH",
    -- },

    -- false counts staff like anyone else, so every figure covers the whole roster
    -- enabled = false,

    -- false drops the shared list, leaving only the citizenIds above
    -- useSharedList = false,

    -- replaces { "admin" }, and is only read where a source is known but the
    -- character is not. This resource always has a citizenId, so it changes nothing here
    -- aceGroups = {
    --     "admin",
    --     "moderator",
    -- },
})

---[[
---     Admin UI
---     The in-game UI (/economy) is always available and is gated by aceGroups.
---     http below only affects the browser route.
---
---     Tokens, dist dir and the address allowlist are shared defaults from
---     REC_Library. One convar covers every REC_* panel, so set these once:
---         set REC:adminToken "xxxxx"
---         set REC:supportToken "yyyyy"
---         set REC:allowedAddresses "192.168.1.0/24"   # opens the LAN everywhere
---
---     Naming this resource narrows one of them to this panel alone:
---         set REC_Economy:supportToken "zzzzz"
---]]
config.web = webConfig:build(config.debugMode, {

    ---[[
    ---     Permission areas this resource understands, as "<area>:read" / "<area>:write"
    ---]]
    ---@type string[]
    areas = {
        "economy",      -- balances, history, flows and alerts (write recalculates)
        "notes",        -- the operator notes
        "watchlist",    -- the watched characters and their events
        "settings",     -- the runtime config overrides (write changes them)
    },

    ---[[
    ---     Who opens the in-game panel, and what they may do once inside
    ---     Added on top of the shared list in REC_Library (admin, full access), so
    ---     naming one narrow grant here can never drop the row that lets an admin in.
    ---     Every entry that matches contributes its scopes, and a player holding two
    ---     of them gets the union. There is no deny form: to take something away,
    ---     stop granting it. Naming both an ace and a job means the player needs both.
    ---]]
    ---@type REC_Library.Server.Class.Web.WebConfig.Grant[]
    grants = {
        -- { ace = "moderator", scopes = { "economy:read", "watchlist", }, },
        -- { job = "police", ranks = { [3] = true, [4] = true, }, onDutyOnly = true, scopes = { "watchlist:read", }, label = "police", },
    },

    -- false leaves the shared list out, so only the grants above count here
    -- useSharedGrants = false,

    http = {

        ---@type boolean
        enabled = true, -- RELEASE:false

        -- where the built panel lives, relative to this resource
        -- distDir = "web/build",

        -- replaces the convar built admin/support pair entirely, which puts a literal
        -- token in this file: reach for customTokens below instead
        -- tokens = {
        --     ["a-literal-token"] = { label = "audit", scopes = { "read" }, },
        -- },

        -- named tokens beyond admin/support, each scoped on its own: https://docs.re-cord.dev/en/resources/rec_library/web-config
        -- customTokens = {
        --     { name = "economyOnly", scopes = { "economy" }, },
        --     { name = "fullAccess", scopes = { "*" }, },
        -- },

        -- replaces the convar-built address list entirely, keep loopback in it
        -- allowedAddresses = {
        --     "127.0.0.0/8",
        --     "192.168.1.0/24",
        -- },

        -- trustedProxies = {
        --     "10.0.0.5",
        -- },
    },
}) --[[@as REC_Economy.Server.Config.Web]]

---@class REC_Economy.Server.Config.MoneyItem
---@field type REC_Economy.Shared.Enum.MoneyTypes
---@field multiplier integer

-- put the panel owned defaults in place before anything reads them
WebConfigStore.seed(config, settings.list)

return config

---@class REC_Economy.Server.Config.Web: REC_Library.Server.Class.Web.WebConfig.Result

---@class REC_Economy.Server.Config
---@field staff REC_Library.Server.Class.Staff.Staff
©2026 RE:CORD Development Group. All rights reserved.