local nk = require("nakama") local utils = require("lua.utils") local economy = {} local SHOP_CATALOG_DEFS = { { id = "oldpop-blue-hat", name = "Oldpop Blue Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-green-hat", name = "Oldpop Green Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-red-hat", name = "Oldpop Red Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-yellow-hat", name = "Oldpop Yellow Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-og-pant", name = "Copper OG Pant", category = "costume", gold = 0, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-grey-pant", name = "Copper Grey Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-red-pant", name = "Copper Red Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-yellow-pant", name = "Copper Yellow Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-blue-gloves", name = "Oldpop Blue Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-green-gloves", name = "Oldpop Green Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-red-gloves", name = "Oldpop Red Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" }, { id = "oldpop-yellow-gloves", name = "Oldpop Yellow Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" } } local function build_shop_catalog() local catalog = {} for _, def in ipairs(SHOP_CATALOG_DEFS) do local cat = def.category if not catalog[cat] then catalog[cat] = {} end local entry = { id = def.id, name = def.name, gold = def.gold or 0, star = def.star or 0, rarity = def.rarity or "Common", character = def.character } table.insert(catalog[cat], entry) end return catalog end function economy.rpc_get_shop_catalog(context, payload) if not context.user_id then error("Not authenticated") end local result = { catalog = build_shop_catalog(), featured_banners = {} } local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }}) if status and objs and #objs > 0 then local val = objs[1].value if val.banners then result.featured_banners = val.banners end end return nk.json_encode(result) end function economy.rpc_buy_currency(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload) local packageId = request.package_id local receipt = request.receipt local idempotencyKey = request.idempotency_key if not packageId or packageId == "" then error("Package ID required") end if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }}) if status and existing and #existing > 0 then return nk.json_encode({ success = true, package_id = packageId, duplicate = true, status = existing[1].value.status }) end local changeset = { gold = 0, star = 0 } local requiresVerification = false if packageId == "gold_100" then changeset.gold = 100; requiresVerification = true elseif packageId == "gold_500" then changeset.gold = 550; requiresVerification = true elseif packageId == "gold_1000" then changeset.gold = 1150; requiresVerification = true elseif packageId == "gold_2000" then changeset.gold = 2400; requiresVerification = true elseif packageId == "gold_5000" then changeset.gold = 6250; requiresVerification = true elseif packageId == "gold_10000" then changeset.gold = 13000; requiresVerification = true elseif packageId == "star_100" then changeset.star = 100; changeset.gold = -500 elseif packageId == "star_250" then changeset.star = 250; changeset.gold = -1100 elseif packageId == "star_600" then changeset.star = 600; changeset.gold = -2500 else error("Invalid package ID") end if requiresVerification then if not receipt or receipt == "" then nk.storage_write({{ collection = "receipts", key = idempotencyKey, user_id = context.user_id, value = { type = "currency", package_id = packageId, status = "pending", created_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") }, permission_read = 1, permission_write = 0 }}) return nk.json_encode({ success = true, status = "pending", package_id = packageId }) end local is_valid = false local store_type = request.store_type or "test" if store_type == "google" then local s, response = pcall(nk.purchase_validate_google, context.user_id, receipt) if s and response.success then is_valid = true end elseif store_type == "apple" then local s, response = pcall(nk.purchase_validate_apple, context.user_id, receipt, "") if s and response.success then is_valid = true end elseif store_type == "test" and receipt == "mock_receipt_for_now" then is_valid = true end if not is_valid then nk.logger_warn("Invalid IAP receipt submitted: " .. tostring(receipt)) error("InvalidReceipt") end end local s, err = pcall(function() if changeset.gold ~= 0 or changeset.star ~= 0 then nk.wallet_update(context.user_id, changeset, {}, true) end nk.storage_write({{ collection = "receipts", key = idempotencyKey, user_id = context.user_id, value = { type = "currency", package_id = packageId, changeset = changeset, receipt = receipt or nk.json_null(), status = "verified", processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") }, permission_read = 1, permission_write = 0 }}) end) if not s then nk.logger_error("Currency purchase failed: " .. tostring(err)) error("NotEnoughFunds") end nk.logger_info("User " .. context.user_id .. " bought currency package " .. packageId) return nk.json_encode({ success = true, status = "verified", package_id = packageId }) end function economy.rpc_purchase_item(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload) local itemId = request.item_id local quantity = request.quantity or 1 local idempotencyKey = request.idempotency_key if not itemId or itemId == "" then error("Item ID required") end if quantity < 1 then error("Invalid quantity") end if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }}) if status and existing and #existing > 0 then return nk.json_encode({ success = true, item = itemId, duplicate = true }) end local itemDef = nil for _, def in ipairs(SHOP_CATALOG_DEFS) do if def.id == itemId then itemDef = def break end end if not itemDef then error("ItemNotFound") end local priceGold = (itemDef.gold or 0) * quantity local priceStar = (itemDef.star or 0) * quantity local category = itemDef.category or "accessory" local s, err = pcall(function() local changeset = {} if priceGold > 0 then changeset.gold = -priceGold end if priceStar > 0 then changeset.star = -priceStar end if priceGold > 0 or priceStar > 0 then nk.wallet_update(context.user_id, changeset, {}, true) end end) if not s then nk.logger_error("Wallet update failed: " .. tostring(err)) error("NotEnoughFunds") end local s2, err2 = pcall(function() local writes = { { collection = "inventory", key = itemId, user_id = context.user_id, value = { category = category, purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z"), quantity = quantity }, permission_read = 1, permission_write = 0 }, { collection = "receipts", key = idempotencyKey, user_id = context.user_id, value = { type = "item", item_id = itemId, quantity = quantity, cost = { gold = priceGold, star = priceStar }, processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") }, permission_read = 1, permission_write = 0 } } nk.storage_write(writes) end) if not s2 then nk.logger_error("Purchase failed: " .. tostring(err2)) error("PurchaseFailed") end nk.logger_info("User " .. context.user_id .. " purchased " .. itemId) return nk.json_encode({ success = true, item = itemId }) end function economy.rpc_admin_set_featured_banners(context, payload) utils.require_admin(context) local req = nk.json_decode(payload or "{}") local banners = req.banners or {} local finalBanners = {} for i = 1, math.min(#banners, 3) do table.insert(finalBanners, banners[i]) end for _, b in ipairs(finalBanners) do local itemId = b.item_id or "" if itemId ~= "" then local found = false for _, def in ipairs(SHOP_CATALOG_DEFS) do if def.id == itemId then found = true; break end end if not found then error("Item not found in catalog: " .. itemId) end end end nk.storage_write({{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000", value = { banners = finalBanners }, permission_read = 2, permission_write = 0 }}) nk.logger_info("Featured banners updated by admin " .. context.user_id) return nk.json_encode({ success = true, banners = finalBanners }) end function economy.rpc_admin_get_featured_banners(context, payload) utils.require_admin(context) local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }}) if status and objs and #objs > 0 then return nk.json_encode({ banners = objs[1].value.banners or {} }) end return nk.json_encode({ banners = {} }) end nk.register_rpc(economy.rpc_get_shop_catalog, "get_shop_catalog") nk.register_rpc(economy.rpc_buy_currency, "buy_currency") nk.register_rpc(economy.rpc_purchase_item, "purchase_item") nk.register_rpc(economy.rpc_admin_set_featured_banners, "admin_set_featured_banners") nk.register_rpc(economy.rpc_admin_get_featured_banners, "admin_get_featured_banners") nk.logger_info("LUA TEST: economy module loaded successfully") return economy