from flask import Blueprint, render_template, request, redirect, url_for, jsonify, make_response, after_this_request from app.util import auth, friends, websiteFeatures, transactions, redislock import logging from app.services import economy from app.extensions import db, limiter, csrf, redis_controller, user_limiter from app.models.asset import Asset from app.models.user import User from app.models.userassets import UserAsset from app.models.package_asset import PackageAsset from app.models.place_developer_product import DeveloperProduct from app.models.product_receipt import ProductReceipt from app.models.groups import Group from app.enums.AssetType import AssetType from app.enums.TransactionType import TransactionType from app.pages.catalog.catalog import IncrementAssetCreator, CreateTransactionForSale import math MarketPlaceRoute = Blueprint("marketplace", __name__, url_prefix="/marketplace") EconomyV1Route = Blueprint("economyv1", __name__, url_prefix="/") @MarketPlaceRoute.route("/game-pass-product-info", methods=["GET"]) @MarketPlaceRoute.route("/productinfo", methods=["GET"]) def productinfo(): assetid = request.args.get("assetId") or request.args.get("gamePassId") if assetid is None: return "Invalid request",400 asset : Asset = Asset.query.filter_by(id=assetid).first() if asset is None: return "Asset not found",404 AssetCreator : User | Group = User.query.filter_by(id=asset.creator_id).first() if asset.creator_type == 0 else Group.query.filter_by(id=asset.creator_id).first() if AssetCreator is None: AssetCreatorName = "Unknown" else: AssetCreatorName = AssetCreator.username if isinstance(AssetCreator, User) else AssetCreator.name return jsonify({ "Name": asset.name, "Description": asset.description, "Created": asset.created_at, "Updated": asset.updated_at, "PriceInRobux": asset.price_robux, "PriceInTickets": asset.price_tix, "AssetId": asset.id, "ProductId": asset.id, "AssetTypeId": asset.asset_type.value, "Creator": { "Id": asset.creator_id, "Name": AssetCreatorName, "CreatorType": asset.creator_type }, "MinimumMembershipLevel": 0, "IsForSale": asset.is_for_sale }) @MarketPlaceRoute.route("/productDetails", methods=["GET"]) def productDetails(): productId = request.args.get( key = "productId", default = None, type = int ) if productId is None: return jsonify({ "success": False, "message": "Invalid request" }), 400 TargetDeveloperProduct : DeveloperProduct = DeveloperProduct.query.filter_by( productid = productId ).first() if TargetDeveloperProduct is None: return jsonify({ "success": False, "message": "Invalid request" }), 400 return jsonify({ "TargetId" : 1, "ProductType": "Developer Product", "AssetId": 0, "ProductId": TargetDeveloperProduct.productid, "Name": TargetDeveloperProduct.name, "Description": TargetDeveloperProduct.description, "AssetTypeId": 0, "Creator": { "Id": 0, "Name": None, "CreatorType": None, "CreatorTargetId": 0 }, "IconImageAssetId": TargetDeveloperProduct.iconimage_assetid, "Created": TargetDeveloperProduct.created_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), "Updated": TargetDeveloperProduct.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), "PriceInRobux": TargetDeveloperProduct.robux_price, "PremiumPriceInRobux": 0, "PriceInTickets": 0, "IsNew": False, "IsForSale": TargetDeveloperProduct.is_for_sale, "IsPublicDomain": False, "IsLimited": False, "IsLimitedUnique": False, "Remaining": None, "Sales": None, "MinimumMembershipLevel": 0 }) @MarketPlaceRoute.route("/purchase", methods=["POST"]) @csrf.exempt @auth.authenticated_required_api @limiter.limit("20/minute") @limiter.limit("1/second") @user_limiter.limit("20/minute") @user_limiter.limit("1/second") def inGamePurchaseHandler(): AuthenticatedUser : User = auth.GetCurrentUser() if AuthenticatedUser is None: return jsonify({"success": False, "message": "Unauthorized"}),401 productId = request.form.get("productId", None, int) currencyTypeId = request.form.get("currencyTypeId", None, int) expectedPrice = request.form.get("purchasePrice", None, int) if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"): return jsonify({"success": False, "status": "EconomyDisabled"}),400 if productId is None or currencyTypeId is None or expectedPrice is None: return jsonify({"success": False, "message": "Invalid request"}),400 currencyType = 0 if currencyTypeId == 1 else 1 AssetObj : Asset = Asset.query.filter_by(id=productId).first() if AssetObj is None: return jsonify({"success": False, "message": "Invalid request"}),400 if AssetObj.is_limited or not AssetObj.is_for_sale: return jsonify({"success": False, "status": "NotForSale"}),400 LockAssetName = f"asset:{str(AssetObj.id)}" AssetLock = redislock.acquire_lock(LockAssetName, acquire_timeout=15, lock_timeout=1) if AssetLock is False: return jsonify({"success": False, "status": "InternalServerError"}),500 @after_this_request def release_asset_lock(response): if AssetLock: redislock.release_lock(LockAssetName, AssetLock) UserRobuxBalance, UserTixBalance = economy.GetUserBalance(AuthenticatedUser) if currencyType == 0: if AssetObj.price_robux == 0 and AssetObj.price_tix != 0: return jsonify({"success": False, "status": "CurrencyNotAccepted"}),400 if AssetObj.price_robux != expectedPrice: return jsonify({"success": False, "status": "PriceChanged"}),400 if UserRobuxBalance < expectedPrice: return jsonify({"success": False, "status": "InsufficientFunds"}),400 else: if AssetObj.price_tix == 0 and AssetObj.price_robux != 0: return jsonify({"success": False, "status": "CurrencyNotAccepted"}),400 if AssetObj.price_tix != expectedPrice: return jsonify({"success": False, "status": "PriceChanged"}),400 if UserTixBalance < expectedPrice: return jsonify({"success": False, "status": "InsufficientFunds"}),400 UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=productId).first() if UserAssetObj is not None: return jsonify({"success": False, "status": "AlreadyOwned"}),400 try: economy.DecrementTargetBalance(AuthenticatedUser, expectedPrice, currencyType) except economy.InsufficientFundsException: return jsonify({"success": False, "status": "InsufficientFunds"}),400 UserAssetObj = UserAsset( userid = AuthenticatedUser.id, assetid = productId ) if AssetObj.asset_type == AssetType.Package: PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObj.id).all() for PackageAssetObj in PackageAssets: NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id) db.session.add(NewPackageAssetObj) IncrementAssetCreator(AssetObj, expectedPrice, currencyType) AssetObj.sale_count += 1 db.session.add(UserAssetObj) db.session.commit() try: if AssetObj.creator_type == 0: SellerUserObj : User = User.query.filter_by(id=AssetObj.creator_id).first() CreateTransactionForSale( AssetObj = AssetObj, PurchasePrice = expectedPrice, PurchaseCurrencyType = currencyType, Seller = SellerUserObj, Buyer = AuthenticatedUser, ApplyTaxAutomatically = True ) elif AssetObj.creator_type == 1: SellerGroupObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first() CreateTransactionForSale( AssetObj = AssetObj, PurchasePrice = expectedPrice, PurchaseCurrencyType = currencyType, Seller = SellerGroupObj, Buyer = AuthenticatedUser, ApplyTaxAutomatically = True ) except Exception as e: logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObj.id)}, message: {str(e)}") pass return jsonify({"success": True}),200 @EconomyV1Route.route("/v1/purchases/products/", methods=["POST"]) @csrf.exempt @auth.authenticated_required_api @user_limiter.limit("30/minute") @user_limiter.limit("1/second") def SubmitItemPurchaseEconomy( assetid : int ): AuthenticatedUser : User = auth.GetCurrentUser() if AuthenticatedUser is None: return jsonify({"purchased": False, "reason": "Unauthorized"}),401 if "expectedPrice" not in request.json or "expectedCurrency" not in request.json: return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400 productId = assetid currencyTypeId = request.json["expectedCurrency"] expectedPrice = request.json["expectedPrice"] if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"): return jsonify({"purchased": False, "reason": "EconomyDisabled", "errorMsg": "Purchasing is temporarily disabled, try again later."}),400 if productId is None or currencyTypeId is None or expectedPrice is None: return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400 currencyType = 0 if currencyTypeId == 1 else 1 AssetObj : Asset = Asset.query.filter_by(id=productId).first() if AssetObj is None: return jsonify({"purchased": False, "reason": "Invalid request", "errorMsg": "Invalid Request"}),400 if AssetObj.is_limited or not AssetObj.is_for_sale: return jsonify({"purchased": False, "reason": "NotForSale", "errorMsg": "This item is not for sale"}),400 LockAssetName = f"asset:{str(AssetObj.id)}" AssetLock = redislock.acquire_lock(LockAssetName, acquire_timeout=15, lock_timeout=1) if AssetLock is False: return jsonify({"success": False, "status": "InternalServerError"}),500 @after_this_request def release_asset_lock(response): if AssetLock: redislock.release_lock(LockAssetName, AssetLock) UserRobuxBalance, UserTixBalance = economy.GetUserBalance(AuthenticatedUser) if currencyType == 0: if AssetObj.price_robux == 0 and AssetObj.price_tix != 0: return jsonify({"purchased": False, "reason": "CurrencyNotAccepted", "errorMsg": "This currency is not accepted for this item"}),400 if AssetObj.price_robux != expectedPrice: return jsonify({"purchased": False, "reason": "PriceChanged", "errorMsg": "The price has changed"}),400 if UserRobuxBalance < expectedPrice: return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400 else: if AssetObj.price_tix == 0 and AssetObj.price_robux != 0: return jsonify({"purchased": False, "reason": "CurrencyNotAccepted", "errorMsg": "This currency is not accepted for this item"}),400 if AssetObj.price_tix != expectedPrice: return jsonify({"purchased": False, "reason": "PriceChanged", "errorMsg": "The price has changed"}),400 if UserTixBalance < expectedPrice: return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400 # Check if the user already owns the asset UserAssetObj : UserAsset = UserAsset.query.filter_by(userid=AuthenticatedUser.id, assetid=productId).first() if UserAssetObj is not None: return jsonify({"purchased": False, "reason": "AlreadyOwned", "errorMsg": "You already own this item."}),400 try: economy.DecrementTargetBalance(AuthenticatedUser, expectedPrice, currencyType) except economy.InsufficientFundsException: return jsonify({"purchased": False, "reason": "InsufficientFunds", "errorMsg": "You do not have enough Robux to purchase this item."}),400 UserAssetObj = UserAsset( userid = AuthenticatedUser.id, assetid = productId ) if AssetObj.asset_type == AssetType.Package: PackageAssets = PackageAsset.query.filter_by(package_asset_id=AssetObj.id).all() for PackageAssetObj in PackageAssets: NewPackageAssetObj = UserAsset(userid=AuthenticatedUser.id, assetid=PackageAssetObj.asset_id) db.session.add(NewPackageAssetObj) IncrementAssetCreator(AssetObj, expectedPrice, currencyType) AssetObj.sale_count += 1 db.session.add(UserAssetObj) db.session.commit() try: if AssetObj.creator_type == 0: SellerUserObj : User = User.query.filter_by(id=AssetObj.creator_id).first() CreateTransactionForSale( AssetObj = AssetObj, PurchasePrice = expectedPrice, PurchaseCurrencyType = currencyType, Seller = SellerUserObj, Buyer = AuthenticatedUser, ApplyTaxAutomatically = True ) elif AssetObj.creator_type == 1: SellerGroupObj : Group = Group.query.filter_by(id=AssetObj.creator_id).first() CreateTransactionForSale( AssetObj = AssetObj, PurchasePrice = expectedPrice, PurchaseCurrencyType = currencyType, Seller = SellerGroupObj, Buyer = AuthenticatedUser, ApplyTaxAutomatically = True ) except Exception as e: logging.warn(f"Failed to create transaction log for sale of asset {str(AssetObj.id)}, message: {str(e)}") pass if AssetObj.creator_type == 0: CreatorObj : User = User.query.filter_by(id = AssetObj.creator_id).first() sellerName = CreatorObj.username else: CreatorObj : Group = Group.query.filter_by(id = AssetObj.creator_id).first() sellerName = CreatorObj.name return jsonify({ "purchased": True, "reason": "Success", "productId": assetid, "currency": currencyTypeId, "assetId": assetid, "assetName": AssetObj.name, "assetType": AssetObj.asset_type.name, "assetTypeDisplayName": AssetObj.asset_type.name, "assetIsWearable": False, "sellerName": sellerName, "transactionVerb": "bought", "isMultiPrivateSale": False }) @MarketPlaceRoute.route("/submitpurchase", methods=["POST"]) @csrf.exempt @auth.authenticated_required_api @user_limiter.limit("20/minute") @user_limiter.limit("1/second") def SubmitProductPurchase(): productId : int = request.form.get( key = "productId", default = None, type = int ) currencyTypeId : int = request.form.get( key = "currencyTypeId", default = None, type = int ) expectedUnitPrice : int = request.form.get( key = "expectedUnitPrice", default = None, type = int ) placeId : int = request.form.get( key = "placeId", default = None, type = int ) requestId : str = request.form.get( key = "requestId", default = None, type = str ) if not websiteFeatures.GetWebsiteFeature("EconomyPurchase"): return jsonify({"success": False, "status": "EconomyDisabled"}),400 if productId is None or currencyTypeId is None or expectedUnitPrice is None or placeId is None or requestId is None: return jsonify({"success": False, "message": "Invalid request"}),400 currencyType = 0 if currencyTypeId == 1 else 1 TargetDeveloperProduct : DeveloperProduct = DeveloperProduct.query.filter_by( productid = productId, placeid = placeId ).first() if TargetDeveloperProduct is None: return jsonify({"success": False, "message": "Invalid request"}),400 PlaceAssetObj : Asset = Asset.query.filter_by( id = TargetDeveloperProduct.placeid ).first() if TargetDeveloperProduct.is_for_sale == False: return jsonify({"success": False, "message": "Invalid request"}),400 if TargetDeveloperProduct.robux_price != expectedUnitPrice: return jsonify({"success": False, "message": "Invalid request"}),400 if redis_controller.exists(f"purchase_request:{requestId}"): return jsonify({"success": False, "message": "Invalid request"}),400 if currencyType != 0: # Developer Products can only be purchased with Robux return jsonify({"success": False, "message": "Invalid request"}),400 AuthenticatedUser : User = auth.GetCurrentUser() if AuthenticatedUser is None: return jsonify({"success": False, "message": "Unauthorized"}),401 UserRobuxBalance, _ = economy.GetUserBalance(AuthenticatedUser) if UserRobuxBalance < expectedUnitPrice: return jsonify({"success": False, "status": "InsufficientFunds"}),400 try: economy.DecrementTargetBalance(AuthenticatedUser, expectedUnitPrice, currencyType) except economy.InsufficientFundsException: return jsonify({"success": False, "status": "InsufficientFunds"}),400 OwnerObj : User | Group = User.query.filter_by( id = PlaceAssetObj.creator_id ).first() if PlaceAssetObj.creator_type == 0 else Group.query.filter_by( id = PlaceAssetObj.creator_id ).first() transactions.CreateTransaction( Reciever = OwnerObj, Sender = AuthenticatedUser, CurrencyAmount = expectedUnitPrice, CurrencyType = currencyType, TransactionType = TransactionType.Purchase, CustomText = f"Purchase of Product({TargetDeveloperProduct.name}) from {PlaceAssetObj.name}" ) GrossProfitAfterTax = math.floor(expectedUnitPrice * 0.7) economy.IncrementTargetBalance(OwnerObj, GrossProfitAfterTax, currencyType) transactions.CreateTransaction( Reciever = OwnerObj, Sender = AuthenticatedUser, CurrencyAmount = GrossProfitAfterTax, CurrencyType = currencyType, TransactionType = TransactionType.Sale, CustomText = f"Sale of Product({TargetDeveloperProduct.name}) from {PlaceAssetObj.name}" ) redis_controller.set(f"purchase_request:{requestId}", "1", ex = 60 * 10) DeveloperProductReceiptObj : ProductReceipt = ProductReceipt( user_id = AuthenticatedUser.id, product_id = TargetDeveloperProduct.productid, robux_amount = expectedUnitPrice ) TargetDeveloperProduct.sales_count += 1 db.session.add(DeveloperProductReceiptObj) db.session.commit() return jsonify({ "success": True, "receipt": DeveloperProductReceiptObj.receipt_id }),200