Examples

Shop System

Purchase and transaction handling with async operations

This shop system uses async operations to handle purchases without blocking gameplay.

Setup

local shopDB = db:Collection("shop_items")
local transactionsDB = db:Collection("transactions")

-- Create indexes (sync is fine for initialization)
shopDB:CreateIndex({ id = 1 }, true, "item_id_unique")
transactionsDB:CreateIndex({ steamid = 1 }, false, "steamid_idx")
transactionsDB:CreateIndex({ created_at = -1 }, false, "created_desc")

Initialize Shop

function InitShop()
    -- Check if shop is already initialized
    shopDB:CountAsync({}, function(err, count)
        if err then
            ErrorNoHalt("Failed to check shop: " .. err .. "\n")
            return
        end
        
        if count > 0 then
            print("✓ Shop already initialized")
            return
        end
        
        -- Initialize shop items
        shopDB:InsertManyAsync({
            {
                id = "health_kit",
                name = "Health Kit",
                price = 100,
                description = "Restores 50 HP",
                category = "medical",
                stock = -1  -- -1 = unlimited
            },
            {
                id = "armor_vest",
                name = "Armor Vest",
                price = 250,
                description = "Provides 50 armor",
                category = "armor",
                stock = -1
            },
            {
                id = "ammo_pack",
                name = "Ammo Pack",
                price = 50,
                description = "Refills ammunition",
                category = "ammo",
                stock = -1
            }
        }, function(err, ids)
            if err then
                ErrorNoHalt("Failed to initialize shop: " .. err .. "\n")
            else
                print("✓ Shop initialized with " .. #ids .. " items")
            end
        end)
    end)
end

-- Initialize shop on server start
hook.Add("Initialize", "InitShop", InitShop)

Get Shop Items

function GetShopItems(category, callback)
    local filter = category and { category = category } or {}
    
    shopDB:FindAsync(filter, 50, function(err, items)
        if err then
            ErrorNoHalt("Failed to get shop items: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(items or {})
    end)
end

Purchase Item

function PurchaseItem(ply, itemId, callback)
    if not IsValid(ply) then return end
    
    local steamid = ply:SteamID()
    local nickname = ply:Nick()
    
    -- Step 1: Get item details
    shopDB:FindOneAsync({ id = itemId }, function(err, item)
        if err then
            ErrorNoHalt("Failed to get item: " .. err .. "\n")
            if callback then callback(false, "Database error") end
            return
        end
        
        if not item then
            if callback then callback(false, "Item not found") end
            return
        end
        
        -- Step 2: Check and deduct credits atomically
        playersDB:UpdateOneAsync(
            {
                steamid = steamid,
                credits = { ["$gte"] = item.price }
            },
            {
                ["$inc"] = { credits = -item.price }
            },
            false,
            function(err, updated)
                if err then
                    ErrorNoHalt("Failed to deduct credits: " .. err .. "\n")
                    if callback then callback(false, "Database error") end
                    return
                end
                
                if updated == 0 then
                    if callback then callback(false, "Not enough credits") end
                    return
                end
                
                -- Step 3: Log transaction
                transactionsDB:InsertOneAsync({
                    steamid = steamid,
                    username = nickname,
                    item_id = itemId,
                    item_name = item.name,
                    price = item.price,
                    created_at = os.time()
                }, function(err, transactionId)
                    if err then
                        ErrorNoHalt("Failed to log transaction: " .. err .. "\n")
                        -- Refund credits
                        playersDB:UpdateOneAsync(
                            { steamid = steamid },
                            { ["$inc"] = { credits = item.price } }
                        )
                        if callback then callback(false, "Transaction failed") end
                        return
                    end
                    
                    -- Step 4: Give item to player
                    GiveItemToPlayer(ply, itemId)
                    
                    print("" .. nickname .. " purchased " .. item.name)
                    if callback then callback(true, "Purchase successful", item) end
                end)
            end
        )
    end)
end

Give Item to Player

function GiveItemToPlayer(ply, itemId)
    if not IsValid(ply) then return end
    
    -- Add to inventory system (if you have one)
    AddItem(ply:SteamID(), itemId, 1)
    
    -- Give actual in-game item based on ID
    if itemId == "health_kit" then
        ply:SetHealth(math.min(ply:Health() + 50, ply:GetMaxHealth()))
        ply:ChatPrint("Used Health Kit! +50 HP")
    elseif itemId == "armor_vest" then
        ply:SetArmor(math.min(ply:Armor() + 50, 100))
        ply:ChatPrint("Equipped Armor Vest! +50 Armor")
    elseif itemId == "ammo_pack" then
        for _, wep in pairs(ply:GetWeapons()) do
            wep:SetClip1(wep:GetMaxClip1())
        end
        ply:ChatPrint("Refilled ammunition!")
    end
end

Shop Command

concommand.Add("shop", function(ply, cmd, args)
    if not IsValid(ply) then return end
    
    if #args == 0 then
        -- Display shop items
        GetShopItems(nil, function(items)
            ply:ChatPrint("=== SHOP ===")
            
            if #items == 0 then
                ply:ChatPrint("Shop is empty!")
                return
            end
            
            for i, item in ipairs(items) do
                ply:ChatPrint(string.format("%d. %s - $%d", 
                    i, item.name, item.price))
                ply:ChatPrint("   " .. item.description)
            end
            
            ply:ChatPrint("\nUse: shop <item_id>")
        end)
    else
        -- Purchase item
        local itemId = args[1]
        
        PurchaseItem(ply, itemId, function(success, message, item)
            if success then
                ply:ChatPrint("✓ Purchased " .. item.name .. " for $" .. item.price)
                
                -- Update client-side credits
                playersDB:FindOneAsync({ steamid = ply:SteamID() }, function(err, data)
                    if not err and data then
                        ply:SetNWInt("Credits", data.credits)
                    end
                end)
            else
                ply:ChatPrint("✗ Purchase failed: " .. message)
            end
        end)
    end
end)

Get Sales Statistics

function GetSalesStats(days, callback)
    local since = os.time() - (days * 24 * 60 * 60)
    
    transactionsDB:AggregateAsync({
        {
            ["$match"] = {
                created_at = { ["$gte"] = since }
            }
        },
        {
            ["$group"] = {
                _id = "$item_id",
                itemName = { ["$first"] = "$item_name" },
                totalSales = { ["$sum"] = 1 },
                revenue = { ["$sum"] = "$price" }
            }
        },
        {
            ["$sort"] = { totalSales = -1 }
        }
    }, function(err, results)
        if err then
            ErrorNoHalt("Failed to get sales stats: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(results or {})
    end)
end

-- Admin command to view sales
concommand.Add("salesreport", function(ply, cmd, args)
    if IsValid(ply) and not ply:IsAdmin() then
        ply:ChatPrint("Admin only!")
        return
    end
    
    local days = tonumber(args[1]) or 7
    
    GetSalesStats(days, function(stats)
        local output = IsValid(ply) and 
            function(msg) ply:ChatPrint(msg) end or 
            function(msg) print(msg) end
        
        output("=== Sales Report (Last " .. days .. " days) ===")
        
        if #stats == 0 then
            output("No sales recorded")
            return
        end
        
        local totalRevenue = 0
        for i, stat in ipairs(stats) do
            output(string.format("%d. %s - %d sales ($%d)", 
                i, stat.itemName or stat._id, stat.totalSales, stat.revenue))
            totalRevenue = totalRevenue + stat.revenue
        end
        
        output("\nTotal Revenue: $" .. totalRevenue)
    end)
end)

Get Player Purchase History

function GetPurchaseHistory(steamid, limit, callback)
    transactionsDB:AggregateAsync({
        { ["$match"] = { steamid = steamid } },
        { ["$sort"] = { created_at = -1 } },
        { ["$limit"] = limit or 10 },
        {
            ["$project"] = {
                item_name = 1,
                price = 1,
                created_at = 1,
                _id = 0
            }
        }
    }, function(err, history)
        if err then
            ErrorNoHalt("Failed to get purchase history: " .. err .. "\n")
            callback({})
            return
        end
        
        callback(history or {})
    end)
end

concommand.Add("myorders", function(ply)
    if not IsValid(ply) then return end
    
    GetPurchaseHistory(ply:SteamID(), 10, function(history)
        ply:ChatPrint("=== Your Recent Purchases ===")
        
        if #history == 0 then
            ply:ChatPrint("No purchases yet!")
            return
        end
        
        for i, purchase in ipairs(history) do
            local date = os.date("%Y-%m-%d", purchase.created_at)
            ply:ChatPrint(string.format("%s - $%d (%s)", 
                purchase.item_name, purchase.price, date))
        end
    end)
end)

This shop system ensures:

  • Atomic credit deduction - prevents double-spending
  • Transaction logging - audit trail of all purchases
  • Error handling - refunds on failure
  • No server lag - async operations keep game running smoothly