2026-04-14 15:54:53 +02:00

1338 lines
52 KiB
Lua

ESX = exports['es_extended']:getSharedObject()
-- In-memory caches
local shops = {} -- shop_id -> shop data
local shelves = {} -- shop_id -> { item_name -> {id, item_name, item_label, price, quantity} }
local storage = {} -- shop_id -> { item_name -> {id, item_name, item_label, quantity} }
local employees = {} -- shop_id -> { identifier -> {id, name} }
-- =====================
-- DEFAULT ITEMS HELPERS
-- =====================
local function FindDefaultItem(itemName, shopId)
local shopConfig = shopId and Config.Shops[shopId]
local items = Config.DefaultItems
if shopConfig and shopConfig.shopType == 'weapon' then
items = Config.WeaponDefaultItems or {}
end
for _, item in ipairs(items) do
if item.name == itemName then return item end
end
return nil
end
local itemLabelCache = {}
local function GetItemLabel(itemName)
if itemLabelCache[itemName] then return itemLabelCache[itemName] end
-- Versuche codem-inventory Export
local ok, label = pcall(function() return exports['codem-inventory']:GetItemLabel(itemName) end)
if ok and label and label ~= '' then
itemLabelCache[itemName] = label
return label
end
-- Fallback: ESX Items
local ok2, esxItems = pcall(function() return ESX.GetItems() end)
if ok2 and esxItems and esxItems[itemName] then
itemLabelCache[itemName] = esxItems[itemName].label or itemName
return itemLabelCache[itemName]
end
return itemName
end
local function GetDefaultItemsForNUI(shopId)
local shopConfig = Config.Shops[shopId]
local items = Config.DefaultItems
if shopConfig and shopConfig.shopType == 'weapon' then
items = Config.WeaponDefaultItems or {}
end
local result = {}
for _, item in ipairs(items) do
result[#result + 1] = {
item_name = item.name,
item_label = GetItemLabel(item.name),
image = item.image or item.name,
price = item.price,
quantity = -1, -- unlimited
category = item.category or '',
}
end
return result
end
local function GetCategoriesForNUI()
if not Config.Categories then return {} end
local result = {}
for _, cat in ipairs(Config.Categories) do
result[#result + 1] = {
id = cat.id,
label = cat.label,
icon = cat.icon or 'circle',
}
end
return result
end
-- In-memory Lizenz-Cache (identifier -> { licenseName -> true })
local licenseCache = {}
local function GetThemeForNUI()
if not Config.Theme then return {} end
return Config.Theme
end
-- =====================
-- WEBHOOK HELPER
-- =====================
local function SendWebhook(title, message, color)
if not Config.WebhookURL or Config.WebhookURL == "" then return end
local embed = {
{
["color"] = color or 3447003,
["title"] = "**" .. title .. "**",
["description"] = message,
["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") },
}
}
PerformHttpRequest(Config.WebhookURL, function() end, 'POST', json.encode({username = "Shop Log", embeds = embed}), { ['Content-Type'] = 'application/json' })
end
-- =====================
-- HELPERS
-- =====================
local function GetIdentifier(source)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return nil end
return xPlayer.getIdentifier()
end
local function LoadPlayerLicenses(identifier)
if licenseCache[identifier] then return end
licenseCache[identifier] = {}
local result = MySQL.query.await('SELECT license_name FROM mercyv_shop_licenses WHERE identifier = ?', { identifier })
if result then
for _, row in ipairs(result) do
licenseCache[identifier][row.license_name] = true
end
end
end
local function PlayerHasLicense(source, licenseName)
local identifier = GetIdentifier(source)
if not identifier then return false end
LoadPlayerLicenses(identifier)
return licenseCache[identifier][licenseName] == true
end
local function GrantLicense(source, licenseName)
local identifier = GetIdentifier(source)
if not identifier then return false end
LoadPlayerLicenses(identifier)
if licenseCache[identifier][licenseName] then return true end
MySQL.insert.await('INSERT IGNORE INTO mercyv_shop_licenses (identifier, license_name) VALUES (?, ?)', { identifier, licenseName })
licenseCache[identifier][licenseName] = true
return true
end
local function GetLockedCategories(source, shopId)
local shopConfig = Config.Shops[shopId]
if not shopConfig then return {} end
local categories = Config.Categories
if shopConfig.shopType == 'weapon' then
categories = Config.WeaponCategories or {}
end
local locked = {}
for _, cat in ipairs(categories) do
if cat.requiresLicense and cat.requiresLicense ~= '' then
if not PlayerHasLicense(source, cat.requiresLicense) then
locked[#locked + 1] = cat.id
end
end
end
return locked
end
local function GetPlayerName(source)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return 'Unbekannt' end
return xPlayer.getName()
end
local function IsOwner(shopId, identifier)
local shop = shops[shopId]
return shop and shop.owner_identifier == identifier
end
local function IsEmployee(shopId, identifier)
return employees[shopId] and employees[shopId][identifier] ~= nil
end
local function CountPlayerShops(identifier)
local count = 0
for _, shop in pairs(shops) do
if shop.owner_identifier == identifier then count = count + 1 end
end
return count
end
local function GetItemCategory(itemName)
for _, item in ipairs(Config.DefaultItems) do
if item.name == itemName then return item.category or '' end
end
if Config.WeaponDefaultItems then
for _, item in ipairs(Config.WeaponDefaultItems) do
if item.name == itemName then return item.category or '' end
end
end
return ''
end
local function GetShelfData(shopId)
local result = {}
if shelves[shopId] then
for _, item in pairs(shelves[shopId]) do
if item.quantity > 0 then
result[#result + 1] = {
item_name = item.item_name,
item_label = item.item_label,
price = item.price,
quantity = item.quantity,
category = GetItemCategory(item.item_name),
}
end
end
end
return result
end
local function GetStorageData(shopId)
local result = {}
if storage[shopId] then
for _, item in pairs(storage[shopId]) do
if item.quantity > 0 then
result[#result + 1] = {
item_name = item.item_name,
item_label = item.item_label,
quantity = item.quantity,
}
end
end
end
return result
end
local function GetEmployeeData(shopId)
local result = {}
if employees[shopId] then
for identifier, data in pairs(employees[shopId]) do
result[#result + 1] = {
identifier = identifier,
name = data.name,
}
end
end
return result
end
local function GetShopPublicData(shopId)
local shop = shops[shopId]
if not shop then return nil end
local configShop = Config.Shops[shopId]
return {
shop_id = shopId,
shop_name = shop.shop_name,
owner_name = shop.owner_name,
owner_identifier = shop.owner_identifier,
vault_money = shop.vault_money,
is_for_sale = shop.is_for_sale,
sale_price = shop.sale_price,
config_price = configShop and configShop.price or 0,
config_label = configShop and configShop.label or '',
sell_back_percentage = Config.SellBackPercentage or 0.70,
}
end
-- Nur öffentliche Daten fürClient-Sync (ohne vault_money, owner_identifier etc.)
local function GetSyncData()
local syncData = {}
for shopId, shop in pairs(shops) do
syncData[shopId] = {
owner_identifier = shop.owner_identifier, -- benötigt fürBlip-Farbe
owner_name = shop.owner_name,
shop_name = shop.shop_name,
}
end
return syncData
end
local function CountShelfSlots(shopId)
local count = 0
if shelves[shopId] then
for _, item in pairs(shelves[shopId]) do
if item.quantity and item.quantity > 0 then count = count + 1 end
end
end
return count
end
local function CountStorageSlots(shopId)
local count = 0
if storage[shopId] then
for _, item in pairs(storage[shopId]) do
if item.quantity and item.quantity > 0 then count = count + 1 end
end
end
return count
end
local function ClearShopData(shopId)
shelves[shopId] = {}
storage[shopId] = {}
employees[shopId] = {}
MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ?', { shopId })
MySQL.query('DELETE FROM mercyv_shop_storage WHERE shop_id = ?', { shopId })
MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ?', { shopId })
end
-- =====================
-- SQL SETUP & CACHE LOAD
-- =====================
MySQL.ready(function()
-- Create tables
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shops (
id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL UNIQUE,
owner_identifier VARCHAR(60) DEFAULT NULL, owner_name VARCHAR(100) DEFAULT NULL,
shop_name VARCHAR(100) NOT NULL, vault_money INT NOT NULL DEFAULT 0,
is_for_sale TINYINT(1) NOT NULL DEFAULT 0, sale_price INT NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_owner (owner_identifier)
)]])
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_employees (
id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL,
employee_identifier VARCHAR(60) NOT NULL, employee_name VARCHAR(100) NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_shop (shop_id), INDEX idx_employee (employee_identifier),
UNIQUE KEY uq_shop_employee (shop_id, employee_identifier)
)]])
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_shelves (
id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL,
item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL,
price INT NOT NULL, quantity INT NOT NULL DEFAULT 0,
INDEX idx_shop_shelves (shop_id), UNIQUE KEY uq_shop_item (shop_id, item_name)
)]])
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_storage (
id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL,
item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL,
quantity INT NOT NULL DEFAULT 0,
INDEX idx_shop_storage (shop_id), UNIQUE KEY uq_shop_storage_item (shop_id, item_name)
)]])
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_sales_history (
id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL,
buyer_identifier VARCHAR(60) NOT NULL, buyer_name VARCHAR(100) NOT NULL,
item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL,
price INT NOT NULL, quantity INT NOT NULL, total INT NOT NULL,
sold_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_shop_sales (shop_id), INDEX idx_sold_at (sold_at)
)]])
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_licenses (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(60) NOT NULL,
license_name VARCHAR(50) NOT NULL,
purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_player_license (identifier, license_name),
INDEX idx_identifier (identifier)
)]])
-- Ensure all config shops exist in DB
for shopId, configShop in pairs(Config.Shops) do
MySQL.query.await('INSERT IGNORE INTO mercyv_shops (shop_id, shop_name) VALUES (?, ?)', { shopId, configShop.label })
end
-- Load shops
local shopRows = MySQL.query.await('SELECT * FROM mercyv_shops')
if shopRows then
for _, row in ipairs(shopRows) do
shops[row.shop_id] = {
id = row.id,
shop_id = row.shop_id,
owner_identifier = row.owner_identifier,
owner_name = row.owner_name,
shop_name = row.shop_name,
vault_money = row.vault_money,
is_for_sale = row.is_for_sale == 1,
sale_price = row.sale_price,
}
shelves[row.shop_id] = {}
storage[row.shop_id] = {}
employees[row.shop_id] = {}
end
end
-- Load shelves
local shelfRows = MySQL.query.await('SELECT * FROM mercyv_shop_shelves WHERE quantity > 0')
if shelfRows then
for _, row in ipairs(shelfRows) do
if shelves[row.shop_id] then
shelves[row.shop_id][row.item_name] = {
id = row.id, item_name = row.item_name, item_label = row.item_label,
price = row.price, quantity = row.quantity,
}
end
end
end
-- Load storage
local storageRows = MySQL.query.await('SELECT * FROM mercyv_shop_storage WHERE quantity > 0')
if storageRows then
for _, row in ipairs(storageRows) do
if storage[row.shop_id] then
storage[row.shop_id][row.item_name] = {
id = row.id, item_name = row.item_name, item_label = row.item_label,
quantity = row.quantity,
}
end
end
end
-- Load employees
local empRows = MySQL.query.await('SELECT * FROM mercyv_shop_employees')
if empRows then
for _, row in ipairs(empRows) do
if employees[row.shop_id] then
employees[row.shop_id][row.employee_identifier] = {
id = row.id, name = row.employee_name,
}
end
end
end
print('[mercyv-shops] Loaded ' .. #(shopRows or {}) .. ' shops')
-- Sync shops to all clients
TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData())
end)
-- =====================
-- PLAYER ITEMS CALLBACK
-- =====================
ESX.RegisterServerCallback('mercyv-shops:getPlayerItems', function(source, cb)
local items = {}
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return cb(items) end
local success, playerItems = pcall(function()
return exports['codem-inventory']:GetInventory(xPlayer.getIdentifier(), source)
end)
if not success or not playerItems then
playerItems = xPlayer.getInventory()
end
if playerItems then
for _, item in pairs(playerItems) do
if item and item.name then
local amount = tonumber(item.amount or item.count) or 0
if amount > 0 then
items[#items + 1] = { name = item.name, label = item.label or item.name, amount = amount }
end
end
end
end
cb(items)
end)
-- =====================
-- ACCESS LEVEL CALLBACK
-- =====================
ESX.RegisterServerCallback('mercyv-shops:getAccessLevel', function(source, cb, shopId)
-- Waffen-Shops haben keinen Management-Zugang
local shopConfig = Config.Shops[shopId]
if shopConfig and shopConfig.shopType == 'weapon' then
return cb('none', nil)
end
local identifier = GetIdentifier(source)
if not identifier then return cb('none', nil) end
local shop = shops[shopId]
if not shop then return cb('none', nil) end
if not shop.owner_identifier then
return cb('unowned', GetShopPublicData(shopId))
end
if shop.owner_identifier == identifier then
return cb('owner', {
shop = GetShopPublicData(shopId),
shelves = GetShelfData(shopId),
storage = GetStorageData(shopId),
employees = GetEmployeeData(shopId),
defaultItems = GetDefaultItemsForNUI(shopId),
})
end
if IsEmployee(shopId, identifier) then
return cb('employee', {
shop = GetShopPublicData(shopId),
shelves = GetShelfData(shopId),
storage = GetStorageData(shopId),
})
end
return cb('none', nil)
end)
-- =====================
-- SHOP DATA CALLBACKS
-- =====================
ESX.RegisterServerCallback('mercyv-shops:getShelfItems', function(source, cb, shopId)
local shopConfig = Config.Shops[shopId]
local locked = GetLockedCategories(source, shopId)
-- Waffen-Shops sind immer NPC-Shops
if shopConfig and shopConfig.shopType == 'weapon' then
return cb(GetDefaultItemsForNUI(shopId), locked)
end
local shop = shops[shopId]
if shop and not shop.owner_identifier then
-- NPC-Shop: return default items
cb(GetDefaultItemsForNUI(shopId), locked)
else
cb(GetShelfData(shopId), locked)
end
end)
ESX.RegisterServerCallback('mercyv-shops:getSalesHistory', function(source, cb, shopId)
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then return cb({}) end
local rows = MySQL.query.await('SELECT * FROM mercyv_shop_sales_history WHERE shop_id = ? ORDER BY sold_at DESC LIMIT 50', { shopId })
cb(rows or {})
end)
-- =====================
-- PURCHASE SHOP
-- =====================
RegisterNetEvent('mercyv-shops:purchaseShop')
AddEventHandler('mercyv-shops:purchaseShop', function(shopId)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier then return end
local shop = shops[shopId]
local configShop = Config.Shops[shopId]
if not shop or not configShop then
TriggerClientEvent('mercyv-shops:notify', source, 'Shop nicht gefunden', 'error')
TriggerClientEvent('mercyv-shops:forceClose', source)
return
end
if shop.owner_identifier then
TriggerClientEvent('mercyv-shops:notify', source, 'Dieser Shop ist bereits vergeben', 'error')
TriggerClientEvent('mercyv-shops:forceClose', source)
return
end
if CountPlayerShops(identifier) >= Config.MaxShopsPerPlayer then
TriggerClientEvent('mercyv-shops:notify', source, 'Du besitzt bereits die maximale Anzahl an Shops (' .. Config.MaxShopsPerPlayer .. ')', 'error')
TriggerClientEvent('mercyv-shops:forceClose', source)
return
end
local price = configShop.price
if xPlayer.getAccount('money').money < price then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. price .. ' benötigt)', 'error')
TriggerClientEvent('mercyv-shops:forceClose', source)
return
end
xPlayer.removeAccountMoney('money', price)
shop.owner_identifier = identifier
shop.owner_name = GetPlayerName(source)
shop.shop_name = configShop.label
MySQL.query('UPDATE mercyv_shops SET owner_identifier = ?, owner_name = ?, shop_name = ? WHERE shop_id = ?', {
identifier, shop.owner_name, shop.shop_name, shopId
})
-- Add start stock (DefaultItems) directly to shelves so customers can buy immediately
if Config.StartStock and Config.StartStock > 0 and Config.DefaultItems then
print('[mercyv-shops] Adding start stock for shop ' .. shopId .. ' (' .. #Config.DefaultItems .. ' items, ' .. Config.StartStock .. ' each)')
if not shelves[shopId] then shelves[shopId] = {} end
for _, defaultItem in ipairs(Config.DefaultItems) do
local itemLabel = GetItemLabel(defaultItem.name)
shelves[shopId][defaultItem.name] = {
item_name = defaultItem.name,
item_label = itemLabel,
price = defaultItem.price,
quantity = Config.StartStock,
}
MySQL.query('INSERT INTO mercyv_shop_shelves (shop_id, item_name, item_label, price, quantity) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', {
shopId, defaultItem.name, itemLabel, defaultItem.price, Config.StartStock
})
end
print('[mercyv-shops] Start stock added to shelves successfully')
end
TriggerClientEvent('mercyv-shops:notify', source, 'Shop "' .. shop.shop_name .. '" gekauft!', 'success')
TriggerClientEvent('mercyv-shops:forceClose', source)
TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData())
SendWebhook("Shop Gekauft", string.format("Spieler: **%s** (%s)\nShop: **%s**\nPreis: **$%d**", shop.owner_name, identifier, shop.shop_name, price), 3066993)
end)
-- =====================
-- CUSTOMER BUY
-- =====================
RegisterNetEvent('mercyv-shops:buyProduct')
AddEventHandler('mercyv-shops:buyProduct', function(shopId, itemName, quantity)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier then return end
quantity = math.max(1, math.floor(tonumber(quantity) or 1))
local shopConfig = Config.Shops[shopId]
if not shopConfig then
TriggerClientEvent('mercyv-shops:notify', source, 'Shop nicht gefunden', 'error')
return
end
-- ===== WAFFEN-SHOP oder NPC-SHOP (kein Owner) =====
local shop = shops[shopId]
local isWeaponShop = shopConfig.shopType == 'weapon'
local isNpcShop = isWeaponShop or (shop and not shop.owner_identifier)
if isNpcShop then
local defaultItem = FindDefaultItem(itemName, shopId)
if not defaultItem then
TriggerClientEvent('mercyv-shops:notify', source, 'Item nicht verfügbar', 'error')
return
end
-- Prüfe ob das Item selbst eine Lizenz ist
local isLicenseItem = false
local categories = isWeaponShop and (Config.WeaponCategories or {}) or (Config.Categories or {})
for _, cat in ipairs(categories) do
if cat.requiresLicense == itemName then
isLicenseItem = true
break
end
end
-- Lizenz-Check: Prüfe ob Kategorie eine Lizenz erfordert
if not isLicenseItem then
local itemCategory = defaultItem.category or ''
for _, cat in ipairs(categories) do
if cat.id == itemCategory and cat.requiresLicense and cat.requiresLicense ~= '' then
if not PlayerHasLicense(source, cat.requiresLicense) then
TriggerClientEvent('mercyv-shops:notify', source, 'Du benötigst eine Lizenz für dieses Item', 'error')
return
end
end
end
end
-- Lizenz: bereits vorhanden?
if isLicenseItem and PlayerHasLicense(source, itemName) then
TriggerClientEvent('mercyv-shops:notify', source, 'Du besitzt diese Lizenz bereits', 'error')
return
end
local total = defaultItem.price * quantity
if xPlayer.getAccount('money').money < total then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. total .. ' benötigt)', 'error')
return
end
if isLicenseItem then
-- Lizenz in Datenbank speichern (permanent)
GrantLicense(source, itemName)
else
-- Normales Item ins Inventar
local addOk = pcall(function() exports['codem-inventory']:AddItem(source, itemName, quantity) end)
if not addOk then
TriggerClientEvent('mercyv-shops:notify', source, 'Item konnte nicht hinzugefuegt werden', 'error')
return
end
end
xPlayer.removeAccountMoney('money', total)
TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. GetItemLabel(itemName) .. ' gekauft für$' .. total, 'success')
TriggerClientEvent('mercyv-shops:buyResult', source, true, shopId, GetLockedCategories(source, shopId))
return
end
-- ===== OWNER-SHOP (Shelves-basiert) =====
local shelfItem = shelves[shopId] and shelves[shopId][itemName]
if not shelfItem or shelfItem.quantity < quantity then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug auf Lager', 'error')
return
end
local total = shelfItem.price * quantity
if xPlayer.getAccount('money').money < total then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. total .. ' benötigt)', 'error')
return
end
-- Race condition protection
local affected = MySQL.update.await('UPDATE mercyv_shop_shelves SET quantity = quantity - ? WHERE shop_id = ? AND item_name = ? AND quantity >= ?', {
quantity, shopId, itemName, quantity
})
if not affected or affected == 0 then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht mehr verfügbar', 'error')
return
end
-- Item zuerst hinzufuegen, dann Geld abziehen
local addOk = pcall(function() exports['codem-inventory']:AddItem(source, itemName, quantity) end)
if not addOk then
-- Shelf-Quantity in DB zurücksetzen
MySQL.query('UPDATE mercyv_shop_shelves SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName })
TriggerClientEvent('mercyv-shops:notify', source, 'Item konnte nicht hinzugefuegt werden', 'error')
return
end
xPlayer.removeAccountMoney('money', total)
-- Update cache
shelfItem.quantity = shelfItem.quantity - quantity
if shelfItem.quantity <= 0 then
shelves[shopId][itemName] = nil
MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName })
end
-- Add to vault (minus tax)
local tax = math.floor(total * Config.SalesTax)
local revenue = total - tax
shop.vault_money = shop.vault_money + revenue
MySQL.query('UPDATE mercyv_shops SET vault_money = vault_money + ? WHERE shop_id = ?', { revenue, shopId })
-- Record sale
MySQL.query('INSERT INTO mercyv_shop_sales_history (shop_id, buyer_identifier, buyer_name, item_name, item_label, price, quantity, total) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
shopId, identifier, GetPlayerName(source), itemName, shelfItem.item_label, shelfItem.price, quantity, total
})
TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. shelfItem.item_label .. ' gekauft für$' .. total, 'success')
TriggerClientEvent('mercyv-shops:buyResult', source, true, shopId)
SendWebhook("Produkt Verkauft", string.format("Shop: **%s**\nKäufer: **%s**\nItem: **%s** x%d\nUmsatz: **$%d**", shop.shop_name, GetPlayerName(source), shelfItem.item_label, quantity, total), 5763719)
end)
-- =====================
-- STORAGE MANAGEMENT
-- =====================
RegisterNetEvent('mercyv-shops:addToStorage')
AddEventHandler('mercyv-shops:addToStorage', function(shopId, itemName, itemLabel, quantity)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier then return end
if not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
quantity = math.max(1, math.floor(tonumber(quantity) or 1))
-- itemLabel serverseitig ermitteln (Client-Daten nicht vertrauen)
itemLabel = GetItemLabel(itemName)
-- Check storage slot limit
if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then
TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll (max ' .. Config.MaxStorageSlots .. ' verschiedene Items)', 'error')
return
end
-- Check player has items
local totalAmount = exports['codem-inventory']:GetItemsTotalAmount(source, itemName)
if not totalAmount or tonumber(totalAmount) < quantity then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Items im Inventar', 'error')
return
end
exports['codem-inventory']:RemoveItem(source, itemName, quantity)
if storage[shopId][itemName] then
storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + quantity
MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName })
else
storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = quantity }
MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', {
shopId, itemName, itemLabel, quantity
})
end
TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. itemLabel .. ' eingelagert', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
-- =====================
-- SHELF MANAGEMENT
-- =====================
RegisterNetEvent('mercyv-shops:moveToShelves')
AddEventHandler('mercyv-shops:moveToShelves', function(shopId, itemName, quantity, price)
local source = source
local identifier = GetIdentifier(source)
if not identifier then return end
if not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
quantity = math.max(1, math.floor(tonumber(quantity) or 1))
price = math.max(1, math.floor(tonumber(price) or 1))
if price > Config.MaxPrice then
TriggerClientEvent('mercyv-shops:notify', source, 'Maximaler Preis: $' .. Config.MaxPrice, 'error')
return
end
local storageItem = storage[shopId] and storage[shopId][itemName]
if not storageItem or storageItem.quantity < quantity then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Lager', 'error')
return
end
-- Check shelf slot limit
if not shelves[shopId][itemName] and CountShelfSlots(shopId) >= Config.MaxShelfSlots then
TriggerClientEvent('mercyv-shops:notify', source, 'Regale voll (max ' .. Config.MaxShelfSlots .. ' verschiedene Items)', 'error')
return
end
-- Save label before potentially removing from storage
local itemLabel = storageItem.item_label
-- Remove from storage
storageItem.quantity = storageItem.quantity - quantity
if storageItem.quantity <= 0 then
storage[shopId][itemName] = nil
MySQL.query('DELETE FROM mercyv_shop_storage WHERE shop_id = ? AND item_name = ?', { shopId, itemName })
else
MySQL.query('UPDATE mercyv_shop_storage SET quantity = ? WHERE shop_id = ? AND item_name = ?', { storageItem.quantity, shopId, itemName })
end
-- Add to shelves
if shelves[shopId][itemName] then
shelves[shopId][itemName].quantity = shelves[shopId][itemName].quantity + quantity
shelves[shopId][itemName].price = price
MySQL.query('UPDATE mercyv_shop_shelves SET quantity = quantity + ?, price = ? WHERE shop_id = ? AND item_name = ?', { quantity, price, shopId, itemName })
else
shelves[shopId][itemName] = { item_name = itemName, item_label = itemLabel, price = price, quantity = quantity }
MySQL.query('INSERT INTO mercyv_shop_shelves (shop_id, item_name, item_label, price, quantity) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity), price = VALUES(price)', {
shopId, itemName, itemLabel, price, quantity
})
end
TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ins Regal gestellt für$' .. price, 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
RegisterNetEvent('mercyv-shops:moveToStorage')
AddEventHandler('mercyv-shops:moveToStorage', function(shopId, itemName, quantity)
local source = source
local identifier = GetIdentifier(source)
if not identifier or (not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier)) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
quantity = math.max(1, math.floor(tonumber(quantity) or 1))
local shelfItem = shelves[shopId] and shelves[shopId][itemName]
if not shelfItem or shelfItem.quantity < quantity then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Regal', 'error')
return
end
-- Check storage slot limit
if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then
TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll', 'error')
return
end
-- Save label before potentially removing from shelves
local itemLabel = shelfItem.item_label
-- Remove from shelves
shelfItem.quantity = shelfItem.quantity - quantity
if shelfItem.quantity <= 0 then
shelves[shopId][itemName] = nil
MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName })
else
MySQL.query('UPDATE mercyv_shop_shelves SET quantity = ? WHERE shop_id = ? AND item_name = ?', { shelfItem.quantity, shopId, itemName })
end
-- Add to storage
if storage[shopId][itemName] then
storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + quantity
MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName })
else
storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = quantity }
MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', {
shopId, itemName, itemLabel, quantity
})
end
TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x zurück ins Lager', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
RegisterNetEvent('mercyv-shops:updateShelfPrice')
AddEventHandler('mercyv-shops:updateShelfPrice', function(shopId, itemName, newPrice)
local source = source
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
newPrice = math.max(1, math.floor(tonumber(newPrice) or 1))
if newPrice > Config.MaxPrice then
TriggerClientEvent('mercyv-shops:notify', source, 'Maximaler Preis: $' .. Config.MaxPrice, 'error')
return
end
local shelfItem = shelves[shopId] and shelves[shopId][itemName]
if not shelfItem then
TriggerClientEvent('mercyv-shops:notify', source, 'Item nicht im Regal', 'error')
return
end
shelfItem.price = newPrice
MySQL.query('UPDATE mercyv_shop_shelves SET price = ? WHERE shop_id = ? AND item_name = ?', { newPrice, shopId, itemName })
TriggerClientEvent('mercyv-shops:notify', source, 'Preis aktualisiert auf $' .. newPrice, 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
RegisterNetEvent('mercyv-shops:removeFromShelves')
AddEventHandler('mercyv-shops:removeFromShelves', function(shopId, itemName)
local source = source
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
local shelfItem = shelves[shopId] and shelves[shopId][itemName]
if not shelfItem then return end
-- Save data before removing
local itemLabel = shelfItem.item_label
local itemQty = shelfItem.quantity
-- Check storage slot limit
if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then
TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll (max ' .. Config.MaxStorageSlots .. ' verschiedene Items) — Item kann nicht verschoben werden', 'error')
return
end
-- Move to storage
if storage[shopId][itemName] then
storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + itemQty
MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { itemQty, shopId, itemName })
else
storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = itemQty }
MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', {
shopId, itemName, itemLabel, itemQty
})
end
shelves[shopId][itemName] = nil
MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName })
TriggerClientEvent('mercyv-shops:notify', source, itemLabel .. ' vom Regal entfernt', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
-- =====================
-- VAULT
-- =====================
RegisterNetEvent('mercyv-shops:withdrawVault')
AddEventHandler('mercyv-shops:withdrawVault', function(shopId, amount)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier or not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
amount = math.max(1, math.floor(tonumber(amount) or 0))
local shop = shops[shopId]
if amount > shop.vault_money then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Tresor', 'error')
return
end
-- Zuerst DB aktualisieren, dann Cache
local affected = MySQL.update.await('UPDATE mercyv_shops SET vault_money = vault_money - ? WHERE shop_id = ? AND vault_money >= ?', { amount, shopId, amount })
if not affected or affected == 0 then
TriggerClientEvent('mercyv-shops:notify', source, 'Abhebung fehlgeschlagen', 'error')
return
end
shop.vault_money = shop.vault_money - amount
xPlayer.addAccountMoney('money', amount)
TriggerClientEvent('mercyv-shops:notify', source, '$' .. amount .. ' aus dem Tresor abgehoben', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
SendWebhook("Tresor Abhebung", string.format("Spieler: **%s**\nShop: **%s**\nBetrag: **$%d**", GetPlayerName(source), shop.shop_name, amount), 16766720)
end)
-- =====================
-- EMPLOYEES
-- =====================
RegisterNetEvent('mercyv-shops:addEmployee')
AddEventHandler('mercyv-shops:addEmployee', function(shopId, targetServerId)
local source = source
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
local targetPlayer = ESX.GetPlayerFromId(tonumber(targetServerId))
if not targetPlayer then
TriggerClientEvent('mercyv-shops:notify', source, 'Spieler nicht gefunden (ID: ' .. tostring(targetServerId) .. ')', 'error')
return
end
local targetIdentifier = targetPlayer.getIdentifier()
local targetName = targetPlayer.getName()
if targetIdentifier == identifier then
TriggerClientEvent('mercyv-shops:notify', source, 'Du kannst dich nicht selbst einstellen', 'error')
return
end
if IsEmployee(shopId, targetIdentifier) then
TriggerClientEvent('mercyv-shops:notify', source, targetName .. ' ist bereits Mitarbeiter', 'error')
return
end
local empCount = 0
if employees[shopId] then
for _ in pairs(employees[shopId]) do empCount = empCount + 1 end
end
if empCount >= Config.MaxEmployeesPerShop then
TriggerClientEvent('mercyv-shops:notify', source, 'Maximale Mitarbeiter erreicht (' .. Config.MaxEmployeesPerShop .. ')', 'error')
return
end
employees[shopId][targetIdentifier] = { name = targetName }
MySQL.query('INSERT IGNORE INTO mercyv_shop_employees (shop_id, employee_identifier, employee_name) VALUES (?, ?, ?)', {
shopId, targetIdentifier, targetName
})
TriggerClientEvent('mercyv-shops:notify', source, targetName .. ' als Mitarbeiter eingestellt', 'success')
TriggerClientEvent('mercyv-shops:notify', targetPlayer.source, 'Du wurdest als Mitarbeiter bei "' .. shops[shopId].shop_name .. '" eingestellt!', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
RegisterNetEvent('mercyv-shops:removeEmployee')
AddEventHandler('mercyv-shops:removeEmployee', function(shopId, empIdentifier)
local source = source
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
if not employees[shopId] or not employees[shopId][empIdentifier] then
TriggerClientEvent('mercyv-shops:notify', source, 'Mitarbeiter nicht gefunden', 'error')
return
end
local empName = employees[shopId][empIdentifier].name
employees[shopId][empIdentifier] = nil
MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ? AND employee_identifier = ?', { shopId, empIdentifier })
TriggerClientEvent('mercyv-shops:notify', source, empName .. ' entlassen', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
end)
-- =====================
-- SETTINGS
-- =====================
RegisterNetEvent('mercyv-shops:renameShop')
AddEventHandler('mercyv-shops:renameShop', function(shopId, newName)
local source = source
local identifier = GetIdentifier(source)
if not identifier or not IsOwner(shopId, identifier) then return end
newName = tostring(newName or ''):sub(1, 50)
if #newName < 2 then
TriggerClientEvent('mercyv-shops:notify', source, 'Name zu kurz (min 2 Zeichen)', 'error')
return
end
shops[shopId].shop_name = newName
MySQL.query('UPDATE mercyv_shops SET shop_name = ? WHERE shop_id = ?', { newName, shopId })
TriggerClientEvent('mercyv-shops:notify', source, 'Shop umbenannt zu "' .. newName .. '"', 'success')
TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData())
end)
RegisterNetEvent('mercyv-shops:sellToPlayer')
AddEventHandler('mercyv-shops:sellToPlayer', function(shopId, targetServerId, salePrice)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier or not IsOwner(shopId, identifier) then return end
salePrice = math.max(1, math.floor(tonumber(salePrice) or 0))
local targetPlayer = ESX.GetPlayerFromId(tonumber(targetServerId))
if not targetPlayer then
TriggerClientEvent('mercyv-shops:notify', source, 'Spieler nicht gefunden', 'error')
return
end
local targetIdentifier = targetPlayer.getIdentifier()
if targetIdentifier == identifier then
TriggerClientEvent('mercyv-shops:notify', source, 'Du kannst den Shop nicht an dich selbst verkaufen', 'error')
return
end
if CountPlayerShops(targetIdentifier) >= Config.MaxShopsPerPlayer then
TriggerClientEvent('mercyv-shops:notify', source, 'Spieler besitzt bereits die maximale Anzahl an Shops', 'error')
return
end
if targetPlayer.getAccount('money').money < salePrice then
TriggerClientEvent('mercyv-shops:notify', source, 'Spieler hat nicht genug Geld', 'error')
return
end
-- Transfer
targetPlayer.removeAccountMoney('money', salePrice)
xPlayer.addAccountMoney('money', salePrice)
-- Pay out vault to old owner
if shops[shopId].vault_money > 0 then
xPlayer.addAccountMoney('money', shops[shopId].vault_money)
shops[shopId].vault_money = 0
end
-- Transfer ownership
local shop = shops[shopId]
shop.owner_identifier = targetIdentifier
shop.owner_name = targetPlayer.getName()
-- Clear employees
employees[shopId] = {}
MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ?', { shopId })
MySQL.query('UPDATE mercyv_shops SET owner_identifier = ?, owner_name = ?, vault_money = 0 WHERE shop_id = ?', {
targetIdentifier, shop.owner_name, shopId
})
TriggerClientEvent('mercyv-shops:notify', source, 'Shop verkauft an ' .. shop.owner_name .. ' für$' .. salePrice, 'success')
TriggerClientEvent('mercyv-shops:notify', targetPlayer.source, 'Du hast "' .. shop.shop_name .. '" gekauft für$' .. salePrice .. '!', 'success')
TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData())
SendWebhook("Shop Verkauft (Spieler)", string.format("Von: **%s**\nAn: **%s**\nShop: **%s**\nPreis: **$%d**", GetPlayerName(source), shop.owner_name, shop.shop_name, salePrice), 16766720)
end)
RegisterNetEvent('mercyv-shops:sellToServer')
AddEventHandler('mercyv-shops:sellToServer', function(shopId)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier or not IsOwner(shopId, identifier) then return end
local configShop = Config.Shops[shopId]
if not configShop then return end
local payout = math.floor(configShop.price * Config.SellBackPercentage)
local shop = shops[shopId]
-- Pay owner: sell price + vault money
local totalPayout = payout + shop.vault_money
xPlayer.addAccountMoney('money', totalPayout)
-- Return storage + shelf items to owner inventory
local failedItems = {}
if storage[shopId] then
for _, item in pairs(storage[shopId]) do
if item.quantity > 0 then
local ok = pcall(function() exports['codem-inventory']:AddItem(source, item.item_name, item.quantity) end)
if not ok then
failedItems[#failedItems + 1] = item.quantity .. 'x ' .. item.item_label
end
end
end
end
if shelves[shopId] then
for _, item in pairs(shelves[shopId]) do
if item.quantity > 0 then
local ok = pcall(function() exports['codem-inventory']:AddItem(source, item.item_name, item.quantity) end)
if not ok then
failedItems[#failedItems + 1] = item.quantity .. 'x ' .. item.item_label
end
end
end
end
if #failedItems > 0 then
TriggerClientEvent('mercyv-shops:notify', source, 'Warnung: Einige Items konnten nicht zurückgegeben werden: ' .. table.concat(failedItems, ', '), 'error')
end
-- Reset shop
shop.owner_identifier = nil
shop.owner_name = nil
shop.shop_name = configShop.label
shop.vault_money = 0
shop.is_for_sale = false
shop.sale_price = 0
ClearShopData(shopId)
MySQL.query('UPDATE mercyv_shops SET owner_identifier = NULL, owner_name = NULL, shop_name = ?, vault_money = 0, is_for_sale = 0, sale_price = 0 WHERE shop_id = ?', {
configShop.label, shopId
})
TriggerClientEvent('mercyv-shops:notify', source, 'Shop an Server verkauft! $' .. totalPayout .. ' erhalten', 'success')
TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData())
-- Close NUI for the seller
TriggerClientEvent('mercyv-shops:forceClose', source)
SendWebhook("Shop an Server Verkauft", string.format("Spieler: **%s**\nShop: **%s**\nAuszahlung: **$%d**", GetPlayerName(source), configShop.label, totalPayout), 15158332)
end)
-- =====================
-- DATA REFRESH
-- =====================
RegisterNetEvent('mercyv-shops:requestRefresh')
AddEventHandler('mercyv-shops:requestRefresh', function(shopId)
local source = source
local identifier = GetIdentifier(source)
if not identifier then return end
local shop = shops[shopId]
if not shop then return end
-- Nur Owner und Employees duerfen Daten abrufen
if not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier) then return end
local data = {
shop = GetShopPublicData(shopId),
shelves = GetShelfData(shopId),
storage = GetStorageData(shopId),
}
-- Employees erhalten keine Employee-Liste
if IsOwner(shopId, identifier) then
data.employees = GetEmployeeData(shopId)
end
TriggerClientEvent('mercyv-shops:refreshData', source, data)
end)
-- =====================
-- ORDER ITEMS (Nachbestellen aus Tresor)
-- =====================
RegisterNetEvent('mercyv-shops:orderItems')
AddEventHandler('mercyv-shops:orderItems', function(shopId, items)
local source = source
local identifier = GetIdentifier(source)
if not identifier then return end
if not IsOwner(shopId, identifier) then
TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error')
return
end
local shop = shops[shopId]
if not shop then return end
if not items or type(items) ~= 'table' or #items == 0 then
TriggerClientEvent('mercyv-shops:notify', source, 'Keine Items ausgewaehlt', 'error')
return
end
-- Validate items, calculate total cost, and check storage capacity upfront
local totalCost = 0
local validItems = {}
local newSlots = 0
for _, orderItem in ipairs(items) do
local defaultItem = FindDefaultItem(orderItem.name)
if not defaultItem then
TriggerClientEvent('mercyv-shops:notify', source, 'Item "' .. tostring(orderItem.name) .. '" nicht verfügbar', 'error')
return
end
local qty = math.max(1, math.floor(tonumber(orderItem.quantity) or 1))
totalCost = totalCost + (defaultItem.price * qty)
-- Zähle neue Storage-Slots die benötigt werden
if not storage[shopId][defaultItem.name] then
newSlots = newSlots + 1
end
validItems[#validItems + 1] = { name = defaultItem.name, quantity = qty, price = defaultItem.price }
end
-- Check storage capacity BEFORE payment
local currentSlots = CountStorageSlots(shopId)
if currentSlots + newSlots > Config.MaxStorageSlots then
TriggerClientEvent('mercyv-shops:notify', source, 'Lager hat nicht genug Platz (braucht ' .. newSlots .. ' neue Slots, ' .. (Config.MaxStorageSlots - currentSlots) .. ' frei)', 'error')
return
end
-- Check vault
if shop.vault_money < totalCost then
TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Tresor ($' .. totalCost .. ' benötigt, $' .. shop.vault_money .. ' vorhanden)', 'error')
return
end
-- Deduct from vault (now safe since we checked capacity)
shop.vault_money = shop.vault_money - totalCost
MySQL.query('UPDATE mercyv_shops SET vault_money = vault_money - ? WHERE shop_id = ? AND vault_money >= ?', { totalCost, shopId, totalCost })
-- Add items to storage
for _, item in ipairs(validItems) do
local itemLabel = GetItemLabel(item.name)
if storage[shopId][item.name] then
storage[shopId][item.name].quantity = storage[shopId][item.name].quantity + item.quantity
MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { item.quantity, shopId, item.name })
else
storage[shopId][item.name] = { item_name = item.name, item_label = itemLabel, quantity = item.quantity }
MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', {
shopId, item.name, itemLabel, item.quantity
})
end
end
TriggerClientEvent('mercyv-shops:notify', source, 'Bestellung aufgegeben! $' .. totalCost .. ' vom Tresor abgezogen', 'success')
TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId)
SendWebhook("Nachbestellung", string.format("Owner: **%s**\nShop: **%s**\nKosten: **$%d**\nItems: **%d**", GetPlayerName(source), shop.shop_name, totalCost, #validItems), 3447003)
end)
-- =====================
-- SYNC ON JOIN
-- =====================
RegisterNetEvent('mercyv-shops:requestSync')
AddEventHandler('mercyv-shops:requestSync', function()
TriggerClientEvent('mercyv-shops:syncShops', source, GetSyncData())
end)