diff --git a/.env.example b/.env.example index d058c34ef2..458905c8ad 100644 --- a/.env.example +++ b/.env.example @@ -41,4 +41,4 @@ PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" -MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" \ No newline at end of file diff --git a/app/Console/Commands/AddSiteSettings.php b/app/Console/Commands/AddSiteSettings.php index ca71ea6ee4..91f8a0cd4f 100644 --- a/app/Console/Commands/AddSiteSettings.php +++ b/app/Console/Commands/AddSiteSettings.php @@ -83,6 +83,12 @@ public function handle() { $this->addSiteSetting('comment_dislikes_enabled', 0, '0: Dislikes disabled, 1: Dislikes enabled.'); + $this->addSiteSetting('shop_type', 0, '0: Default, 1: Collapsible.'); + + $this->addSiteSetting('coupon_settings', 0, '0: Percentage is taken from total (e.g 20% from 2 items costing a total of 100 = 80), 1: Percentage is taken from item (e.g 20% from 2 items costing a total of 100 = 90)'); + + $this->addSiteSetting('limited_stock_coupon_settings', 0, '0: Does not allow coupons to be used on limited stock items, 1: Allows coupons to be used on limited stock items'); + $this->addSiteSetting('can_transfer_currency_directly', 1, 'Whether or not users can directly transfer currency to other users without trading. 0: Users cannot directly transfer currency. 1: Direct currency transfers are allowed.'); $this->addSiteSetting('can_transfer_items_directly', 1, 'Whether or not users can directly transfer items to other users without trading. 0: Users cannot directly transfer items. 1: Direct item transfers are allowed.'); diff --git a/app/Console/Commands/ConvertShopLimits.php b/app/Console/Commands/ConvertShopLimits.php new file mode 100644 index 0000000000..2cdde0dd14 --- /dev/null +++ b/app/Console/Commands/ConvertShopLimits.php @@ -0,0 +1,58 @@ +info('No shop limits to convert.'); + + return; + } + + $shopLimits = DB::table('shop_limits')->get(); + $bar = $this->output->createProgressBar(count($shopLimits)); + $bar->start(); + foreach ($shopLimits as $shopLimit) { + Limit::create([ + 'object_model' => 'App\Models\Shop\Shop', + 'object_id' => $shopLimit->shop_id, + 'limit_type' => 'item', + 'limit_id' => $shopLimit->item_id, + 'quantity' => 1, + ]); + + $bar->advance(); + } + $bar->finish(); + + // drop the is_restricted column from the shops table + Schema::table('shops', function ($table) { + $table->dropColumn('is_restricted'); + }); + + Schema::dropIfExists('shop_limits'); + } +} diff --git a/app/Console/Commands/RestockShops.php b/app/Console/Commands/RestockShops.php new file mode 100644 index 0000000000..cb7b6abacf --- /dev/null +++ b/app/Console/Commands/RestockShops.php @@ -0,0 +1,81 @@ +where('restock', 1)->get(); + foreach ($stocks as $stock) { + if ($stock->restock_interval == 2) { + // check if it's start of week + $now = Carbon::now(); + $day = $now->dayOfWeek; + if ($day != 1) { + continue; + } + } elseif ($stock->restock_interval == 3) { + // check if it's start of month + $now = Carbon::now(); + $day = $now->day; + if ($day != 1) { + continue; + } + } + + // if the stock is random, restock from the stock type + if ($stock->isRandom) { + $type = $stock->stock_type; + $model = getAssetModelString(strtolower($type)); + if (method_exists($model, 'visible')) { + $itemId = $stock->categoryId ? + $model::visible()->where(strtolower($type).'_category_id', $stock->categoryId)->inRandomOrder()->first()->id : + $model::visible()->inRandomOrder()->first()->id; + } elseif (method_exists($model, 'released')) { + $itemId = $stock->categoryId ? + $model::released()->where(strtolower($type).'_category_id', $stock->categoryId)->inRandomOrder()->first()->id : + $model::released()->inRandomOrder()->first()->id; + } else { + $itemId = $stock->categoryId ? + $model::where(strtolower($type).'_category_id', $stock->categoryId)->inRandomOrder()->first()->id : + $model::inRandomOrder()->first()->id; + } + + $stock->item_id = $itemId; + $stock->save(); + } + + $stock->quantity = $stock->range ? mt_rand(1, $stock->restock_quantity) : $stock->restock_quantity; + $stock->save(); + } + } +} diff --git a/app/Console/Commands/UpdateTimedStock.php b/app/Console/Commands/UpdateTimedStock.php new file mode 100644 index 0000000000..0443446b03 --- /dev/null +++ b/app/Console/Commands/UpdateTimedStock.php @@ -0,0 +1,74 @@ +where('is_visible', 1)->get()->filter(function ($stock) { + return !$stock->isActive; + }); + $showstock = ShopStock::where('is_timed_stock', 1)->where('is_visible', 0)->get()->filter(function ($stock) { + return $stock->isActive; + }); + + // set stock that should be active to active + foreach ($showstock as $showstock) { + $showstock->is_visible = 1; + $showstock->save(); + } + // hide stock that should be hidden now + foreach ($hidestock as $hidestock) { + $hidestock->is_visible = 0; + $hidestock->save(); + } + + // also activate or deactivate the shops + $hideshop = Shop::where('is_timed_shop', 1)->where('is_active', 1)->get()->filter(function ($shop) { + return !$shop->isActive; + }); + $showshop = Shop::where('is_timed_shop', 1)->where('is_active', 0)->get()->filter(function ($shop) { + return $shop->isActive; + }); + + // set shop that should be active to active + foreach ($showshop as $showshop) { + $showshop->is_active = 1; + $showshop->save(); + } + // hide shop that should be hidden now + foreach ($hideshop as $hideshop) { + $hideshop->is_active = 0; + $hideshop->save(); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a8bae72c58..053f4bd595 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -29,6 +29,10 @@ protected function schedule(Schedule $schedule) { ->daily(); $schedule->command('update-staff-reward-actions') ->daily(); + $schedule->command('restock-shops') + ->daily(); + $schedule->command('update-timed-stock') + ->everyMinute(); } /** diff --git a/app/Helpers/AssetHelpers.php b/app/Helpers/AssetHelpers.php index c870b91f35..7d3a6f1d15 100644 --- a/app/Helpers/AssetHelpers.php +++ b/app/Helpers/AssetHelpers.php @@ -89,7 +89,7 @@ function getAssetKeys($isCharacter = false) { */ function getAssetModelString($type, $namespaced = true) { switch ($type) { - case 'items': + case 'items': case 'item': if ($namespaced) { return '\App\Models\Item\Item'; } else { @@ -97,7 +97,7 @@ function getAssetModelString($type, $namespaced = true) { } break; - case 'currencies': + case 'currencies': case 'currency': if ($namespaced) { return '\App\Models\Currency\Currency'; } else { @@ -273,6 +273,42 @@ function parseAssetData($array) { return $assets; } +/** + * Returns if two asset arrays are identical. + * + * @param array $first + * @param array $second + * @param mixed $isCharacter + * @param mixed $absQuantities + * + * @return bool + */ +function compareAssetArrays($first, $second, $isCharacter = false, $absQuantities = false) { + $keys = getAssetKeys($isCharacter); + foreach ($keys as $key) { + if (count($first[$key]) != count($second[$key])) { + return false; + } + foreach ($first[$key] as $id => $asset) { + if (!isset($second[$key][$id])) { + return false; + } + + if ($absQuantities) { + if (abs($asset['quantity']) != abs($second[$key][$id]['quantity'])) { + return false; + } + } else { + if ($asset['quantity'] != $second[$key][$id]['quantity']) { + return false; + } + } + } + } + + return true; +} + /** * Distributes the assets in an assets array to the given recipient (user). * Loot tables will be rolled before distribution. @@ -299,20 +335,42 @@ function fillUserAssets($assets, $sender, $recipient, $logType, $data) { $service = new App\Services\InventoryManager; foreach ($contents as $asset) { if (!$service->creditItem($sender, $recipient, $logType, $data, $asset['asset'], $asset['quantity'])) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + return false; } } } elseif ($key == 'currencies' && count($contents)) { $service = new App\Services\CurrencyManager; foreach ($contents as $asset) { - if (!$service->creditCurrency($sender, $recipient, $logType, $data['data'], $asset['asset'], $asset['quantity'])) { - return false; + if ($asset['quantity'] < 0) { + if (!$service->debitCurrency($sender, $recipient, $logType, $data['data'], $asset['asset'], abs($asset['quantity']))) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return false; + } + } else { + if (!$service->creditCurrency($sender, $recipient, $logType, $data['data'], $asset['asset'], $asset['quantity'])) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + + return false; + } } } } elseif ($key == 'raffle_tickets' && count($contents)) { $service = new App\Services\RaffleManager; foreach ($contents as $asset) { if (!$service->addTicket($recipient, $asset['asset'], $asset['quantity'])) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + return false; } } @@ -320,6 +378,10 @@ function fillUserAssets($assets, $sender, $recipient, $logType, $data) { $service = new App\Services\InventoryManager; foreach ($contents as $asset) { if (!$service->moveStack($sender, $recipient, $logType, $data, $asset['asset'])) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + return false; } } @@ -327,6 +389,10 @@ function fillUserAssets($assets, $sender, $recipient, $logType, $data) { $service = new App\Services\CharacterManager; foreach ($contents as $asset) { if (!$service->moveCharacter($asset['asset'], $recipient, $data, $asset['quantity'], $logType)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + return false; } } @@ -336,6 +402,24 @@ function fillUserAssets($assets, $sender, $recipient, $logType, $data) { return $assets; } +/** + * Returns the total count of all assets in an asset array. + * + * @param array $array + * + * @return int + */ +function countAssets($array) { + $count = 0; + foreach ($array as $key => $contents) { + foreach ($contents as $asset) { + $count += $asset['quantity']; + } + } + + return $count; +} + /** * Distributes the assets in an assets array to the given recipient (character). * Loot tables will be rolled before distribution. @@ -389,14 +473,22 @@ function fillCharacterAssets($assets, $sender, $recipient, $logType, $data, $sub * Creates a rewards string from an asset array. * * @param array $array + * @param mixed $useDisplayName + * @param mixed $absQuantities * * @return string */ -function createRewardsString($array) { +function createRewardsString($array, $useDisplayName = true, $absQuantities = false) { $string = []; foreach ($array as $key => $contents) { foreach ($contents as $asset) { - $string[] = $asset['asset']->displayName.' x'.$asset['quantity']; + if ($useDisplayName) { + $name = $asset['asset']->displayName ?? ($asset['asset']->name ?? 'Deleted '.ucfirst(str_replace('_', ' ', $key))); + $string[] = $name.' x'.($absQuantities ? abs($asset['quantity']) : $asset['quantity']); + } else { + $name = $asset['asset']->name ?? 'Deleted '.ucfirst(str_replace('_', ' ', $key)); + $string[] = $name.' x'.($absQuantities ? abs($asset['quantity']) : $asset['quantity']); + } } } if (!count($string)) { diff --git a/app/Helpers/Helpers.php b/app/Helpers/Helpers.php index b3507768a4..f0ac04e6ca 100644 --- a/app/Helpers/Helpers.php +++ b/app/Helpers/Helpers.php @@ -449,6 +449,17 @@ function prettyProfileName($url) { } } +/** + * Returns the given objects limits, if any. + * + * @param mixed $object + * + * @return bool + */ +function getLimits($object) { + return App\Models\Limit\Limit::where('object_model', get_class($object))->where('object_id', $object->id)->get(); +} + /** * Checks the site setting and returns the appropriate FontAwesome version. * diff --git a/app/Http/Controllers/Admin/Data/LimitController.php b/app/Http/Controllers/Admin/Data/LimitController.php new file mode 100644 index 0000000000..b3df37711e --- /dev/null +++ b/app/Http/Controllers/Admin/Data/LimitController.php @@ -0,0 +1,141 @@ + DynamicLimit::get(), + ]); + } + + /** + * Shows the create limit page. + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getCreateLimit() { + return view('admin.limits.create_edit_limit', [ + 'limit' => new DynamicLimit, + ]); + } + + /** + * Shows the edit limit page. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getEditLimit($id) { + $limit = DynamicLimit::find($id); + if (!$limit) { + abort(404); + } + + return view('admin.limits.create_edit_limit', [ + 'limit' => $limit, + ]); + } + + /** + * Creates or edits a limit. + * + * @param App\Services\LimitService $service + * @param int|null $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postCreateEditLimit(Request $request, LimitService $service, $id = null) { + $data = $request->only([ + 'name', 'description', 'evaluation', + ]); + if ($id && $service->updateLimit(DynamicLimit::find($id), $data, Auth::user())) { + flash('Limit updated successfully.')->success(); + } elseif (!$id && $limit = $service->createLimit($data, Auth::user())) { + flash('Limit created successfully.')->success(); + + return redirect()->to('admin/data/limits/edit/'.$limit->id); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } + + /** + * Gets the limit deletion modal. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getDeleteLimit($id) { + $limit = DynamicLimit::find($id); + + return view('admin.limits._delete_limit', [ + 'limit' => $limit, + ]); + } + + /** + * Deletes a limit. + * + * @param App\Services\LimitService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postDeleteLimit(Request $request, LimitService $service, $id) { + if ($id && $service->deleteLimit(DynamicLimit::find($id))) { + flash('Limit deleted successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->to('admin/data/limits'); + } + + /** + * Sorts limits. + * + * @param App\Services\LimitService $service + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postSortLimit(Request $request, LimitService $service) { + if ($service->sortLimit($request->get('sort'))) { + flash('Limit order updated successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/Admin/Data/ShopController.php b/app/Http/Controllers/Admin/Data/ShopController.php index f15a769f07..c726f4a524 100644 --- a/app/Http/Controllers/Admin/Data/ShopController.php +++ b/app/Http/Controllers/Admin/Data/ShopController.php @@ -6,6 +6,7 @@ use App\Models\Currency\Currency; use App\Models\Item\Item; use App\Models\Shop\Shop; +use App\Models\Shop\ShopStock; use App\Services\ShopService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -37,8 +38,15 @@ public function getIndex() { * @return \Illuminate\Contracts\Support\Renderable */ public function getCreateShop() { + // get all items where they have a tag 'coupon' + $coupons = Item::whereHas('tags', function ($query) { + $query->where('tag', 'coupon')->where('is_active', 1); + })->orderBy('name')->pluck('name', 'id'); + return view('admin.shops.create_edit_shop', [ - 'shop' => new Shop, + 'shop' => new Shop, + 'items' => Item::orderBy('name')->pluck('name', 'id'), + 'coupons' => $coupons, ]); } @@ -55,10 +63,16 @@ public function getEditShop($id) { abort(404); } + // get all items where they have a tag 'coupon' + $coupons = Item::released()->whereHas('tags', function ($query) { + $query->where('tag', 'coupon'); + })->orderBy('name')->pluck('name', 'id'); + return view('admin.shops.create_edit_shop', [ 'shop' => $shop, 'items' => Item::orderBy('name')->pluck('name', 'id'), 'currencies' => Currency::orderBy('name')->pluck('name', 'id'), + 'coupons' => $coupons, ]); } @@ -73,7 +87,8 @@ public function getEditShop($id) { public function postCreateEditShop(Request $request, ShopService $service, $id = null) { $id ? $request->validate(Shop::$updateRules) : $request->validate(Shop::$createRules); $data = $request->only([ - 'name', 'description', 'image', 'remove_image', 'is_active', + 'name', 'description', 'image', 'remove_image', 'is_active', 'is_staff', 'use_coupons', 'is_fto', 'allowed_coupons', 'is_timed_shop', 'start_at', 'end_at', + 'is_hidden', 'shop_days', 'shop_months', ]); if ($id && $service->updateShop(Shop::find($id), $data, Auth::user())) { flash('Shop updated successfully.')->success(); @@ -90,6 +105,107 @@ public function postCreateEditShop(Request $request, ShopService $service, $id = return redirect()->back(); } + /** + * loads the create stock modal. + * + * @param mixed $id + */ + public function getCreateShopStock($id) { + $shop = Shop::find($id); + if (!$shop) { + abort(404); + } + + return view('admin.shops._stock_modal', [ + 'shop' => $shop, + 'currencies' => Currency::orderBy('name')->pluck('name', 'id'), + 'stock' => new ShopStock, + ]); + } + + /** + * loads the edit stock modal. + * + * @param mixed $id + */ + public function getEditShopStock($id) { + $stock = ShopStock::find($id); + if (!$stock) { + abort(404); + } + // get base modal from type using asset helper + $type = $stock->stock_type; + $model = getAssetModelString(strtolower($type)); + + // check if categories exist for this model ($model.'Category') + $categoryClass = $model.'Category'; + if (class_exists($categoryClass)) { + // map the categories to be name-id + $categories = $categoryClass::orderBy('name')->get()->mapWithKeys(function ($category) { + return [$category->id.'-category' => $category->name]; + }); + $items = [ + $type => $model::orderBy('name')->pluck('name', 'id')->toArray() + ['random' => 'Random '.$type], + $type.'Category' => $categories->toArray(), + ]; + } else { + $items = $model::orderBy('name')->pluck('name', 'id')->toArray(); + } + + return view('admin.shops._stock_modal', [ + 'shop' => $stock->shop, + 'stock' => $stock, + 'items' => $items, + ]); + } + + /** + * gets stock of a certain type. + */ + public function getShopStockType(Request $request) { + $type = $request->input('type'); + if (!$type) { + return null; + } + // get base modal from type using asset helper + $model = getAssetModelString(strtolower($type)); + + // check if categories exist for this model ($model.'Category') + $categoryClass = $model.'Category'; + if (class_exists($categoryClass)) { + // map the categories to be name-id + $categories = $categoryClass::orderBy('name')->get()->mapWithKeys(function ($category) { + return [$category->id.'-category' => $category->name]; + }); + $items = [ + $type => $model::orderBy('name')->pluck('name', 'id')->toArray() + ['random' => 'Random '.$type], + $type.'Category' => $categories->toArray(), + ]; + } else { + $items = $model::orderBy('name')->pluck('name', 'id')->toArray(); + } + + return view('admin.shops._stock_item', [ + 'items' => $items, + ]); + } + + /** + * gets the type of a cost for a stock. + */ + public function getShopStockCostType(Request $request) { + $type = $request->input('type'); + if (!$type) { + return null; + } + // get base modal from type using asset helper + $model = getAssetModelString(strtolower($type)); + + return view('admin.shops._stock_cost', [ + 'costItems' => $model::orderBy('name')->pluck('name', 'id'), + ]); + } + /** * Edits a shop's stock. * @@ -100,9 +216,11 @@ public function postCreateEditShop(Request $request, ShopService $service, $id = */ public function postEditShopStock(Request $request, ShopService $service, $id) { $data = $request->only([ - 'shop_id', 'item_id', 'currency_id', 'cost', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', + 'shop_id', 'item_id', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', 'purchase_limit_timeframe', 'is_fto', 'stock_type', 'is_visible', + 'restock', 'restock_quantity', 'restock_interval', 'range', 'disallow_transfer', 'is_timed_stock', 'stock_start_at', 'stock_end_at', + 'cost_type', 'cost_quantity', 'cost_id', 'group', 'can_group_use_coupon', 'stock_days', 'stock_months', ]); - if ($service->updateShopStock(Shop::find($id), $data, Auth::user())) { + if ($service->editShopStock(ShopStock::find($id), $data, Auth::user())) { flash('Shop stock updated successfully.')->success(); return redirect()->back(); @@ -115,6 +233,70 @@ public function postEditShopStock(Request $request, ShopService $service, $id) { return redirect()->back(); } + /** + * Edits a shop's stock. + * + * @param App\Services\ShopService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postCreateShopStock(Request $request, ShopService $service, $id) { + $data = $request->only([ + 'shop_id', 'item_id', 'currency_id', 'cost', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', 'purchase_limit_timeframe', 'is_fto', 'stock_type', 'is_visible', + 'restock', 'restock_quantity', 'restock_interval', 'range', 'disallow_transfer', 'is_timed_stock', 'stock_start_at', 'stock_end_at', + 'cost_type', 'cost_quantity', 'cost_id', 'group', 'stock_days', 'stock_months', + ]); + if ($service->createShopStock(Shop::find($id), $data, Auth::user())) { + flash('Shop stock updated successfully.')->success(); + + return redirect()->back(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } + + /** + * Gets the stock deletion modal. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getDeleteShopStock($id) { + $stock = ShopStock::find($id); + + return view('admin.shops._delete_stock', [ + 'stock' => $stock, + ]); + } + + /** + * Deletes a stock. + * + * @param App\Services\StockService $service + * @param int $id + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postDeleteShopStock(Request $request, ShopService $service, $id) { + $stock = ShopStock::find($id); + $shop = $stock->shop; + if ($id && $service->deleteStock($stock)) { + flash('Stock deleted successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->to('admin/data/shops/edit/'.$shop->id); + } + /** * Gets the shop deletion modal. * diff --git a/app/Http/Controllers/Admin/LimitController.php b/app/Http/Controllers/Admin/LimitController.php new file mode 100644 index 0000000000..955670a200 --- /dev/null +++ b/app/Http/Controllers/Admin/LimitController.php @@ -0,0 +1,32 @@ +only([ + 'object_model', 'object_id', 'limit_type', 'limit_id', 'quantity', 'debit', 'is_unlocked', + ]); + if ($service->editLimits($data['object_model'], $data['object_id'], $data, Auth::user())) { + flash('Limits updated successfully.')->success(); + } else { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + } + + return redirect()->back(); + } +} diff --git a/app/Http/Controllers/ShopController.php b/app/Http/Controllers/ShopController.php index 6807d0c48e..2f85effe61 100644 --- a/app/Http/Controllers/ShopController.php +++ b/app/Http/Controllers/ShopController.php @@ -2,13 +2,13 @@ namespace App\Http\Controllers; -use App\Models\Currency\Currency; use App\Models\Item\Item; -use App\Models\Item\ItemCategory; +use App\Models\Item\ItemTag; use App\Models\Shop\Shop; use App\Models\Shop\ShopLog; use App\Models\Shop\ShopStock; use App\Models\User\UserItem; +use App\Services\LimitManager; use App\Services\ShopManager; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -30,7 +30,7 @@ class ShopController extends Controller { */ public function getIndex() { return view('shops.index', [ - 'shops' => Shop::where('is_active', 1)->orderBy('sort', 'DESC')->get(), + 'shops' => Shop::where('is_active', 1)->where('is_hidden', 0)->orderBy('sort', 'DESC')->get(), ]); } @@ -42,25 +42,81 @@ public function getIndex() { * @return \Illuminate\Contracts\Support\Renderable */ public function getShop($id) { - $categories = ItemCategory::visible(Auth::user() ?? null)->orderBy('sort', 'DESC')->get(); $shop = Shop::where('id', $id)->where('is_active', 1)->first(); + if (!$shop) { abort(404); } - $query = $shop->displayStock()->where(function ($query) use ($categories) { - $query->whereIn('item_category_id', $categories->pluck('id')->toArray()) - ->orWhereNull('item_category_id'); - }); + if ($shop->is_staff) { + if (!Auth::check()) { + abort(404); + } + if (!Auth::user()->isStaff) { + abort(404); + } + } + + if (count(getLimits($shop))) { + if (!Auth::check()) { + flash('You must be logged in to enter this shop.')->error(); + + return redirect()->to('shops'); + } + + $limitService = new LimitManager; + if (!$limitService->checkLimits($shop)) { + flash($limitService->errors()->getMessages()['error'][0])->error(); - $items = count($categories) ? $query->orderByRaw('FIELD(item_category_id,'.implode(',', $categories->pluck('id')->toArray()).')')->orderBy('name')->get()->groupBy('item_category_id') : $shop->displayStock()->orderBy('name')->get()->groupBy('item_category_id'); + return redirect()->to('shops'); + } + } + + if ($shop->is_fto) { + if (!Auth::check()) { + flash('You must be logged in to enter this shop.')->error(); + + return redirect()->to('/shops'); + } + if (!Auth::user()->settings->is_fto && !Auth::user()->isStaff) { + flash('You must be a FTO to enter this shop.')->error(); + + return redirect()->to('/shops'); + } + } + + // get all types of stock in the shop + $stock_types = ShopStock::where('shop_id', $shop->id)->pluck('stock_type')->unique(); + $stocks = []; + foreach ($stock_types as $type) { + // get the model for the stock type (item, pet, etc) + $type = strtolower($type); + $model = getAssetModelString($type); + // get the category of the stock + if (!class_exists($model.'Category')) { + $stock = $shop->displayStock($model, $type)->where('stock_type', $type)->orderBy('name')->get()->groupBy($type.'_category_id'); + $stocks[$type] = $stock; + continue; // If the category model doesn't exist, skip it + } + $stock_category = ($model.'Category')::orderBy('sort', 'DESC')->get(); + // order the stock + $stock = count($stock_category) ? $shop->displayStock($model, $type)->where('stock_type', $type) + ->orderByRaw('FIELD('.$type.'_category_id,'.implode(',', $stock_category->pluck('id')->toArray()).')') + ->orderBy('name')->get()->groupBy($type.'_category_id') + : $shop->displayStock($model, $type)->where('stock_type', $type)->orderBy('name')->get()->groupBy($type.'_category_id'); + + // make it so key "" appears last + $stock = $stock->sortBy(function ($item, $key) { + return $key == '' ? 1 : 0; + }); + + $stocks[$type] = $stock; + } return view('shops.shop', [ 'shop' => $shop, - 'categories' => $categories->keyBy('id'), - 'items' => $items, + 'stocks' => $stocks, 'shops' => Shop::where('is_active', 1)->orderBy('sort', 'DESC')->get(), - 'currencies' => Currency::whereIn('id', ShopStock::where('shop_id', $shop->id)->pluck('currency_id')->toArray())->get()->keyBy('id'), ]); } @@ -75,7 +131,19 @@ public function getShop($id) { */ public function getShopStock(ShopManager $service, $id, $stockId) { $shop = Shop::where('id', $id)->where('is_active', 1)->first(); - $stock = ShopStock::with('item')->where('id', $stockId)->where('shop_id', $id)->first(); + $stock = ShopStock::where('id', $stockId)->where('shop_id', $id)->first(); + if (!$shop) { + abort(404); + } + + if (count(getLimits($shop))) { + $limitService = new LimitManager; + if (!$limitService->checkLimits($shop)) { + flash($limitService->errors()->getMessages()['error'][0])->error(); + + return redirect()->to('shops'); + } + } $user = Auth::user(); $quantityLimit = 0; @@ -85,16 +153,27 @@ public function getShopStock(ShopManager $service, $id, $stockId) { $quantityLimit = $service->getStockPurchaseLimit($stock, Auth::user()); $userPurchaseCount = $service->checkUserPurchases($stock, Auth::user()); $purchaseLimitReached = $service->checkPurchaseLimitReached($stock, Auth::user()); - $userOwned = UserItem::where('user_id', $user->id)->where('item_id', $stock->item->id)->where('count', '>', 0)->get(); + $userOwned = $service->getUserOwned($stock, Auth::user()); } - if (!$shop) { - abort(404); + if ($shop->use_coupons) { + $couponId = ItemTag::where('tag', 'coupon')->where('is_active', 1); // Removed get() + $itemIds = $couponId->pluck('item_id'); // Could be combined with above + // get rid of any itemIds that are not in allowed_coupons + if ($shop->allowed_coupons && count(json_decode($shop->allowed_coupons, 1))) { + $itemIds = $itemIds->filter(function ($itemId) use ($shop) { + return in_array($itemId, json_decode($shop->allowed_coupons, 1)); + }); + } + $check = UserItem::with('item')->whereIn('item_id', $itemIds)->where('user_id', Auth::user()->id)->where('count', '>', 0)->get()->pluck('item.name', 'id'); + } else { + $check = null; } return view('shops._stock_modal', [ 'shop' => $shop, 'stock' => $stock, + 'userCoupons' => $check, 'quantityLimit' => $quantityLimit, 'userPurchaseCount' => $userPurchaseCount, 'purchaseLimitReached' => $purchaseLimitReached, @@ -111,7 +190,7 @@ public function getShopStock(ShopManager $service, $id, $stockId) { */ public function postBuy(Request $request, ShopManager $service) { $request->validate(ShopLog::$createRules); - if ($service->buyStock($request->only(['stock_id', 'shop_id', 'slug', 'bank', 'quantity']), Auth::user())) { + if ($service->buyStock($request->only(['stock_id', 'shop_id', 'slug', 'bank', 'quantity', 'use_coupon', 'coupon', 'cost_group']), Auth::user())) { flash('Successfully purchased item.')->success(); } else { foreach ($service->errors()->getMessages()['error'] as $error) { diff --git a/app/Http/Controllers/Users/SubmissionController.php b/app/Http/Controllers/Users/SubmissionController.php index 780e7ba76a..00e476fedb 100644 --- a/app/Http/Controllers/Users/SubmissionController.php +++ b/app/Http/Controllers/Users/SubmissionController.php @@ -198,6 +198,24 @@ public function getPromptInfo($id) { ]); } + /** + * Shows prompt requirement information. + * + * @param int $id + * + * @return \Illuminate\Contracts\Support\Renderable + */ + public function getPromptRequirementInfo($id) { + $prompt = Prompt::active()->where('id', $id)->first(); + if (!$prompt) { + return response(404); + } + + return view('home._prompt_requirements', [ + 'prompt' => $prompt, + ]); + } + /** * Creates a new submission. * diff --git a/app/Http/Middleware/ParsePostRequestFields.php b/app/Http/Middleware/ParsePostRequestFields.php index e81d7f4cd8..f108b5c7fe 100644 --- a/app/Http/Middleware/ParsePostRequestFields.php +++ b/app/Http/Middleware/ParsePostRequestFields.php @@ -15,7 +15,7 @@ class ParsePostRequestFields { */ public function handle(Request $request, Closure $next) { if ($request->isMethod('post')) { - $excludedFields = ['_token', 'password', 'email', 'description', 'text', 'criteria']; + $excludedFields = ['_token', 'password', 'email', 'description', 'text', 'evaluation', 'criteria']; $strippedFields = ['name', 'title']; $parsedFields = []; diff --git a/app/Http/Middleware/PostRequestThrottleMiddleware.php b/app/Http/Middleware/PostRequestThrottleMiddleware.php index ce1b2890d9..d22a69e4ec 100644 --- a/app/Http/Middleware/PostRequestThrottleMiddleware.php +++ b/app/Http/Middleware/PostRequestThrottleMiddleware.php @@ -19,7 +19,7 @@ public function handle(Request $request, Closure $next): Response { $allowedRoutes = [ 'admin/*', ]; - if ($request->isMethod('get') || $request->is(...$allowedRoutes)) { + if ($request->isMethod('get') || $request->is(...$allowedRoutes) || app()->environment('local')) { return $next($request); } diff --git a/app/Models/Item/Item.php b/app/Models/Item/Item.php index d9985a19b3..2cfe64a5d8 100644 --- a/app/Models/Item/Item.php +++ b/app/Models/Item/Item.php @@ -109,7 +109,7 @@ public function rarity() { * Get shop stock for this item. */ public function shopStock() { - return $this->hasMany(ShopStock::class, 'item_id'); + return $this->hasMany(ShopStock::class, 'item_id')->where('is_visible', 1); } /********************************************************************************************** @@ -368,21 +368,6 @@ public function getResellAttribute() { return collect($this->data['resell']); } - /** - * Get the shops that stock this item. - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getShopsAttribute() { - if (!config('lorekeeper.extensions.item_entry_expansion.extra_fields') || !$this->shop_stock_count) { - return null; - } - - $shops = Shop::whereIn('id', $this->shopStock->pluck('shop_id')->toArray())->orderBy('sort', 'DESC')->get(); - - return $shops; - } - /** * Get the prompts attribute as an associative array. * @@ -446,4 +431,27 @@ public function hasTag($tag) { public function tag($tag) { return $this->tags()->where('tag', $tag)->where('is_active', 1)->first(); } + + /** + * Get the shops that stock this item. + * + * @param mixed|null $user + * + * @return \Illuminate\Database\Eloquent\Collection + */ + public function shops($user = null) { + if (!config('lorekeeper.extensions.item_entry_expansion.extra_fields') || !$this->shop_stock_count) { + return null; + } + + $shops = Shop::where(function ($query) use ($user) { + if ($user && $user->hasPower('edit_data')) { + return $query; + } + + return $query->where('is_hidden', 0)->where('is_staff', 0); + })->whereIn('id', $this->shopStock->pluck('shop_id')->toArray())->get(); + + return $shops; + } } diff --git a/app/Models/Limit/DynamicLimit.php b/app/Models/Limit/DynamicLimit.php new file mode 100644 index 0000000000..208871802b --- /dev/null +++ b/app/Models/Limit/DynamicLimit.php @@ -0,0 +1,58 @@ +name.' Check'; + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * preforms the evaluation of the dynamic limit in a try / except and returns true or false + * if the evaluation is fails. + */ + public function evaluate() { + try { + $eval = preg_replace('/<\?php/', '', $this->evaluation); + $eval = preg_replace('/\n/', '', $eval); + $eval = preg_replace('/\r/', '', $eval); + + return eval($eval); + } catch (\Throwable $th) { + return false; + } + } +} diff --git a/app/Models/Limit/Limit.php b/app/Models/Limit/Limit.php new file mode 100644 index 0000000000..f75cb6e180 --- /dev/null +++ b/app/Models/Limit/Limit.php @@ -0,0 +1,88 @@ +belongsTo($this->object_model, 'object_id'); + } + + /** + * gets the limit of this ... limit. + */ + public function limit() { + switch ($this->limit_type) { + case 'prompt': + return $this->belongsTo(Prompt::class, 'limit_id'); + case 'item': + return $this->belongsTo(Item::class, 'limit_id'); + case 'currency': + return $this->belongsTo(Currency::class, 'limit_id'); + case 'dynamic': + return $this->belongsTo(DynamicLimit::class, 'limit_id'); + } + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * checks if a certain object has any limits. + * + * @param mixed $object + */ + public static function hasLimits($object) { + return self::where('object_model', get_class($object))->where('object_id', $object->id)->exists(); + } + + /** + * get the limits of a certain object. + * + * @param mixed $object + */ + public static function getLimits($object) { + return self::where('object_model', get_class($object))->where('object_id', $object->id)->get(); + } + + /** + * Checks if a user has unlocked this. + * + * @param mixed $user + */ + public function isUnlocked($user) { + return $this->is_unlocked && $user->unlockedLimits()->where('object_model', $this->object_model)->where('object_id', $this->object_id)->exists(); + } +} diff --git a/app/Models/Limit/UserUnlockedLimit.php b/app/Models/Limit/UserUnlockedLimit.php new file mode 100644 index 0000000000..ccf18862a1 --- /dev/null +++ b/app/Models/Limit/UserUnlockedLimit.php @@ -0,0 +1,37 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/Model.php b/app/Models/Model.php index 7525891258..33938e9b25 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -2,6 +2,9 @@ namespace App\Models; +use App\Models\Prompt\Prompt; +use App\Models\Submission\Submission; +use App\Services\LimitManager; use Illuminate\Database\Eloquent\Model as EloquentModel; class Model extends EloquentModel { @@ -11,4 +14,48 @@ class Model extends EloquentModel { * @var string */ public $timestamps = false; + + protected static function boot() { + parent::boot(); + + $service = new LimitManager; + $object = null; + static::creating(function ($model) use ($service, $object) { + switch (get_class($model)) { + case Submission::class: + if ($model->status == 'Pending') { + return true; + } + $object = Prompt::find($model->prompt_id); + break; + } + + if (!$object) { + return true; + } + + if (!$service->checkLimits($object)) { + throw new \Exception($service->errors()->getMessages()['error'][0]); + } + }); + + static::updating(function ($model) use ($service, $object) { + switch (get_class($model)) { + case Submission::class: + if ($model->status != 'Pending' || $model->staff_comments || $model->staff_id) { + return true; + } + $object = Prompt::find($model->prompt_id); + break; + } + + if (!$object) { + return true; + } + + if (!$service->checkLimits($object)) { + throw new \Exception($service->errors()->getMessages()['error'][0]); + } + }); + } } diff --git a/app/Models/Shop/Shop.php b/app/Models/Shop/Shop.php index ca7987fdcd..1a99b07efd 100644 --- a/app/Models/Shop/Shop.php +++ b/app/Models/Shop/Shop.php @@ -4,6 +4,7 @@ use App\Models\Item\Item; use App\Models\Model; +use Carbon\Carbon; class Shop extends Model { /** @@ -12,7 +13,8 @@ class Shop extends Model { * @var array */ protected $fillable = [ - 'name', 'sort', 'has_image', 'description', 'parsed_description', 'is_active', 'hash', + 'name', 'sort', 'has_image', 'description', 'parsed_description', 'is_active', 'hash', 'is_staff', 'use_coupons', 'is_fto', 'allowed_coupons', 'is_timed_shop', 'start_at', 'end_at', + 'is_hidden', 'data', ]; /** @@ -21,6 +23,18 @@ class Shop extends Model { * @var string */ protected $table = 'shops'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'data' => 'array', + 'end_at' => 'datetime', + 'start_at' => 'datetime', + ]; + /** * Validation rules for creation. * @@ -58,9 +72,30 @@ public function stock() { /** * Get the shop stock as items for display purposes. + * + * @param mixed|null $model + * @param mixed|null $type */ - public function displayStock() { - return $this->belongsToMany(Item::class, 'shop_stock')->withPivot('item_id', 'currency_id', 'cost', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', 'id'); + public function displayStock($model = null, $type = null) { + if (!$model || !$type) { + return $this->belongsToMany(Item::class, 'shop_stock')->where('stock_type', 'Item')->withPivot('item_id', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', 'id', 'is_timed_stock') + ->wherePivot('is_visible', 1)->where(function ($query) { + $query->whereNull('shop_stock.start_at') + ->orWhere('shop_stock.start_at', '<', Carbon::now()); + })->where(function ($query) { + $query->whereNull('shop_stock.end_at') + ->orWhere('shop_stock.end_at', '>', Carbon::now()); + }); + } + + return $this->belongsToMany($model, 'shop_stock', 'shop_id', 'item_id')->where('stock_type', $type)->withPivot('item_id', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'purchase_limit', 'id', 'is_timed_stock') + ->wherePivot('is_visible', 1)->where(function ($query) { + $query->whereNull('shop_stock.start_at') + ->orWhere('shop_stock.start_at', '<', Carbon::now()); + })->where(function ($query) { + $query->whereNull('shop_stock.end_at') + ->orWhere('shop_stock.end_at', '>', Carbon::now()); + }); } /********************************************************************************************** @@ -75,7 +110,7 @@ public function displayStock() { * @return string */ public function getDisplayNameAttribute() { - return ''.$this->name.''; + return ''.(!$this->isActive ? ' ' : '').$this->name.''; } /** @@ -144,4 +179,70 @@ public function getAdminUrlAttribute() { public function getAdminPowerAttribute() { return 'edit_data'; } + + /** + * Returns the days the shop is available, if set. + */ + public function getDaysAttribute() { + return $this->data['shop_days'] ?? null; + } + + /** + * Returns the months the shop is available, if set. + */ + public function getMonthsAttribute() { + return $this->data['shop_months'] ?? null; + } + + /** + * Returns if this shop should be active or not. + * We dont account for is_visible here, as this is used for checking both visible and invisible shop. + */ + public function getIsActiveAttribute() { + if ($this->start_at && $this->start_at > Carbon::now()) { + return false; + } + + if ($this->end_at && $this->end_at < Carbon::now()) { + return false; + } + + if ($this->days && !in_array(Carbon::now()->format('l'), $this->days)) { + return false; + } + + if ($this->months && !in_array(Carbon::now()->format('F'), $this->months)) { + return false; + } + + return true; + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * Gets all the coupons useable in the shop. + */ + public function getAllAllowedCouponsAttribute() { + if (!$this->use_coupons || !$this->allowed_coupons) { + return; + } + // Get the coupons from the id in allowed_coupons + $coupons = Item::whereIn('id', json_decode($this->allowed_coupons, 1))->get(); + + return $coupons; + } + + /** + * Gets the shop's stock costs. + * + * @param mixed $id + */ + public function displayStockCosts($id) { + return $this->stock()->where('id', $id)->first()->displayCosts(); + } } diff --git a/app/Models/Shop/ShopLog.php b/app/Models/Shop/ShopLog.php index 0e9f41266c..ba965ca7c1 100644 --- a/app/Models/Shop/ShopLog.php +++ b/app/Models/Shop/ShopLog.php @@ -15,7 +15,7 @@ class ShopLog extends Model { * @var array */ protected $fillable = [ - 'shop_id', 'character_id', 'user_id', 'currency_id', 'cost', 'item_id', 'quantity', + 'shop_id', 'character_id', 'user_id', 'cost', 'item_id', 'quantity', 'stock_type', ]; /** @@ -24,6 +24,16 @@ class ShopLog extends Model { * @var string */ protected $table = 'shop_log'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'cost' => 'array', + ]; + /** * Whether the model contains timestamps to be saved and updated. * @@ -66,7 +76,9 @@ public function character() { * Get the purchased item. */ public function item() { - return $this->belongsTo(Item::class); + $model = getAssetModelString(strtolower($this->stock_type ?? 'Item')); + + return $this->belongsTo($model); } /** @@ -95,6 +107,85 @@ public function currency() { * @return string */ public function getItemDataAttribute() { - return 'Purchased from '.$this->shop->name.' by '.($this->character_id ? $this->character->slug.' (owned by '.$this->user->name.')' : $this->user->displayName).' for '.$this->cost.' '.$this->currency->name.'.'; + $cost = mergeAssetsArrays(parseAssetData($this->cost['user']), parseAssetData($this->cost['character'])); + + return 'Purchased from '.$this->shop->name.' by '. + ($this->character_id ? $this->character->slug.' (owned by '.$this->user->name.')' : $this->user->displayName).' using '. + (createRewardsString($cost, true, true) == 'Nothing. :(' ? 'Free' : createRewardsString($cost, true, true)) + .($this->coupon ? ' with coupon '.$this->coupon->displayName : ''); + } + + /** + * Get the cost of the item. + */ + public function getTotalCostAttribute() { + if (isset($this->cost['user']) && isset($this->cost['character'])) { + return mergeAssetsArrays(parseAssetData($this->cost['user']), parseAssetData($this->cost['character'])); + } + + return parseAssetData($this->cost); + } + + /** + * Get the base cost of the item. + */ + public function getBaseCostAttribute() { + if (isset($this->cost['base'])) { + return parseAssetData($this->cost['base']); + } + + if (isset($this->cost['user']) && isset($this->cost['character'])) { + $assets = mergeAssetsArrays(parseAssetData($this->cost['user']), parseAssetData($this->cost['character'])); + } else { + $assets = parseAssetData($this->cost); + } + + foreach ($assets as $key=>$contents) { + if (count($contents) == 0) { + continue; + } + foreach ($contents as $asset) { + $assets[$key][$asset['asset']->id]['quantity'] = $asset['quantity'] / $this->quantity; + } + } + + return $assets; + } + + /** + * Get the cost of the item in a readable format. + * + * @return string + */ + public function getDisplayCostAttribute() { + if (isset($this->cost['user']) && isset($this->cost['character'])) { + $assets = mergeAssetsArrays(parseAssetData($this->cost['user']), parseAssetData($this->cost['character'])); + } else { + $assets = parseAssetData($this->cost); + } + + return createRewardsString($assets, true, true) == 'Nothing. :(' ? 'Free' : createRewardsString($assets, true, true); + } + + /** + * Get the cost of the item in a readable format. + * + * @return string + */ + public function getDisplayBaseCostAttribute() { + return createRewardsString($this->baseCost, true, true) == 'Nothing. :(' ? 'Free' : createRewardsString($this->baseCost, true, true); + } + + /** + * Returns the coupon used (if any). + * + * @return Item|null + */ + public function getCouponAttribute() { + if (!isset($this->cost['coupon'])) { + return null; + } + + return Item::find($this->cost['coupon']) ?? null; } } diff --git a/app/Models/Shop/ShopStock.php b/app/Models/Shop/ShopStock.php index 9a096bee6f..a45e98f337 100644 --- a/app/Models/Shop/ShopStock.php +++ b/app/Models/Shop/ShopStock.php @@ -2,9 +2,9 @@ namespace App\Models\Shop; -use App\Models\Currency\Currency; use App\Models\Item\Item; use App\Models\Model; +use Carbon\Carbon; class ShopStock extends Model { /** @@ -13,7 +13,8 @@ class ShopStock extends Model { * @var array */ protected $fillable = [ - 'shop_id', 'item_id', 'currency_id', 'cost', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'sort', 'purchase_limit', + 'shop_id', 'item_id', 'use_user_bank', 'use_character_bank', 'is_limited_stock', 'quantity', 'sort', 'purchase_limit', 'purchase_limit_timeframe', 'is_fto', 'stock_type', 'is_visible', + 'restock', 'restock_quantity', 'restock_interval', 'range', 'disallow_transfer', 'is_timed_stock', 'start_at', 'end_at', 'data', ]; /** @@ -23,6 +24,26 @@ class ShopStock extends Model { */ protected $table = 'shop_stock'; + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'data' => 'array', + 'end_at' => 'datetime', + 'start_at' => 'datetime', + ]; + + /** + * Validation rules for creation. + * + * @var array + */ + public static $createRules = [ + 'purchase_limit_timeframe' => 'in:lifetime,yearly,monthly,weekly,daily', + ]; + /********************************************************************************************** RELATIONS @@ -33,7 +54,14 @@ class ShopStock extends Model { * Get the item being stocked. */ public function item() { - return $this->belongsTo(Item::class); + $model = getAssetModelString(strtolower($this->stock_type)); + + if (!class_exists($model)) { + // Laravel requires a relationship instance to be returned (cannot return null), so returning one that doesn't exist here. + return $this->belongsTo(self::class, 'id', 'item_id')->whereNull('item_id'); + } + + return $this->belongsTo($model); } /** @@ -44,9 +72,210 @@ public function shop() { } /** - * Get the currency the item must be purchased with. + * Get the costs associated with this stock. + */ + public function costs() { + return $this->hasMany(ShopStockCost::class, 'shop_stock_id'); + } + + /********************************************************************************************** + + SCOPES + + **********************************************************************************************/ + + /** + * Scopes active stock. + * + * @param mixed $query + */ + public function scopeActive($query) { + return $query->where('is_visible', 1); + } + + /********************************************************************************************** + + ATTRIBUTES + + **********************************************************************************************/ + + /** + * Returns if this stock should be active or not. + * We dont account for is_visible here, as this is used for checking both visible and invisible stock. + */ + public function getIsActiveAttribute() { + if ($this->start_at && $this->start_at > Carbon::now()) { + return false; + } + + if ($this->end_at && $this->end_at < Carbon::now()) { + return false; + } + + if ($this->days && !in_array(Carbon::now()->format('l'), $this->days)) { + return false; + } + + if ($this->months && !in_array(Carbon::now()->format('F'), $this->months)) { + return false; + } + + return true; + } + + /** + * Returns if the stock is random or not. + */ + public function getIsRandomAttribute() { + return isset($this->data['is_random']) && $this->data['is_random']; + } + + /** + * Returns if the stock is category based or not. + */ + public function getIsCategoryAttribute() { + $model = getAssetModelString(strtolower($this->stock_type)); + + return isset($this->data['is_category']) && $this->data['is_category'] && class_exists($model.'Category'); + } + + /** + * Returns the stock category id, only if the stock is category based. */ - public function currency() { - return $this->belongsTo(Currency::class); + public function getCategoryIdAttribute() { + return $this->isCategory ? ($this->data['category_id'] ?? null) : null; + } + + /** + * Returns the days the stock is available, if set. + */ + public function getDaysAttribute() { + return $this->data['stock_days'] ?? null; + } + + /** + * Returns the months the stock is available, if set. + */ + public function getMonthsAttribute() { + return $this->data['stock_months'] ?? null; + } + + /** + * Returns all of the existing groups based on the costs. + */ + public function getGroupsAttribute() { + return $this->costs()->get()->pluck('group')->unique(); + } + + /** + * Makes the costs an array of arrays. + */ + public function getCostGroupsAttribute() { + $costs = $this->costs()->get()->groupBy('group'); + $groupedCosts = []; + foreach ($costs as $group => $costGroup) { + $assets = createAssetsArray(); + foreach ($costGroup as $cost) { + addAsset($assets, $cost->item, $cost->quantity); + } + $groupedCosts[$group] = $assets; + } + + return $groupedCosts; + } + + /* + * Gets the current date associated to the current stocks purchase limit timeframe + */ + public function getPurchaseLimitDateAttribute() { + switch ($this->purchase_limit_timeframe) { + case 'yearly': + $date = strtotime('January 1st'); + break; + case 'monthly': + $date = Carbon::now()->startOfMonth()->timestamp; + break; + case 'weekly': + $date = strtotime('last sunday'); + break; + case 'daily': + $date = strtotime('midnight'); + break; + default: + $date = null; + } + + return $date; + } + + /********************************************************************************************** + + OTHER FUNCTIONS + + **********************************************************************************************/ + + /** + * Returns formatted lists of costs for display. + */ + public function displayCosts() { + $display = []; + $costs = $this->costGroups; + foreach ($costs as $group => $groupCosts) { + $display[] = createRewardsString($groupCosts); + } + + return count($display) ? implode(' OR ', $display) : null; + } + + /** + * Returns the costs in the format group => rewardString for a form select. + */ + public function costForm() { + $costs = $this->costGroups; + $select = []; + foreach ($costs as $group => $groupCosts) { + $select[$group] = createRewardsString($groupCosts, false); + } + + return $select; + } + + /** + * Returns if a group can use coupons. + * + * @param mixed $group + */ + public function canGroupUseCoupons($group) { + return in_array($group, $this->data['can_group_use_coupon'] ?? []); + } + + /** + * Displays when this stock is available in human readable format. + */ + public function displayTime() { + // {!! '
' . ($stock->start_at ? pretty_date($stock->start_at) : 'Now') . ' - ' . ($stock->end_at ? pretty_date($stock->end_at) : 'Always') . '
' !!} + $days = $this->days; + $months = $this->months; + $string = '
'; + // if start_at and end_at are set, we need to show that its avaialble only during that time and THEN only on the days and months + if ($this->start_at && $this->end_at) { + $string .= pretty_date($this->start_at).' - '.pretty_date($this->end_at); + } elseif ($this->start_at) { + $string .= 'From '.pretty_date($this->start_at); + } elseif ($this->end_at) { + $string .= 'Until '.pretty_date($this->end_at); + } + + if ($days) { + $string .= '
Available on '.implode(', ', $days); + } + + if ($months) { + $string .= '
During '.implode(', ', $months); + } + + $string .= '
'; + + return $string; } } diff --git a/app/Models/Shop/ShopStockCost.php b/app/Models/Shop/ShopStockCost.php new file mode 100644 index 0000000000..3de6a99824 --- /dev/null +++ b/app/Models/Shop/ShopStockCost.php @@ -0,0 +1,68 @@ +belongsTo(ShopStock::class, 'shop_stock_id'); + } + + /** + * Get the item being stocked. + */ + public function item() { + $model = getAssetModelString(strtolower($this->cost_type)); + + return $this->belongsTo($model, 'cost_id'); + } + + /** + * Gets all of the other costs for this stock in the same group. + */ + public function group() { + return $this->hasMany(self::class, 'group'); + } + + /********************************************************************************************** + + ATTRIBUTES + + **********************************************************************************************/ + + /** + * Gets all of the other costs for this stock in the same group. + */ + public function getItemsAttribute() { + $model = getAssetModelString(strtolower($this->cost_type)); + + return $model::all()->pluck('name', 'id'); + } +} diff --git a/app/Models/User/User.php b/app/Models/User/User.php index b617f43ba2..d247021dcd 100644 --- a/app/Models/User/User.php +++ b/app/Models/User/User.php @@ -14,6 +14,7 @@ use App\Models\Gallery\GallerySubmission; use App\Models\Item\Item; use App\Models\Item\ItemLog; +use App\Models\Limit\UserUnlockedLimit; use App\Models\Notification; use App\Models\Rank\Rank; use App\Models\Rank\RankPower; @@ -206,6 +207,13 @@ public function commentLikes() { return $this->hasMany(CommentLike::class); } + /** + * Gets all of the user's unlocked limits. + */ + public function unlockedLimits() { + return $this->hasMany(UserUnlockedLimit::class); + } + /********************************************************************************************** SCOPES diff --git a/app/Models/User/UserItem.php b/app/Models/User/UserItem.php index 41bef1ba7c..80ea449a8d 100644 --- a/app/Models/User/UserItem.php +++ b/app/Models/User/UserItem.php @@ -73,7 +73,7 @@ public function getDataAttribute() { * @return array */ public function getIsTransferrableAttribute() { - if (!isset($this->data['disallow_transfer']) && $this->item->allow_transfer) { + if ((!isset($this->data['disallow_transfer']) || !$this->data['disallow_transfer']) && $this->item->allow_transfer) { return true; } diff --git a/app/Services/CurrencyService.php b/app/Services/CurrencyService.php index 38bbc69b6e..1c283abdf1 100644 --- a/app/Services/CurrencyService.php +++ b/app/Services/CurrencyService.php @@ -323,7 +323,7 @@ public function deleteCurrency($currency, $user) { if (DB::table('prompt_rewards')->where('rewardable_type', 'Currency')->where('rewardable_id', $currency->id)->exists()) { throw new \Exception('A prompt currently distributes this currency as a reward. Please remove the currency before deleting it.'); } - if (DB::table('shop_stock')->where('currency_id', $currency->id)->exists()) { + if (DB::table('shop_stock_costs')->where('cost_type', 'Currency')->where('cost_id', $currency->id)->exists()) { throw new \Exception('A shop currently requires this currency to purchase an currency. Please change the currency before deleting it.'); } // Disabled for now due to issues with JSON lookup with older mysql versions/mariaDB diff --git a/app/Services/Item/CouponService.php b/app/Services/Item/CouponService.php new file mode 100644 index 0000000000..9c538de007 --- /dev/null +++ b/app/Services/Item/CouponService.php @@ -0,0 +1,63 @@ +data['discount'] ?? null; + $couponData['infinite'] = $tag->data['infinite'] ?? null; + + return $couponData; + } + + /** + * Processes the data attribute of the tag and returns it in the preferred format. + * + * @param string $tag + * @param array $data + * + * @return bool + */ + public function updateData($tag, $data) { + DB::beginTransaction(); + + try { + if (!isset($data['infinite'])) { + $data['infinite'] = 0; + } + + $coupon['discount'] = $data['discount']; + $coupon['infinite'] = $data['infinite']; + $tag->update(['data' => json_encode($coupon)]); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } +} diff --git a/app/Services/LimitManager.php b/app/Services/LimitManager.php new file mode 100644 index 0000000000..d8f47eec2d --- /dev/null +++ b/app/Services/LimitManager.php @@ -0,0 +1,137 @@ +first()->is_unlocked) { + if ($user->unlockedLimits()->where('object_model', get_class($object))->where('object_id', $object->id)->exists()) { + return true; + } + } + + $plucked_stacks = []; + foreach ($limits as $limit) { + switch ($limit->limit_type) { + case 'prompt': + // check at least quantity of prompts has been approved + if (Submission::where('user_id', $user->id)->where('status', 'Approved')->where('prompt_id', $limit->limit_id)->count() < $limit->quantity) { + throw new \Exception('You have not completed the prompt '.$limit->limit->displayName.' enough times to complete this action.'); + } + break; + case 'item': + if (!$user->items()->where('item_id', $limit->limit_id)->sum('count') >= $limit->quantity) { + throw new \Exception('You do not have enough of the item '.$limit->object->name.' to complete this action.'); + } + + if ($limit->debit) { + $stacks = UserItem::where('user_id', $user->id)->where('item_id', $limit->limit_id)->orderBy('count', 'asc')->get(); // asc because pop() removes from the end + + $count = $limit->quantity; + while ($count > 0) { + $stack = $stacks->pop(); + $quantity = $stack->count >= $count ? $count : $stack->count; + $count -= $quantity; + $plucked_stacks[$stack->id] = $quantity; + } + } + break; + case 'currency': + if (DB::table('user_currencies')->where('user_id', $user->id)->where('currency_id', $limit->limit_id)->value('quantity') < $limit->quantity) { + throw new \Exception('You do not have enough '.$limit->limit->displayName.' to complete this action.'); + } + + if ($limit->debit) { + $service = new CurrencyManager; + if (!$service->debitCurrency($user, null, 'Limit Requirements', 'Used in '.$limit->object->displayName.' limit requirements.', $limit->limit, $limit->quantity)) { + foreach ($service->errors()->getMessages()['error'] as $error) { + flash($error)->error(); + } + throw new \Exception('Currency could not be removed.'); + } + } + break; + case 'dynamic': + if (!$this->checkDynamicLimit($limit, $user)) { + throw new \Exception('You do not meet the requirements to complete this action.'); + } + break; + } + } + + if (count($plucked_stacks)) { + $inventoryManager = new InventoryManager; + $type = 'Limit Requirements'; + $data = [ + 'data' => 'Used in '.$limit->object->displayName ?? $limit->object->name.'\'s limit requirements.', + ]; + + foreach ($plucked_stacks as $id=>$quantity) { + $stack = UserItem::find($id); + if (!$inventoryManager->debitStack($user, $type, $data, $stack, $quantity)) { + throw new \Exception('Items could not be removed.'); + } + } + } + + if ($limits->first()->is_unlocked) { + $user->unlockedLimits()->create([ + 'object_model' => get_class($object), + 'object_id' => $object->id, + ]); + } + + return true; + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + } + + /** + * checks a dynamic limit. + * + * @param mixed $limit + * @param mixed $user + */ + private function checkDynamicLimit($limit, $user) { + try { + return $limit->limit->evaluate(); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + } +} diff --git a/app/Services/LimitService.php b/app/Services/LimitService.php new file mode 100644 index 0000000000..3912731a0a --- /dev/null +++ b/app/Services/LimitService.php @@ -0,0 +1,169 @@ + 0) { + $limits->each(function ($limit) { + $limit->delete(); + }); + } + if (count($limits) > 0) { + flash('Deleted '.count($limits).' old limits.')->success(); + } + + if (isset($data['limit_type'])) { + foreach ($data['limit_type'] as $key => $type) { + $limit = new Limit([ + 'object_model' => $object_model, + 'object_id' => $object_id, + 'limit_type' => $data['limit_type'][$key], + 'limit_id' => $data['limit_id'][$key], + 'quantity' => $data['quantity'][$key], + 'debit' => $data['debit'][$key] == 'no' ? 0 : 1, + 'is_unlocked' => $data['is_unlocked'] == 'no' ? 0 : 1, + ]); + + if (!$limit->save()) { + throw new \Exception('Failed to save limit.'); + } + } + } + + // log the action + if ($log && !$this->logAdminAction(Auth::user(), 'Edited Limits', 'Edited '.$object->displayName.' limits')) { + throw new \Exception('Failed to log admin action.'); + } + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /********************************************************************************************** + + DYNAMIC LIMITS + + **********************************************************************************************/ + + /** + * Creates a new limit. + * + * @param array $data + * @param \App\Models\User\User $user + * + * @return bool|Limit + */ + public function createLimit($data, $user) { + DB::beginTransaction(); + + try { + $data['description'] = isset($data['description']) ? parse($data['description']) : null; + $data['evaluation'] = str_replace('\\', '\\\\', $data['evaluation']); + + $limit = DynamicLimit::create($data); + + return $this->commitReturn($limit); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Updates a limit. + * + * @param Limit $limit + * @param array $data + * @param \App\Models\User\User $user + * + * @return bool|Limit + */ + public function updateLimit($limit, $data, $user) { + DB::beginTransaction(); + + try { + if (DynamicLimit::where('name', $data['name'])->where('id', '!=', $limit->id)->exists()) { + throw new \Exception('The name has already been taken.'); + } + + $data['description'] = isset($data['description']) ? parse($data['description']) : null; + $data['evaluation'] = str_replace('\\', '\\\\', $data['evaluation']); + + $limit->update($data); + + return $this->commitReturn($limit); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + /** + * Deletes a limit. + * + * @param Limit $limit + * + * @return bool + */ + public function deleteLimit($limit) { + DB::beginTransaction(); + + try { + if (Limit::where('limit_type', 'dynamic')->where('limit_id', $limit->id)->exists()) { + throw new \Exception('This limit is currently in use and cannot be deleted.'); + } + $limit->delete(); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } +} diff --git a/app/Services/ShopManager.php b/app/Services/ShopManager.php index 749e4f9050..d7b2b043ed 100644 --- a/app/Services/ShopManager.php +++ b/app/Services/ShopManager.php @@ -3,10 +3,13 @@ namespace App\Services; use App\Models\Character\Character; +use App\Models\Item\Item; use App\Models\Shop\Shop; use App\Models\Shop\ShopLog; use App\Models\Shop\ShopStock; +use App\Models\User\UserItem; use Illuminate\Support\Facades\DB; +use Settings; class ShopManager extends Service { /* @@ -42,7 +45,7 @@ public function buyStock($data, $user) { } // Check that the stock exists and belongs to the shop - $shopStock = ShopStock::where('id', $data['stock_id'])->where('shop_id', $data['shop_id'])->with('currency')->with('item')->first(); + $shopStock = ShopStock::where('id', $data['stock_id'])->where('shop_id', $data['shop_id'])->first(); if (!$shopStock) { throw new \Exception('Invalid item selected.'); } @@ -52,23 +55,61 @@ public function buyStock($data, $user) { throw new \Exception('There is insufficient stock to fulfill your request.'); } + if (isset($data['cost_group'])) { + $costs = $shopStock->costs()->where('group', $data['cost_group'])->get(); + } else { + $costs = $shopStock->costs()->get(); + // make sure that there is not differing groups + if (count($costs->pluck('group')->unique()) > 1) { + throw new \Exception('There are multiple cost groups for this item, please select one.'); + } + } + // Check if the user can only buy a limited number of this item, and if it does, check that the user hasn't hit the limit if ($shopStock->purchase_limit && $this->checkPurchaseLimitReached($shopStock, $user)) { throw new \Exception('You have already purchased the maximum amount of this item you can buy.'); } - if ($shopStock->purchase_limit && $quantity > $shopStock->purchase_limit) { - throw new \Exception('The quantity specified exceeds the amount of this item you can buy.'); - } + $coupon = null; + $couponUserItem = null; + if (isset($data['use_coupon'])) { + // check if the the stock is limited stock + if ($shopStock->is_limited_stock && !Settings::get('limited_stock_coupon_settings')) { + throw new \Exception('Sorry! You can\'t use coupons on limited stock items'); + } + if (!isset($data['coupon'])) { + throw new \Exception('Please select a coupon to use.'); + } + // finding the users tag + $couponUserItem = UserItem::find($data['coupon']); + // check if the item id is inside allowed_coupons + if ($shop->allowed_coupons && count(json_decode($shop->allowed_coupons, 1)) > 0 && !in_array($couponUserItem->item_id, json_decode($shop->allowed_coupons, 1))) { + throw new \Exception('Sorry! You can\'t use this coupon.'); + } + // finding bought item + $item = Item::find($couponUserItem->item_id); + $tag = $item->tags()->where('tag', 'Coupon')->first(); + $coupon = $tag->data; - $total_cost = $shopStock->cost * $quantity; + if (!$coupon['discount']) { + throw new \Exception('No discount amount set, please contact a site admin before trying to purchase again.'); + } + + // make sure this item isn't free + if (!$shopStock->costs()->count()) { + throw new \Exception('Cannot use a coupon on an item that is free.'); + } + + // if the coupon isn't infinite kill it + if (!$coupon['infinite']) { + if (!(new InventoryManager)->debitStack($user, 'Coupon Used', ['data' => 'Coupon used in purchase of '.$shopStock->item->name.' from '.$shop->name], $couponUserItem, 1)) { + throw new \Exception('Failed to remove coupon.'); + } + } + } $character = null; if ($data['bank'] == 'character') { - // Check if the user is using a character to pay - // - stock must be purchaseable with characters - // - currency must be character-held - // - character has enough currency if (!$shopStock->use_character_bank || !$shopStock->currency->is_character_owned) { throw new \Exception("You cannot use a character's bank to pay for this item."); } @@ -82,21 +123,70 @@ public function buyStock($data, $user) { if ($character->user_id != $user->id) { throw new \Exception('That character does not belong to you.'); } - if (!(new CurrencyManager)->debitCurrency($character, null, 'Shop Purchase', 'Purchased '.$shopStock->item->name.' from '.$shop->name, $shopStock->currency, $total_cost)) { - throw new \Exception('Not enough currency to make this purchase.'); + } + + $baseStockCost = mergeAssetsArrays(createAssetsArray(true), createAssetsArray()); + $userCostAssets = createAssetsArray(); + $characterCostAssets = createAssetsArray(true); + foreach ($costs as $cost) { + $costQuantity = abs($cost->quantity); + if ($coupon) { // coupon applies to ALL costs in the selected group. + if (!Settings::get('coupon_settings')) { + $minus = ($coupon['discount'] / 100) * ($costQuantity * $quantity); + $base = ($costQuantity * $quantity); + if ($base <= 0) { + throw new \Exception('Cannot use a coupon on an item that is free.'); + } + $new = $base - $minus; + $costQuantity = round($new); + } else { + $minus = ($coupon['discount'] / 100) * ($costQuantity); + $base = ($costQuantity * $quantity); + if ($base <= 0) { + throw new \Exception('Cannot use a coupon on an item that is free.'); + } + $new = $base - $minus; + $costQuantity = round($new); + } + } else { + $costQuantity *= $quantity; } - } else { - // If the user is paying by themselves - // - stock must be purchaseable by users - // - currency must be user-held - // - user has enough currency - if (!$shopStock->use_user_bank || !$shopStock->currency->is_user_owned) { - throw new \Exception('You cannot use your user bank to pay for this item.'); + + if ($cost->item->assetType == 'currency') { + if ($data['bank'] == 'user') { + if (!$cost->item->is_user_owned) { + throw new \Exception('You cannot use your user bank to pay for this item.'); + } + + addAsset($userCostAssets, $cost->item, -$costQuantity); + } else { + if (!$cost->item->is_character_owned) { + throw new \Exception("You cannot use a character's bank to pay for this item."); + } + + addAsset($characterCostAssets, $cost->item, -$costQuantity); + } + } else { + addAsset($userCostAssets, $cost->item, -$costQuantity); } - if ($shopStock->cost > 0 && !(new CurrencyManager)->debitCurrency($user, null, 'Shop Purchase', 'Purchased '.$shopStock->item->name.' from '.$shop->name, $shopStock->currency, $total_cost)) { - throw new \Exception('Not enough currency to make this purchase.'); + + addAsset($baseStockCost, $cost->item, $cost->quantity); + } + + if ($character) { + if (!fillCharacterAssets($characterCostAssets, $character, null, 'Shop Purchase', [ + 'data' => 'Purchased '.$shopStock->item->name.' x'.$quantity.' from '.$shop->name. + ($coupon ? '. Coupon used: '.$couponUserItem->item->name : ''), + ])) { + throw new \Exception('Failed to purchase item.'); } } + if (!fillUserAssets($userCostAssets, $user, null, 'Shop Purchase', [ + 'data' => 'Purchased '.$shopStock->item->name.' x'.$quantity.' from '.$shop->name. + ($coupon ? '. Coupon used: '.$couponUserItem->item->name : ''), + ])) { + throw new \Exception('Failed to purchase item - could not debit costs.'); + } // If the item has a limited quantity, decrease the quantity if ($shopStock->is_limited_stock) { @@ -109,18 +199,26 @@ public function buyStock($data, $user) { 'shop_id' => $shop->id, 'character_id' => $character ? $character->id : null, 'user_id' => $user->id, - 'currency_id' => $shopStock->currency->id, - 'cost' => $total_cost, + 'cost' => [ + 'base' => getDataReadyAssets($baseStockCost), + 'user' => getDataReadyAssets($userCostAssets), + 'character' => getDataReadyAssets($characterCostAssets), + 'coupon' => $couponUserItem ? $couponUserItem->item->id : null, + ], + 'stock_type' => $shopStock->stock_type, 'item_id' => $shopStock->item_id, 'quantity' => $quantity, ]); // Give the user the item, noting down 1. whose currency was used (user or character) 2. who purchased it 3. which shop it was purchased from - if (!(new InventoryManager)->creditItem(null, $user, 'Shop Purchase', [ + $assets = createAssetsArray(); + addAsset($assets, $shopStock->item, $quantity); + + if (!fillUserAssets($assets, null, $user, 'Shop Purchase', [ 'data' => $shopLog->itemData, 'notes' => 'Purchased '.format_date($shopLog->created_at), - ], $shopStock->item, $quantity)) { - throw new \Exception('Failed to purchase item.'); + ] + ($shopStock->disallow_transfer ? ['disallow_transfer' => true] : []))) { + throw new \Exception('Failed to purchase item - could not credit item.'); } return $this->commitReturn($shop); @@ -156,9 +254,37 @@ public function checkPurchaseLimitReached($shopStock, $user) { * @return int */ public function checkUserPurchases($shopStock, $user) { - return ShopLog::where('shop_id', $shopStock->shop_id)->where('item_id', $shopStock->item_id)->where('user_id', $user->id)->sum('quantity'); + $date = $shopStock->purchaseLimitDate; + $shopQuery = ShopLog::where('shop_id', $shopStock->shop_id) + ->where('item_id', $shopStock->item_id) + ->where('user_id', $user->id); + $shopQuery = isset($date) ? $shopQuery->where('created_at', '>=', date('Y-m-d H:i:s', $date)) : $shopQuery; + + // check the costs vs the user's purchase recorded costs + $shopQuery = $shopQuery->get()->filter(function ($log) use ($shopStock) { + // if there is no costs, then return true, since free items should also have limits + if (!count($shopStock->costGroups) && countAssets($log->baseCost) == 0) { + return true; + } + + foreach ($shopStock->costGroups as $group => $costs) { + if (compareAssetArrays($log->baseCost, $costs, false, true)) { + return true; + } + } + + return false; + }); + + return $shopQuery->sum('quantity'); } + /** + * Gets the purchase limit for an item from a shop. + * + * @param mixed $shopStock + * @param mixed $user + */ public function getStockPurchaseLimit($shopStock, $user) { $limit = config('lorekeeper.settings.default_purchase_limit'); if ($shopStock->purchase_limit > 0) { @@ -175,4 +301,17 @@ public function getStockPurchaseLimit($shopStock, $user) { return $limit; } + + /** + * Gets how many of a shop item a user owns. + * + * @param mixed $stock + * @param mixed $user + */ + public function getUserOwned($stock, $user) { + switch (strtolower($stock->stock_type)) { + case 'item': + return $user->items()->where('item_id', $stock->item_id)->sum('count'); + } + } } diff --git a/app/Services/ShopService.php b/app/Services/ShopService.php index 83901ff697..6e3a66358b 100644 --- a/app/Services/ShopService.php +++ b/app/Services/ShopService.php @@ -45,6 +45,8 @@ public function createShop($data, $user) { $data['has_image'] = 0; } + $data['is_timed_shop'] = isset($data['is_timed_shop']); + $shop = Shop::create($data); if ($image) { @@ -87,6 +89,8 @@ public function updateShop($shop, $data, $user) { unset($data['image']); } + $data['is_timed_shop'] = isset($data['is_timed_shop']); + $shop->update($data); if ($shop) { @@ -102,7 +106,7 @@ public function updateShop($shop, $data, $user) { } /** - * Updates shop stock. + * Creates shop stock. * * @param Shop $shop * @param array $data @@ -110,39 +114,87 @@ public function updateShop($shop, $data, $user) { * * @return bool|Shop */ - public function updateShopStock($shop, $data, $user) { + public function createShopStock($shop, $data, $user) { DB::beginTransaction(); try { - if (isset($data['item_id'])) { - foreach ($data['item_id'] as $key => $itemId) { - if ($data['cost'][$key] == null) { - throw new \Exception('One or more of the items is missing a cost.'); - } - if ($data['cost'][$key] < 0) { - throw new \Exception('One or more of the items has a negative cost.'); - } + if (!$data['stock_type']) { + throw new \Exception('Please select a stock type.'); + } + if (!$data['item_id']) { + throw new \Exception('You must select an item.'); + } + + $is_random = false; + $is_category = false; + $categoryId = null; + // if the id is not numeric, it's a random item + if (!is_numeric($data['item_id'])) { + $is_random = true; + + $type = $data['stock_type']; + $model = getAssetModelString(strtolower($type)); + if ($data['item_id'] != 'random') { + // this means its a category, extract the id from the string + $categoryId = explode('-', $data['item_id'])[0]; + $is_category = true; } - // Clear the existing shop stock - $shop->stock()->delete(); - - foreach ($data['item_id'] as $key => $itemId) { - $shop->stock()->create([ - 'shop_id' => $shop->id, - 'item_id' => $data['item_id'][$key], - 'currency_id' => $data['currency_id'][$key], - 'cost' => $data['cost'][$key], - 'use_user_bank' => isset($data['use_user_bank'][$key]), - 'use_character_bank' => isset($data['use_character_bank'][$key]), - 'is_limited_stock' => isset($data['is_limited_stock'][$key]), - 'quantity' => isset($data['is_limited_stock'][$key]) ? $data['quantity'][$key] : 0, - 'purchase_limit' => $data['purchase_limit'][$key], + // check if "visible" method exists, if it does only get visible items + // also check for "released" method, if it exists only get released items + if (method_exists($model, 'visible')) { + $data['item_id'] = $categoryId ? + $model::visible()->where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::visible()->inRandomOrder()->first()->id; + } elseif (method_exists($model, 'released')) { + $data['item_id'] = $categoryId ? + $model::released()->where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::released()->inRandomOrder()->first()->id; + } else { + $data['item_id'] = $categoryId ? + $model::where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::inRandomOrder()->first()->id; + } + } + + $stock = $shop->stock()->create([ + 'shop_id' => $shop->id, + 'item_id' => $data['item_id'], + 'use_user_bank' => isset($data['use_user_bank']), + 'use_character_bank' => isset($data['use_character_bank']), + 'is_fto' => isset($data['is_fto']), + 'is_limited_stock' => isset($data['is_limited_stock']), + 'quantity' => isset($data['is_limited_stock']) ? $data['quantity'] : 0, + 'purchase_limit' => $data['purchase_limit'] ?? 0, + 'purchase_limit_timeframe' => isset($data['purchase_limit']) ? $data['purchase_limit_timeframe'] : null, + 'stock_type' => $data['stock_type'], + 'is_visible' => $data['is_visible'] ?? 0, + 'restock' => $data['restock'] ?? 0, + 'restock_quantity' => isset($data['restock']) && isset($data['quantity']) ? $data['quantity'] : 1, + 'restock_interval' => $data['restock_interval'] ?? 2, + 'range' => $data['range'] ?? 0, + 'disallow_transfer' => $data['disallow_transfer'] ?? 0, + 'is_timed_stock' => isset($data['is_timed_stock']), + 'start_at' => $data['stock_start_at'], + 'end_at' => $data['stock_end_at'], + 'data' => [ + 'is_random' => $is_random, + 'is_category' => $is_category, + 'category_id' => $categoryId, + 'stock_days' => $data['stock_days'] ?? null, + 'stock_months' => $data['stock_months'] ?? null, + ], + ]); + + if (isset($data['cost_type']) && isset($data['cost_quantity'])) { + foreach ($data['cost_type'] as $key => $costType) { + $stock->costs()->create([ + 'cost_type' => $costType, + 'cost_id' => $data['cost_id'][$key], + 'quantity' => $data['cost_quantity'][$key], + 'group' => $data['group'][$key] ?? 1, ]); } - } else { - // Clear the existing shop stock - $shop->stock()->delete(); } return $this->commitReturn($shop); @@ -153,6 +205,136 @@ public function updateShopStock($shop, $data, $user) { return $this->rollbackReturn(false); } + /** + * Updates shop stock. + * + * @param array $data + * @param \App\Models\User\User $user + * @param mixed $stock + * + * @return bool|Shop + */ + public function editShopStock($stock, $data, $user) { + DB::beginTransaction(); + + try { + if (!$data['stock_type']) { + throw new \Exception('Please select a stock type.'); + } + if (!$data['item_id']) { + throw new \Exception('You must select an item.'); + } + + $is_random = false; + $is_category = false; + $categoryId = null; + // if the id is not numeric, it's a random item + if (!is_numeric($data['item_id'])) { + $is_random = true; + + $type = $data['stock_type']; + $model = getAssetModelString(strtolower($type)); + if ($data['item_id'] != 'random') { + // this means its a category, extract the id from the string + $categoryId = explode('-', $data['item_id'])[0]; + $is_category = true; + } + + // check if "visible" method exists, if it does only get visible items + // also check for "released" method, if it exists only get released items + if (method_exists($model, 'visible')) { + $data['item_id'] = $categoryId ? + $model::visible()->where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::visible()->inRandomOrder()->first()->id; + } elseif (method_exists($model, 'released')) { + $data['item_id'] = $categoryId ? + $model::released()->where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::released()->inRandomOrder()->first()->id; + } else { + $data['item_id'] = $categoryId ? + $model::where(strtolower($type).'_category_id', $categoryId)->inRandomOrder()->first()->id : + $model::inRandomOrder()->first()->id; + } + } + + $stock->update([ + 'shop_id' => $stock->shop->id, + 'item_id' => $data['item_id'], + 'use_user_bank' => isset($data['use_user_bank']), + 'use_character_bank' => isset($data['use_character_bank']), + 'is_fto' => isset($data['is_fto']), + 'is_limited_stock' => isset($data['is_limited_stock']), + 'quantity' => isset($data['is_limited_stock']) ? $data['quantity'] : 0, + 'purchase_limit' => $data['purchase_limit'] ?? 0, + 'purchase_limit_timeframe' => $data['purchase_limit_timeframe'] ?? null, + 'stock_type' => $data['stock_type'], + 'is_visible' => $data['is_visible'] ?? 0, + 'restock' => $data['restock'] ?? 0, + 'restock_quantity' => isset($data['restock']) && isset($data['quantity']) ? $data['quantity'] : 1, + 'restock_interval' => $data['restock_interval'] ?? 2, + 'range' => $data['range'] ?? 0, + 'disallow_transfer' => $data['disallow_transfer'] ?? 0, + 'is_timed_stock' => isset($data['is_timed_stock']), + 'start_at' => $data['stock_start_at'], + 'end_at' => $data['stock_end_at'], + ]); + + $stock->costs()->delete(); + + if (isset($data['cost_type']) && isset($data['cost_quantity'])) { + foreach ($data['cost_type'] as $key => $costType) { + $stock->costs()->create([ + 'cost_type' => $costType, + 'cost_id' => $data['cost_id'][$key], + 'quantity' => $data['cost_quantity'][$key], + 'group' => $data['group'][$key] ?? 1, + ]); + } + } + + // add coupon usage based on groups + $stockData = [ + 'is_random' => $is_random, + 'is_category' => $is_category, + 'category_id' => $categoryId, + 'stock_days' => $data['stock_days'] ?? null, + 'stock_months' => $data['stock_months'] ?? null, + 'can_group_use_coupon' => [], + ]; + if (isset($data['can_group_use_coupon'])) { + foreach ($data['can_group_use_coupon'] as $group => $canUseCoupon) { + // check if the group exists in the costs, since it may have been removed + if ($stock->costs()->where('group', $group)->exists() && $canUseCoupon) { + $stockData['can_group_use_coupon'][] = $group; + } + } + } + $stock->update([ + 'data' => $stockData, + ]); + + return $this->commitReturn($stock); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + + public function deleteStock($stock) { + DB::beginTransaction(); + + try { + $stock->delete(); + + return $this->commitReturn(true); + } catch (\Exception $e) { + $this->setError('error', $e->getMessage()); + } + + return $this->rollbackReturn(false); + } + /** * Deletes a shop. * @@ -217,10 +399,17 @@ public function sortShop($data) { private function populateShopData($data, $shop = null) { if (isset($data['description']) && $data['description']) { $data['parsed_description'] = parse($data['description']); - } else { - $data['parsed_description'] = null; } $data['is_active'] = isset($data['is_active']); + $data['is_hidden'] = isset($data['is_hidden']); + $data['is_staff'] = isset($data['is_staff']); + $data['use_coupons'] = isset($data['use_coupons']); + $data['allowed_coupons'] ??= null; + $data['data'] = [ + 'shop_days' => $data['shop_days'] ?? null, + 'shop_months' => $data['shop_months'] ?? null, + ]; + unset($data['shop_days'], $data['shop_months']); if (isset($data['remove_image'])) { if ($shop && $shop->has_image && $data['remove_image']) { diff --git a/composer.json b/composer.json index 80150b404f..ddcc4be20a 100644 --- a/composer.json +++ b/composer.json @@ -87,4 +87,4 @@ "./vendor/bin/pint" ] } -} +} \ No newline at end of file diff --git a/config/lorekeeper/admin_sidebar.php b/config/lorekeeper/admin_sidebar.php index c1deabf75d..409c62f00d 100644 --- a/config/lorekeeper/admin_sidebar.php +++ b/config/lorekeeper/admin_sidebar.php @@ -194,6 +194,10 @@ 'name' => 'Items', 'url' => 'admin/data/items', ], + [ + 'name' => 'Dynamic Limits', + 'url' => 'admin/data/limits', + ], ], ], 'Raffles' => [ diff --git a/config/lorekeeper/ext-tracker/shop_features.php b/config/lorekeeper/ext-tracker/shop_features.php new file mode 100644 index 0000000000..96571ca753 --- /dev/null +++ b/config/lorekeeper/ext-tracker/shop_features.php @@ -0,0 +1,9 @@ + 'Shop_Features', + 'creators' => json_encode([ + 'Newt' => 'https://github.com/ne-wt/', + ]), + 'version' => '1.0.4', +]; diff --git a/config/lorekeeper/item_tags.php b/config/lorekeeper/item_tags.php index 3435be8b6a..9323dbe922 100644 --- a/config/lorekeeper/item_tags.php +++ b/config/lorekeeper/item_tags.php @@ -24,4 +24,10 @@ 'text_color' => '#ffffff', 'background_color' => '#1fd1a7', ], + + 'coupon' => [ + 'name' => 'Coupon', + 'text_color' => '#ffffff', + 'background_color' => '#ff5ca8', + ], ]; diff --git a/config/lorekeeper/limits.php b/config/lorekeeper/limits.php new file mode 100644 index 0000000000..7622261ef2 --- /dev/null +++ b/config/lorekeeper/limits.php @@ -0,0 +1,23 @@ + [ + 'prompt' => [ + 'name' => 'Prompts', + 'description' => 'Prompt limits require a user to have submitted to the specified prompt a certain number of times.', + ], + 'item' => [ + 'name' => 'Items', + 'description' => 'Item limits require a user to have a certain number of items in their inventory.', + ], + 'currency' => [ + 'name' => 'Currency', + 'description' => 'Currency limits require a user to have a certain amount of currency.', + ], + 'dynamic' => [ + 'name' => 'Dynamic', + 'description' => 'Dynamic limits require a user to meet a certain condition. The condition is evaluated at runtime.', + ], + ], +]; diff --git a/database/migrations/2020_11_19_201307_add_admin_shops.php b/database/migrations/2020_11_19_201307_add_admin_shops.php new file mode 100644 index 0000000000..c7f578d259 --- /dev/null +++ b/database/migrations/2020_11_19_201307_add_admin_shops.php @@ -0,0 +1,31 @@ +boolean('is_staff')->default(0); + $table->boolean('use_coupons')->default(0); + $table->boolean('is_restricted')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::table('shops', function (Blueprint $table) { + $table->dropColumn('is_staff'); + $table->dropColumn('use_coupons'); + $table->dropColumn('is_restricted'); + }); + } +} diff --git a/database/migrations/2020_11_19_205400_add_shop_restriction_table.php b/database/migrations/2020_11_19_205400_add_shop_restriction_table.php new file mode 100644 index 0000000000..6ab65b0237 --- /dev/null +++ b/database/migrations/2020_11_19_205400_add_shop_restriction_table.php @@ -0,0 +1,27 @@ +increments('id'); + $table->integer('shop_id'); + $table->integer('item_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::dropIfExists('shop_limits'); + } +} diff --git a/database/migrations/2020_11_26_161652_add_fto_shops.php b/database/migrations/2020_11_26_161652_add_fto_shops.php new file mode 100644 index 0000000000..db1500d15d --- /dev/null +++ b/database/migrations/2020_11_26_161652_add_fto_shops.php @@ -0,0 +1,35 @@ +boolean('is_fto')->default(0); + }); + + Schema::table('shop_stock', function (Blueprint $table) { + $table->boolean('is_fto')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::table('shops', function (Blueprint $table) { + $table->dropColumn('is_fto'); + }); + + Schema::table('shop_stock', function (Blueprint $table) { + $table->dropColumn('is_fto'); + }); + } +} diff --git a/database/migrations/2021_09_02_193957_add_shop_stock_type.php b/database/migrations/2021_09_02_193957_add_shop_stock_type.php new file mode 100644 index 0000000000..946e8e1233 --- /dev/null +++ b/database/migrations/2021_09_02_193957_add_shop_stock_type.php @@ -0,0 +1,27 @@ +string('stock_type')->default('Item'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::table('shop_stock', function (Blueprint $table) { + $table->dropColumn('stock_type'); + }); + } +} diff --git a/database/migrations/2021_10_13_185131_make_shop_cost_float.php b/database/migrations/2021_10_13_185131_make_shop_cost_float.php new file mode 100644 index 0000000000..3c362d4b77 --- /dev/null +++ b/database/migrations/2021_10_13_185131_make_shop_cost_float.php @@ -0,0 +1,27 @@ +float('cost')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::table('shop_stock', function (Blueprint $table) { + $table->integer('cost')->change(); + }); + } +} diff --git a/database/migrations/2021_10_14_044223_make_shop_log_float.php b/database/migrations/2021_10_14_044223_make_shop_log_float.php new file mode 100644 index 0000000000..93e6a77cab --- /dev/null +++ b/database/migrations/2021_10_14_044223_make_shop_log_float.php @@ -0,0 +1,27 @@ +float('cost')->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + // + Schema::table('shop_log', function (Blueprint $table) { + $table->integer('cost')->change(); + }); + } +} diff --git a/database/migrations/2021_12_15_102154_add_visble_to_stock.php b/database/migrations/2021_12_15_102154_add_visble_to_stock.php new file mode 100644 index 0000000000..2592ff0694 --- /dev/null +++ b/database/migrations/2021_12_15_102154_add_visble_to_stock.php @@ -0,0 +1,27 @@ +boolean('is_visible')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shop_stock', function (Blueprint $table) { + // + $table->dropColumn('is_visible'); + }); + } +} diff --git a/database/migrations/2022_02_06_131334_add_restock_to_shop_stock.php b/database/migrations/2022_02_06_131334_add_restock_to_shop_stock.php new file mode 100644 index 0000000000..3f354eb38b --- /dev/null +++ b/database/migrations/2022_02_06_131334_add_restock_to_shop_stock.php @@ -0,0 +1,33 @@ +boolean('restock')->default(false); + $table->unsignedInteger('restock_quantity')->default(1); + $table->unsignedInteger('restock_interval')->default(2); + $table->boolean('range')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shop_stock', function (Blueprint $table) { + // + $table->dropColumn('restock'); + $table->dropColumn('restock_quantity'); + $table->dropColumn('restock_interval'); + $table->dropColumn('range'); + }); + } +} diff --git a/database/migrations/2022_03_01_094114_add_disallow_transfer_option_to_shop_stock.php b/database/migrations/2022_03_01_094114_add_disallow_transfer_option_to_shop_stock.php new file mode 100644 index 0000000000..2150e8d1fd --- /dev/null +++ b/database/migrations/2022_03_01_094114_add_disallow_transfer_option_to_shop_stock.php @@ -0,0 +1,27 @@ +boolean('disallow_transfer')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shop_stock', function (Blueprint $table) { + // + $table->dropColumn('disallow_transfer'); + }); + } +} diff --git a/database/migrations/2022_03_17_123003_add_allowed_coupons_to_shops.php b/database/migrations/2022_03_17_123003_add_allowed_coupons_to_shops.php new file mode 100644 index 0000000000..1903f62663 --- /dev/null +++ b/database/migrations/2022_03_17_123003_add_allowed_coupons_to_shops.php @@ -0,0 +1,27 @@ +string('allowed_coupons')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shops', function (Blueprint $table) { + // + $table->dropColumn('allowed_coupons'); + }); + } +} diff --git a/database/migrations/2022_05_16_211400_add_purchase_limit_timeframe_to_shop_stock.php b/database/migrations/2022_05_16_211400_add_purchase_limit_timeframe_to_shop_stock.php new file mode 100644 index 0000000000..c12fdbe564 --- /dev/null +++ b/database/migrations/2022_05_16_211400_add_purchase_limit_timeframe_to_shop_stock.php @@ -0,0 +1,25 @@ +text('purchase_limit_timeframe')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shop_stock', function (Blueprint $table) { + $table->dropColumn('purchase_limit_timeframe'); + }); + } +} diff --git a/database/migrations/2023_03_07_002536_add_timed_stock.php b/database/migrations/2023_03_07_002536_add_timed_stock.php new file mode 100644 index 0000000000..18e5afaa97 --- /dev/null +++ b/database/migrations/2023_03_07_002536_add_timed_stock.php @@ -0,0 +1,30 @@ +boolean('is_timed_stock')->default(false); + $table->timestamps(); + $table->timestamp('start_at')->nullable()->default(null); + $table->timestamp('end_at')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shop_stock', function (Blueprint $table) { + $table->dropColumn('is_timed_stock'); + $table->dropColumn('start_at'); + $table->dropColumn('start_at'); + }); + } +} diff --git a/database/migrations/2023_03_07_144637_add_timed_shop.php b/database/migrations/2023_03_07_144637_add_timed_shop.php new file mode 100644 index 0000000000..099fc0769f --- /dev/null +++ b/database/migrations/2023_03_07_144637_add_timed_shop.php @@ -0,0 +1,30 @@ +boolean('is_timed_shop')->default(false); + $table->timestamps(); + $table->timestamp('start_at')->nullable()->default(null); + $table->timestamp('end_at')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down() { + Schema::table('shops', function (Blueprint $table) { + $table->dropColumn('is_timed_shop'); + $table->dropColumn('start_at'); + $table->dropColumn('start_at'); + }); + } +} diff --git a/database/migrations/2024_07_20_185559_create_limit_tables.php b/database/migrations/2024_07_20_185559_create_limit_tables.php new file mode 100644 index 0000000000..36bc989f9e --- /dev/null +++ b/database/migrations/2024_07_20_185559_create_limit_tables.php @@ -0,0 +1,48 @@ +id(); + $table->string('name'); + $table->text('description')->nullable()->default(null); + $table->text('evaluation')->nullable()->default(null); + }); + + Schema::create('limits', function (Blueprint $table) { + $table->id(); + $table->string('object_model'); + $table->integer('object_id'); + + $table->string('limit_type'); + $table->integer('limit_id'); + $table->integer('quantity')->nullable()->default(null); + + $table->boolean('debit')->default(0); + $table->boolean('is_unlocked')->default(0); + }); + + Schema::create('user_unlocked_limits', function (Blueprint $table) { + $table->id(); + $table->integer('user_id'); + $table->string('object_model'); + $table->integer('object_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::dropIfExists('user_unlocked_limits'); + Schema::dropIfExists('dynamic_limits'); + Schema::dropIfExists('limits'); + } +}; diff --git a/database/migrations/2024_09_04_175143_create_shop_stock_cost_table.php b/database/migrations/2024_09_04_175143_create_shop_stock_cost_table.php new file mode 100644 index 0000000000..7365379ba6 --- /dev/null +++ b/database/migrations/2024_09_04_175143_create_shop_stock_cost_table.php @@ -0,0 +1,53 @@ +integer('shop_stock_id')->unsigned(); + $table->string('cost_type'); + $table->integer('cost_id')->unsigned(); + $table->integer('quantity'); + + $table->json('group')->nullable()->default(null); + }); + + // convert all of the existing costs to the new table + $stocks = DB::table('shop_stock')->get(); + foreach ($stocks as $stock) { + DB::table('shop_stock_costs')->insert([ + 'shop_stock_id' => $stock->id, + 'cost_type' => 'Currency', + 'cost_id' => $stock->currency_id, + 'quantity' => $stock->cost, + ]); + } + + Schema::table('shop_stock', function (Blueprint $table) { + $table->dropColumn('cost'); + $table->dropColumn('currency_id'); + + $table->json('data')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::table('shop_stock', function (Blueprint $table) { + $table->integer('currency_id')->unsigned(); + $table->integer('cost'); + + $table->dropColumn('data'); + }); + + Schema::dropIfExists('shop_stock_costs'); + } +}; diff --git a/database/migrations/2024_10_25_113803_add_hidden_shop_option.php b/database/migrations/2024_10_25_113803_add_hidden_shop_option.php new file mode 100644 index 0000000000..3086349347 --- /dev/null +++ b/database/migrations/2024_10_25_113803_add_hidden_shop_option.php @@ -0,0 +1,27 @@ +boolean('is_hidden')->default(false)->after('is_active'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + // + Schema::table('shops', function (Blueprint $table) { + $table->dropColumn('is_hidden'); + }); + } +}; diff --git a/database/migrations/2024_11_24_191058_fix_shop_logs.php b/database/migrations/2024_11_24_191058_fix_shop_logs.php new file mode 100644 index 0000000000..adb15ab932 --- /dev/null +++ b/database/migrations/2024_11_24_191058_fix_shop_logs.php @@ -0,0 +1,42 @@ +string('stock_type')->default('Item'); + $table->json('costs')->nullable()->default(null); + }); + + // convert existing costs and then drop currency_id table + $logs = DB::table('shop_log')->get(); + foreach ($logs as $log) { + $assets = createAssetsArray(false); + addAsset($assets, Currency::find($log->currency_id), $log->cost); + DB::table('shop_log')->where('id', $log->id)->update([ + 'costs' => $log->character_id ? ['character' => getDataReadyAssets($assets)] : ['user' => getDataReadyAssets($assets)], + ]); + } + + Schema::table('shop_log', function (Blueprint $table) { + $table->dropColumn('currency_id'); + // drop cost, then change costs to cost + $table->dropColumn('cost'); + $table->renameColumn('costs', 'cost'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + // not reversible + } +}; diff --git a/database/migrations/2024_11_25_214120_add_extra_timed_columns_to_shops.php b/database/migrations/2024_11_25_214120_add_extra_timed_columns_to_shops.php new file mode 100644 index 0000000000..ad560fb8f5 --- /dev/null +++ b/database/migrations/2024_11_25_214120_add_extra_timed_columns_to_shops.php @@ -0,0 +1,27 @@ +json('data')->nullable()->after('end_at'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void { + Schema::table('shops', function (Blueprint $table) { + // + $table->dropColumn('data'); + }); + } +}; diff --git a/format.sh b/format.sh new file mode 100644 index 0000000000..8ce9a3a439 --- /dev/null +++ b/format.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +find resources/views/ -type d | while read -r dir; do + blade-formatter --progress --write "$dir"/*.blade.php +done \ No newline at end of file diff --git a/package.json b/package.json index b7c0f75e4b..eed321e8f2 100644 --- a/package.json +++ b/package.json @@ -23,4 +23,4 @@ "sass": "^1.83.1", "sass-loader": "^16.0.4" } -} +} \ No newline at end of file diff --git a/resources/views/admin/items/tags/coupon.blade.php b/resources/views/admin/items/tags/coupon.blade.php new file mode 100644 index 0000000000..40ce1c1955 --- /dev/null +++ b/resources/views/admin/items/tags/coupon.blade.php @@ -0,0 +1,15 @@ +

Coupon

+ +

Discount

+

Input discount percent. You can select what coupons can be used in each shop on the shop edit page.

+ +
+
+ {!! Form::label('discount', 'Discount') !!} + {!! Form::number('discount', $tag->getData()['discount'], ['class' => 'form-control', 'placeholder' => 'Input Discount Percent', 'min' => 1, 'max' => 100]) !!} +
+
+ {!! Form::checkbox('infinite', 1, $tag->getData()['infinite'], ['class' => 'form-check-input', 'data-toggle' => 'toggle']) !!} + {!! Form::label('infinite', 'Should this coupon be unlimited use?', ['class' => 'ml-3 form-check-label']) !!} +
+
diff --git a/resources/views/admin/limits/_delete_limit.blade.php b/resources/views/admin/limits/_delete_limit.blade.php new file mode 100644 index 0000000000..09bea327f6 --- /dev/null +++ b/resources/views/admin/limits/_delete_limit.blade.php @@ -0,0 +1,14 @@ +@if ($limit) + {!! Form::open(['url' => 'admin/data/limits/delete/' . $limit->id]) !!} + +

You are about to delete the limit {{ $limit->name }}. This is not reversible.

+

Are you sure you want to delete {{ $limit->name }}?

+ +
+ {!! Form::submit('Delete Limit', ['class' => 'btn btn-danger']) !!} +
+ + {!! Form::close() !!} +@else + Invalid limit selected. +@endif diff --git a/resources/views/admin/limits/create_edit_limit.blade.php b/resources/views/admin/limits/create_edit_limit.blade.php new file mode 100644 index 0000000000..73c6c664d4 --- /dev/null +++ b/resources/views/admin/limits/create_edit_limit.blade.php @@ -0,0 +1,68 @@ +@extends('admin.layout') + +@section('admin-title') + {{ $limit->id ? 'Edit' : 'Create' }} Limit +@endsection + +@section('admin-content') + {!! breadcrumbs(['Admin Panel' => 'admin', 'Limits' => 'admin/data/limits', ($limit->id ? 'Edit' : 'Create') . ' Limit' => $limit->id ? 'admin/data/limits/edit/' . $limit->id : 'admin/data/limits/create']) !!} + +

{{ $limit->id ? 'Edit' : 'Create' }} Limit + @if ($limit->id) + Delete Limit + @endif +

+ + {!! Form::open(['url' => $limit->id ? 'admin/data/limits/edit/' . $limit->id : 'admin/data/limits/create', 'id' => 'form']) !!} + +

Basic Information

+ +
+ {!! Form::label('Name') !!} + {!! Form::text('name', $limit->name, ['class' => 'form-control']) !!} +
+ +
+ {!! Form::label('Description (Optional)') !!} + {!! Form::textarea('description', $limit->description, ['class' => 'form-control wysiwyg']) !!} +
+ +
+ +
Evalutation
+

Enter the PHP code that will be evaluated to determine if the limit is met. The code should return a boolean (true / false) value.

+

Laravel facades are accessible. For example, you can use Auth::user() to get the currently authenticated user.

+
+ + {!! Form::hidden('evaluation', $limit->evaluation, ['id' => 'evaluation']) !!} + +
+ {!! Form::submit($limit->id ? 'Edit' : 'Create', ['class' => 'btn btn-primary', 'id' => 'submit']) !!} +
+ + {!! Form::close() !!} +@endsection + +@section('scripts') + @parent + + + +@endsection diff --git a/resources/views/admin/limits/limits.blade.php b/resources/views/admin/limits/limits.blade.php new file mode 100644 index 0000000000..2149c3f62d --- /dev/null +++ b/resources/views/admin/limits/limits.blade.php @@ -0,0 +1,35 @@ +@extends('admin.layout') + +@section('admin-title') + Dynamic Limits +@endsection + +@section('admin-content') + {!! breadcrumbs(['Admin Panel' => 'admin', 'Limits' => 'admin/data/limits']) !!} + +

Dynamic Limits

+ +
Create New Limit
+ @if (!count($limits)) +

No limits found.

+ @else + + + @foreach ($limits as $limit) + + + + + + @endforeach + +
+ {!! $limit->name !!} + + {!! Str::limit($limit->description, 100) !!} + + Edit +
+ @endif + +@endsection diff --git a/resources/views/admin/prompts/create_edit_prompt.blade.php b/resources/views/admin/prompts/create_edit_prompt.blade.php index 0c522b9dcd..02380f8f2e 100644 --- a/resources/views/admin/prompts/create_edit_prompt.blade.php +++ b/resources/views/admin/prompts/create_edit_prompt.blade.php @@ -107,6 +107,8 @@ @include('widgets._loot_select_row', ['showLootTables' => true, 'showRaffles' => true]) @if ($prompt->id) + @include('widgets._add_limits', ['object' => $prompt]) +

Preview

diff --git a/resources/views/admin/shops/_delete_stock.blade.php b/resources/views/admin/shops/_delete_stock.blade.php new file mode 100644 index 0000000000..a9e452460d --- /dev/null +++ b/resources/views/admin/shops/_delete_stock.blade.php @@ -0,0 +1,14 @@ +@if ($stock) + {!! Form::open(['url' => 'admin/data/shops/stock/delete/' . $stock->id]) !!} + +

You are about to delete the stock {{ $stock->item?->name ?? 'deleted ' . $stock->stock_type }}.

+

Are you sure you want to delete {{ $stock->item->name ?? 'deleted ' . $stock->stock_type }}?

+ +
+ {!! Form::submit('Delete Stock', ['class' => 'btn btn-danger']) !!} +
+ + {!! Form::close() !!} +@else + Invalid stock selected. +@endif diff --git a/resources/views/admin/shops/_stock.blade.php b/resources/views/admin/shops/_stock.blade.php deleted file mode 100644 index 1eb87f2dd7..0000000000 --- a/resources/views/admin/shops/_stock.blade.php +++ /dev/null @@ -1,47 +0,0 @@ -
-
- -
- {!! Form::label('item_id[' . $key . ']', 'Item') !!} - {!! Form::select('item_id[' . $key . ']', $items, $stock ? $stock->item_id : null, ['class' => 'form-control stock-field', 'data-name' => 'item_id']) !!} -
-
- {!! Form::label('cost[' . $key . ']', 'Cost') !!} -
-
- {!! Form::text('cost[' . $key . ']', $stock ? $stock->cost : null, ['class' => 'form-control stock-field', 'data-name' => 'cost']) !!} -
-
- {!! Form::select('currency_id[' . $key . ']', $currencies, $stock ? $stock->currency_id : null, ['class' => 'form-control stock-field', 'data-name' => 'currency_id']) !!} -
-
-
- -
-
- {!! Form::checkbox('use_user_bank[' . $key . ']', 1, $stock ? $stock->use_user_bank : 1, ['class' => 'form-check-input stock-toggle stock-field', 'data-name' => 'use_user_bank']) !!} - {!! Form::label('use_user_bank[' . $key . ']', 'Use User Bank', ['class' => 'form-check-label ml-3']) !!} {!! add_help('This will allow users to purchase the item using the currency in their accounts, provided that users can own that currency.') !!} -
-
- {!! Form::checkbox('use_character_bank[' . $key . ']', 1, $stock ? $stock->use_character_bank : 1, ['class' => 'form-check-input stock-toggle stock-field', 'data-name' => 'use_character_bank']) !!} - {!! Form::label('use_character_bank[' . $key . ']', 'Use Character Bank', ['class' => 'form-check-label ml-3']) !!} {!! add_help('This will allow users to purchase the item using the currency belonging to characters they own, provided that characters can own that currency.') !!} -
-
-
- {!! Form::checkbox('is_limited_stock[' . $key . ']', 1, $stock ? $stock->is_limited_stock : false, ['class' => 'form-check-input stock-limited stock-toggle stock-field', 'data-name' => 'is_limited_stock']) !!} - {!! Form::label('is_limited_stock[' . $key . ']', 'Set Limited Stock', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned on, will limit the amount purchaseable to the quantity set below.') !!} -
-
-
-
- {!! Form::label('quantity[' . $key . ']', 'Quantity') !!} {!! add_help('If left blank, will be set to 0 (sold out).') !!} - {!! Form::text('quantity[' . $key . ']', $stock ? $stock->quantity : 0, ['class' => 'form-control stock-field', 'data-name' => 'quantity']) !!} -
-
-
-
- {!! Form::label('purchase_limit[' . $key . ']', 'User Purchase Limit') !!} {!! add_help('This is the maximum amount of this item a user can purchase from this shop. Set to 0 to allow infinite purchases.') !!} - {!! Form::text('purchase_limit[' . $key . ']', $stock ? $stock->purchase_limit : 0, ['class' => 'form-control stock-field', 'data-name' => 'purchase_limit']) !!} -
-
-
diff --git a/resources/views/admin/shops/_stock_cost.blade.php b/resources/views/admin/shops/_stock_cost.blade.php new file mode 100644 index 0000000000..e46a74acce --- /dev/null +++ b/resources/views/admin/shops/_stock_cost.blade.php @@ -0,0 +1 @@ +{!! Form::select('cost_id[]', $costItems, $cost->cost_id ?? null, ['class' => 'form-control cost-selectize']) !!} diff --git a/resources/views/admin/shops/_stock_item.blade.php b/resources/views/admin/shops/_stock_item.blade.php new file mode 100644 index 0000000000..086e147b96 --- /dev/null +++ b/resources/views/admin/shops/_stock_item.blade.php @@ -0,0 +1,19 @@ +@php + if (!isset($stock)) { + $stock = null; + } + $item_id = $stock ? ($stock->isCategory ? $stock->item_id . '-category' : ($stock->isRandom ? 'random' : $stock->item_id ?? null)) : null; +@endphp +{!! Form::label('Select Stock:') !!} +@if ($stock && $stock->isRandom) + + This Stock Is Currently Random. + +@endif +{!! Form::select('item_id', $items, $item_id, ['class' => 'form-control item-selectize']) !!} + + diff --git a/resources/views/admin/shops/_stock_modal.blade.php b/resources/views/admin/shops/_stock_modal.blade.php new file mode 100644 index 0000000000..2c7fb20c4d --- /dev/null +++ b/resources/views/admin/shops/_stock_modal.blade.php @@ -0,0 +1,358 @@ +
+ @if ($stock->id) + {!! Form::open(['url' => 'admin/data/shops/stock/edit/' . $stock->id]) !!} + @else + {!! Form::open(['url' => 'admin/data/shops/stock/' . $shop->id]) !!} + @endif + +
Stock
+

+ Random stock will select a random item from the list below on creation / edit. +
+ If a restock period is set and the stock is set to "random", it will select a new random stock of the chosen type. +
+ If a category exists for the chosen stock type, it can be used as a random filter. +

+
+
+ {!! Form::label('stock_type', 'Type') !!} + {!! Form::select('stock_type', ['Item' => 'Item'], $stock->stock_type ?? null, ['class' => 'form-control stock-field', 'placeholder' => 'Select Stock Type', 'id' => 'type']) !!} +
+
+ @if ($stock->id) + @include('admin.shops._stock_item', ['items' => $items, 'stock' => $stock]) + @endif +
+
+ +
Costs
+

+ You can select multiple costs for the item. Setting no costs will make the item free. +
+ By default, all costs are required to purchase the item unless they are assigned seperate groups. +

+
+
+
+ Add Cost +
+
+
+
+
Cost Type
+
Cost Object
+
Quantity
+
+ Group + {!! add_help('You can group costs together to allow users to choose which group they want to pay with.') !!} +
+
+ @foreach ($stock->costs ?? [] as $cost) +
+
+ {!! Form::select( + 'cost_type[]', + [ + 'Currency' => 'Currency', + ], + $cost->cost_type ?? null, + ['class' => 'form-control cost-type', 'placeholder' => 'Select Cost Type'], + ) !!} +
+
+ @include('admin.shops._stock_cost', [ + 'cost' => $cost, + 'costItems' => $cost->items, + ]) +
+
+ {!! Form::number('cost_quantity[]', $cost->quantity ?? 1, ['class' => 'form-control', 'min' => 1]) !!} +
+
+ {!! Form::number('group[]', $cost->group ?? 1, ['class' => 'form-control', 'min' => 1]) !!} +
+
+
+ +
+
+
+ @endforeach +
+
+ +
+ +
Coupon Usage
+ @if ($shop->use_coupons) +

You can set which groups can use coupons on this stock. Note that you must do this after creating the stock and groups!

+ @if ($stock->id) + @foreach ($stock->groups ?? [] as $group) +
+ {!! Form::checkbox('can_group_use_coupon[' . $group . ']', 1, $stock->canGroupUseCoupons($group), ['class' => 'form-check-input stock-field', 'data-toggle' => 'checkbox']) !!} + {!! Form::label('can_group_use_coupon[' . $group . ']', 'Allow group #' . $group . ' to use coupons', ['class' => 'form-check-label ml-3']) !!} +
+ @endforeach + @else +
You must create the stock before setting coupon usage.
+ @endif + @else +
Coupons are disabled on this shop.
+ @endif + +
+ +
+
+ {!! Form::label('purchase_limit', 'User Purchase Limit') !!} {!! add_help('This is the maximum amount of this item a user can purchase from this shop. Set to 0 to allow infinite purchases.') !!} + {!! Form::number('purchase_limit', $stock ? $stock->purchase_limit : 0, ['class' => 'form-control stock-field', 'data-name' => 'purchase_limit']) !!} +
+
+ {!! Form::label('purchase_limit_timeframe', 'Purchase Limit Timeout') !!} {!! add_help('This is the timeframe that the purchase limit will apply to. I.E. yearly will only look at purchases made after the beginning of the current year. Weekly starts on Sunday. Rollover will happen on UTC time.') !!} + {!! Form::select('purchase_limit_timeframe', ['lifetime' => 'Lifetime', 'yearly' => 'Yearly', 'monthly' => 'Monthly', 'weekly' => 'Weekly', 'daily' => 'Daily'], $stock ? $stock->purchase_limit_timeframe : 0, [ + 'class' => 'form-control stock-field', + 'data-name' => 'purchase_limit_timeframe', + 'placeholder' => 'Select Timeframe', + ]) !!} +
+
+ +
+
+ {!! Form::checkbox('use_user_bank', 1, $stock->use_user_bank ?? 1, ['class' => 'form-check-input stock-toggle stock-field', 'data-toggle' => 'checkbox', 'data-name' => 'use_user_bank']) !!} + {!! Form::label('use_user_bank', 'Use User Bank', ['class' => 'form-check-label ml-3']) !!} {!! add_help('This will allow users to purchase the item using the currency in their accounts, provided that users can own that currency.') !!} +
+
+ {!! Form::checkbox('use_character_bank', 1, $stock->use_character_bank ?? 1, ['class' => 'form-check-input stock-toggle stock-field', 'data-toggle' => 'checkbox', 'data-name' => 'use_character_bank']) !!} + {!! Form::label('use_character_bank', 'Use Character Bank', ['class' => 'form-check-label ml-3']) !!} {!! add_help('This will allow users to purchase the item using the currency belonging to characters they own, provided that characters can own that currency.') !!} +
+
+ {!! Form::checkbox('is_fto', 1, $stock->is_fto ?? 0, ['class' => 'form-check-input stock-toggle stock-field', 'data-toggle' => 'checkbox', 'data-name' => 'is_fto']) !!} + {!! Form::label('is_fto', 'FTO Only?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned on, only FTO will be able to purchase the item.') !!} +
+
+ {!! Form::checkbox('disallow_transfer', 1, $stock->disallow_transfer ?? 0, ['class' => 'form-check-input stock-toggle stock-field', 'data-name' => 'disallow_transfer']) !!} + {!! Form::label('disallow_transfer', 'Disallow Transfer', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned on, users will be unable to transfer this item after purchase.') !!} +
+
+ +
+ {!! Form::checkbox('is_visible', 1, $stock->is_visible ?? 1, ['class' => 'form-check-input stock-limited stock-toggle stock-field', 'data-toggle' => 'checkbox']) !!} + {!! Form::label('is_visible', 'Set Visibility', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned off it will not appear in the store.') !!} +
+
+ {!! Form::checkbox('is_limited_stock', 1, $stock->is_limited_stock ?? 0, ['class' => 'form-check-input stock-limited stock-toggle stock-field', 'data-toggle' => 'checkbox', 'id' => 'is_limited_stock']) !!} + {!! Form::label('is_limited_stock', 'Set Limited Stock', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned on, will limit the amount purchaseable to the quantity set below.') !!} +
+ +
+
+
+ {!! Form::label('quantity', 'Quantity') !!} {!! add_help('If left blank, will be set to 0 (sold out).') !!} + {!! Form::text('quantity', $stock->quantity ?? 0, ['class' => 'form-control stock-field']) !!} +
+
+ {!! Form::checkbox('restock', 1, $stock->restock ?? 0, ['class' => 'form-check-input', 'data-toggle' => 'checkbox']) !!} + {!! Form::label('restock', 'Auto Restock?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If ticked to yes it will auto restock at the interval defined below.') !!} +
+
+ {!! Form::label('restock_interval', 'Restock Interval') !!} + {!! Form::select('restock_interval', [1 => 'Day', 2 => 'Week', 3 => 'Month'], $stock->restock_interval ?? 2, ['class' => 'form-control stock-field']) !!} +
+
+ {!! Form::checkbox('range', 1, $stock->range ?? 0, ['class' => 'form-check-input', 'data-toggle' => 'checkbox']) !!} + {!! Form::label('range', 'Restock in Range?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If ticked to yes it will restock a random quantity between 1 and the quantity set above.') !!} +
+
+
+ +
+ {!! Form::checkbox('is_timed_stock', 1, $stock->is_timed_stock ?? 0, ['class' => 'form-check-input stock-timed stock-toggle stock-field', 'data-toggle' => 'checkbox', 'id' => 'is_timed_stock']) !!} + {!! Form::label('is_timed_stock', 'Set Timed Stock', ['class' => 'form-check-label ml-3']) !!} {!! add_help('Sets the stock as timed between the chosen dates.') !!} +
+
+
+

Stock Time Period

+

Both of the below options can work together. If both are set, the stock will only be available during the specific time period, and on the specific days of the week and months.

+ +
Specific Time Period
+

The time period below is between the specific dates and times, rather than an agnostic period like "every November".

+
+
+ {!! Form::label('stock_start_at', 'Start Time') !!} {!! add_help('Stock will cycle in at this date.') !!} + {!! Form::text('stock_start_at', $stock->start_at, ['class' => 'form-control datepicker']) !!} +
+
+ {!! Form::label('stock_end_at', 'End Time') !!} {!! add_help('Stock will cycle out at this date.') !!} + {!! Form::text('stock_end_at', $stock->end_at, ['class' => 'form-control datepicker']) !!} +
+
+ +
Repeating Time Period
+

Select the months and days of the week that the stock will be available.

+

If months are set alongside days, the stock will only be available on those days in those months.

+
+ {!! Form::label('stock_days', 'Days of the Week') !!} + {!! Form::select('stock_days[]', ['Monday' => 'Monday', 'Tuesday' => 'Tuesday', 'Wednesday' => 'Wednesday', 'Thursday' => 'Thursday', 'Friday' => 'Friday', 'Saturday' => 'Saturday', 'Sunday' => 'Sunday'], $stock->days ?? null, [ + 'class' => 'form-control selectize', + 'multiple' => 'multiple', + ]) !!} +
+
+ {!! Form::label('stock_months', 'Months of the Year') !!} + {!! Form::select( + 'stock_months[]', + [ + 'January' => 'January', + 'February' => 'February', + 'March' => 'March', + 'April' => 'April', + 'May' => 'May', + 'June' => 'June', + 'July' => 'July', + 'August' => 'August', + 'September' => 'September', + 'October' => 'October', + 'November' => 'November', + 'December' => 'December', + ], + $stock->months ?? null, + ['class' => 'form-control selectize', 'multiple' => 'multiple'], + ) !!} +
+
+
+ +
+ {!! Form::submit($stock->id ? 'Edit' : 'Create', ['class' => 'btn btn-primary']) !!} +
+ {!! Form::close() !!} + +
+
+
+ {!! Form::select( + 'cost_type[]', + [ + 'Currency' => 'Currency', + ], + null, + ['class' => 'form-control cost-type', 'placeholder' => 'Select Cost Type'], + ) !!} +
+
+ Select Cost Type +
+
+ {!! Form::number('cost_quantity[]', 1, ['class' => 'form-control', 'min' => 1]) !!} +
+
+ {!! Form::number('group[]', 1, ['class' => 'form-control', 'min' => 1]) !!} +
+
+
+ +
+
+
+
+
+ diff --git a/resources/views/admin/shops/create_edit_shop.blade.php b/resources/views/admin/shops/create_edit_shop.blade.php index 28cac4e3ed..7151e85220 100644 --- a/resources/views/admin/shops/create_edit_shop.blade.php +++ b/resources/views/admin/shops/create_edit_shop.blade.php @@ -43,9 +43,90 @@ {!! Form::textarea('description', $shop->description, ['class' => 'form-control wysiwyg']) !!}
+
+
+ {!! Form::checkbox('is_active', 1, $shop->id ? $shop->is_active : 1, ['class' => 'form-check-input', 'data-toggle' => 'toggle']) !!} + {!! Form::label('is_active', 'Set Active', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned off, the shop will not be visible to regular users.') !!} +
+
+ {!! Form::checkbox('is_hidden', 0, $shop->id ? $shop->is_hidden : 1, ['class' => 'form-check-input', 'data-toggle' => 'toggle']) !!} + {!! Form::label('is_hidden', 'Set Hidden', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned off, the shop will not be visible on the shop index, but still accessible.') !!} +
+
+ {!! Form::checkbox('is_staff', 1, $shop->id ? $shop->is_staff : 0, ['class' => 'form-check-input', 'data-toggle' => 'toggle']) !!} + {!! Form::label('is_staff', 'For Staff?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned on, the shop will not be visible to regular users, only staff.') !!} +
+
+ {!! Form::checkbox('is_fto', 1, $shop->id ? $shop->is_fto : 0, ['class' => 'form-check-label', 'data-toggle' => 'toggle']) !!} + {!! Form::label('is_fto', 'FTO Only?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('Only users who are currently FTO and staff can enter.') !!} +
+
+ +
+ {!! Form::checkbox('use_coupons', 1, $shop->id ? $shop->use_coupons : 0, ['class' => 'form-check-label', 'data-toggle' => 'toggle', 'id' => 'use_coupons']) !!} + {!! Form::label('use_coupons', 'Allow Coupons?', ['class' => 'form-check-label ml-3']) !!} {!! add_help('Note that ALL coupons will be allowed to be used, unless specified otherwise.') !!} +
+
+ {!! Form::label('allowed_coupons', 'Allowed Coupon(s)', ['class' => 'form-check-label']) !!} +

Leave blank to allow ALL coupons.

+ {!! Form::select('allowed_coupons[]', $coupons, json_decode($shop->allowed_coupons, 1), ['multiple', 'class' => 'form-check-label', 'placeholder' => 'Select Coupons', 'id' => 'allowed_coupons']) !!} +
+
- {!! Form::checkbox('is_active', 1, $shop->id ? $shop->is_active : 1, ['class' => 'form-check-input', 'data-toggle' => 'toggle']) !!} - {!! Form::label('is_active', 'Set Active', ['class' => 'form-check-label ml-3']) !!} {!! add_help('If turned off, the shop will not be visible to regular users.') !!} + {!! Form::checkbox('is_timed_shop', 1, $shop->is_timed_shop ?? 0, ['class' => 'form-check-input shop-timed shop-toggle shop-field', 'data-toggle' => 'toggle', 'id' => 'is_timed_shop']) !!} + {!! Form::label('is_timed_shop', 'Set Timed Shop', ['class' => 'form-check-label ml-3']) !!} {!! add_help('Sets the shop as timed between the chosen dates.') !!} +
+
+
+

Shop Time Period

+

Both of the below options can work together. If both are set, the shop will only be available during the specific time period, and on the specific days of the week and months.

+ +
Specific Time Period
+

The time period below is between the specific dates and times, rather than an agnostic period like "every November".

+
+
+ {!! Form::label('start_at', 'Start Time') !!} {!! add_help('The shop will cycle in at this date.') !!} + {!! Form::text('start_at', $shop->start_at, ['class' => 'form-control datepicker']) !!} +
+
+ {!! Form::label('end_at', 'End Time') !!} {!! add_help('The shop will cycle out at this date.') !!} + {!! Form::text('end_at', $shop->end_at, ['class' => 'form-control datepicker']) !!} +
+
+ +
Repeating Time Period
+

Select the months and days of the week that the shop will be available.

+

If months are set alongside days, the shop will only be available on those days in those months.

+
+ {!! Form::label('shop_days', 'Days of the Week') !!} + {!! Form::select('shop_days[]', ['Monday' => 'Monday', 'Tuesday' => 'Tuesday', 'Wednesday' => 'Wednesday', 'Thursday' => 'Thursday', 'Friday' => 'Friday', 'Saturday' => 'Saturday', 'Sunday' => 'Sunday'], $shop->days ?? null, [ + 'class' => 'form-control selectize', + 'multiple' => 'multiple', + ]) !!} +
+
+ {!! Form::label('shop_months', 'Months of the Year') !!} + {!! Form::select( + 'shop_months[]', + [ + 'January' => 'January', + 'February' => 'February', + 'March' => 'March', + 'April' => 'April', + 'May' => 'May', + 'June' => 'June', + 'July' => 'July', + 'August' => 'August', + 'September' => 'September', + 'October' => 'October', + 'November' => 'November', + 'December' => 'December', + ], + $shop->months ?? null, + ['class' => 'form-control selectize', 'multiple' => 'multiple'], + ) !!} +
+
@@ -55,33 +136,127 @@ {!! Form::close() !!} @if ($shop->id) +
+ + @include('widgets._add_limits', ['object' => $shop]) + +
+

Shop Stock

- {!! Form::open(['url' => 'admin/data/shops/stock/' . $shop->id]) !!} -
- @foreach ($shop->stock as $key => $stock) - @include('admin.shops._stock', ['stock' => $stock, 'key' => $key]) +
+ @foreach ($shop->stock as $stock) +
+
+
+
+ @if ($stock->item?->has_image) +
+ {{ $stock->item?->name }} +
+ @endif +
+ +
Cost: {!! $stock->displayCosts() ?? 'Free' !!}
+ @if (!$stock->is_visible) +
+ @endif + @if ($stock->is_timed_stock) + + @endif + @if ($stock->is_limited_stock) +
Stock: {{ $stock->quantity }}
+ @endif + @if ($stock->is_limited_stock) +
Restock: {!! $stock->restock ? '' : '' !!}
+ @endif + @if ($stock->purchase_limit) +
Max {{ $stock->purchase_limit }} + @if ($stock->purchase_limit_timeframe !== 'lifetime') + {{ $stock->purchase_limit_timeframe }} + @endif per user +
+ @endif + @if ($stock->disallow_transfer) +
Cannot be transferred
+ @endif +
+
+ @if ($stock->is_timed_stock) +
+ + {!! $stock->displayTime() !!} + +
+ @endif +
+ +
+ +
+
+
+
+
@endforeach
-
- {!! Form::submit('Edit', ['class' => 'btn btn-primary']) !!} -
- {!! Form::close() !!} -
- @include('admin.shops._stock', ['stock' => null, 'key' => 0]) -
@endif +
+ {!! Form::label('item_id', 'Item', ['class' => 'col-form-label']) !!} +
+ {!! Form::select('item_id[]', $items, null, ['class' => 'form-control', 'placeholder' => 'Select Item']) !!} +
+ Remove +
@endsection @section('scripts') @parent + @include('widgets._datetimepicker_js') @endsection diff --git a/resources/views/admin/shops/shops.blade.php b/resources/views/admin/shops/shops.blade.php index a4a1f06817..d6e85ac1d5 100644 --- a/resources/views/admin/shops/shops.blade.php +++ b/resources/views/admin/shops/shops.blade.php @@ -22,7 +22,13 @@ + @if ($shop->is_staff) + + @endif {!! $shop->displayName !!} + @if ($shop->is_timed_shop) + + @endif Edit diff --git a/resources/views/home/_prompt.blade.php b/resources/views/home/_prompt.blade.php index 3118314cd8..33bfa9649d 100644 --- a/resources/views/home/_prompt.blade.php +++ b/resources/views/home/_prompt.blade.php @@ -29,4 +29,9 @@
+ @if (count(getLimits($prompt))) + + @endif
diff --git a/resources/views/home/_prompt_requirements.blade.php b/resources/views/home/_prompt_requirements.blade.php new file mode 100644 index 0000000000..3ab7c899bb --- /dev/null +++ b/resources/views/home/_prompt_requirements.blade.php @@ -0,0 +1,18 @@ +@if (count(getLimits($prompt))) +
+ Warning: If you are submitting a prompt, you will not be able to edit the contents after + the submission has been made. +
+ Submitting to: {!! $prompt->displayName !!} +
+ @include('widgets._limits', ['object' => $prompt]) +
+ {!! Form::label('confirm', 'I understand that I will not be able to edit this submission after it has been made.', ['class' => 'alert alert-info']) !!} + {!! Form::checkbox('confirm', '1', false, ['class' => 'form-check-input', 'id' => 'confirm', 'required', 'data-on' => 'Yes', 'data-off' => 'No']) !!} +
+ + +@endif diff --git a/resources/views/home/create_submission.blade.php b/resources/views/home/create_submission.blade.php index f51adeeeeb..718fbcb299 100644 --- a/resources/views/home/create_submission.blade.php +++ b/resources/views/home/create_submission.blade.php @@ -40,6 +40,10 @@ If you aren't certain that you are ready, consider saving as a draft instead. Click the Confirm button to complete the {{ $isClaim ? 'claim' : 'submission' }}.

+ @if (!$isClaim) +
+
+ @endif
Confirm
@@ -95,11 +99,18 @@ @if (!$isClaim) var $prompt = $('#prompt'); var $rewards = $('#rewards'); + var $requirementsWarning = $('#requirementsWarning'); $prompt.selectize(); $prompt.on('change', function(e) { $rewards.load('{{ url('submissions/new/prompt') }}/' + $(this).val()); + $requirementsWarning.load('{{ url('submissions/new/prompt') }}/' + $(this).val() + '/requirements'); }); + + if ($prompt.val()) { + $rewards.load('{{ url('submissions/new/prompt') }}/' + $prompt.val()); + $requirementsWarning.load('{{ url('submissions/new/prompt') }}/' + $prompt.val() + '/requirements'); + } @endif $confirmButton.on('click', function(e) { @@ -111,6 +122,11 @@ $confirmSubmit.on('click', function(e) { e.preventDefault(); + let $confirm = $('#requirementsWarning').find('#confirm').length ? $('#requirementsWarning').find('#confirm').is(':checked') : true; + if ("{{ !$isClaim }}" && !$confirm) { + alert('You must confirm that you understand that you will not be able to edit this submission after it has been made.'); + return; + } $submissionForm.attr('action', '{{ url()->current() }}'); $submissionForm.submit(); }); diff --git a/resources/views/home/edit_submission.blade.php b/resources/views/home/edit_submission.blade.php index 79fb030aa0..b00f4a9905 100644 --- a/resources/views/home/edit_submission.blade.php +++ b/resources/views/home/edit_submission.blade.php @@ -45,6 +45,13 @@ If you aren't certain that you are ready, consider saving as a draft instead. Click the Confirm button to complete the {{ $isClaim ? 'claim' : 'submission' }}.

+ @if (!$isClaim) +
+ @if (count(getLimits($submission->prompt))) + @include('home._prompt_requirements', ['prompt' => $submission->prompt]) + @endif +
+ @endif
Confirm
@@ -121,10 +128,13 @@ @if (!$isClaim) var $prompt = $('#prompt'); var $rewards = $('#rewards'); + var $requirementsWarning = $('#requirementsWarning'); $prompt.selectize(); $prompt.on('change', function(e) { $rewards.load('{{ url('submissions/new/prompt') }}/' + $(this).val()); + $requirementsWarning.html(''); + $requirementsWarning.load('{{ url('submissions/new/prompt') }}/' + $(this).val() + '/requirements'); }); @endif @@ -138,6 +148,11 @@ $confirmSubmit.on('click', function(e) { e.preventDefault(); + let $confirm = $('#requirementsWarning').find('#confirm').length ? $('#requirementsWarning').find('#confirm').is(':checked') : true; + if ("{{ !$isClaim }}" && !$confirm) { + alert('You must confirm that you understand that you will not be able to edit this submission after it has been made.'); + return; + } $submissionForm.attr('action', '{{ url()->current() }}/submit'); $submissionForm.submit(); }); diff --git a/resources/views/home/submission.blade.php b/resources/views/home/submission.blade.php index 6b9b4b615c..494d19d3f7 100644 --- a/resources/views/home/submission.blade.php +++ b/resources/views/home/submission.blade.php @@ -12,9 +12,15 @@ @auth @if ($submission->user_id == Auth::user()->id && $submission->status == 'Pending') {!! Form::open(['url' => url()->current(), 'id' => 'submissionForm']) !!} -
- Cancel {{ $submission->prompt_id ? 'submission' : 'claim' }} -
+ @if ($isClaim || !count(getLimits($submission->prompt))) +
+ Cancel {{ $submission->prompt_id ? 'submission' : 'claim' }} +
+ @else +
+ This prompt cannot be edited after submission. If you need to make changes, please contact a staff member. +
+ @endif