This shop system uses async operations to handle purchases without blocking gameplay.
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")
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)
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
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
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
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)
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)
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: