diff --git a/web/app/Helpers/NumberHelper.php b/web/app/Helpers/NumberHelper.php
index dae2883..73ef585 100644
--- a/web/app/Helpers/NumberHelper.php
+++ b/web/app/Helpers/NumberHelper.php
@@ -26,6 +26,6 @@ class NumberHelper
break;
}
- return number_format($number / $divisor, 0) . $shorthand;
+ return number_format(floor($number / $divisor), 0) . $shorthand;
}
}
diff --git a/web/app/Http/Controllers/Api/ShopController.php b/web/app/Http/Controllers/Api/ShopController.php
index 7619bf8..47d5cde 100644
--- a/web/app/Http/Controllers/Api/ShopController.php
+++ b/web/app/Http/Controllers/Api/ShopController.php
@@ -2,12 +2,17 @@
namespace App\Http\Controllers\Api;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Str;
+
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Validator;
-use Illuminate\Support\Str;
+use App\Models\Transaction;
+use App\Models\TransactionType;
+use App\Models\UserAsset;
class ShopController extends Controller
{
@@ -89,4 +94,57 @@ class ShopController extends Controller
'data' => $data
]);
}
+
+ protected function purchase(Request $request, Asset $asset)
+ {
+ // TODO: XlXi: limiteds
+ $validator = Validator::make($request->all(), [
+ 'expectedPrice' => ['int']
+ ]);
+
+ if($validator->fails()) {
+ return ValidationHelper::generateValidatorError($validator);
+ }
+
+ $valid = $validator->valid();
+
+ $result = [
+ 'success' => false,
+ 'userFacingMessage' => null,
+ 'priceInTokens' => $asset->priceInTokens
+ ];
+ $price = $asset->priceInTokens;
+ $user = Auth::user();
+
+ if($asset->assetType->locked)
+ {
+ $result['userFacingMessage'] = 'This asset cannot be purchased.';
+ $result['priceInTokens'] = null;
+ return response($result);
+ }
+
+ if($user->hasAsset($asset->id))
+ {
+ $result['userFacingMessage'] = 'You already own this item.';
+ return response($result);
+ }
+
+ if($valid['expectedPrice'] != $price)
+ return response($result);
+
+ if($asset->priceInTokens > $user->tokens)
+ {
+ $result['userFacingMessage'] = 'You can\'t afford this item.';
+ return response($result);
+ }
+
+ $result['success'] = true;
+
+ Transaction::createAssetSale($user, $asset);
+ $user->removeFunds($price);
+ $asset->user->addFunds($price * (1-.3)); // XlXi: 30% tax
+ UserAsset::createSerialed($user->id, $asset->id);
+
+ return response($result);
+ }
}
diff --git a/web/app/Http/Controllers/Web/ClientController.php b/web/app/Http/Controllers/Web/ClientController.php
index 34a35a9..36a3d81 100644
--- a/web/app/Http/Controllers/Web/ClientController.php
+++ b/web/app/Http/Controllers/Web/ClientController.php
@@ -13,6 +13,7 @@ use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\AssetVersion;
use App\Models\RobloxAsset;
+use App\Models\UserAsset;
class ClientController extends Controller
{
@@ -43,16 +44,27 @@ class ClientController extends Controller
];
}
+ function userAssetValidator()
+ {
+ return [
+ 'userassetid' => [
+ 'required',
+ Rule::exists('App\Models\UserAsset', 'id')
+ ]
+ ];
+ }
+
function asset(Request $request)
{
- // TODO: XlXi: userAssetId (owned asset)
$reqData = array_change_key_case($request->all());
+ $isVersionIdRequest = array_key_exists('assetversionid', $reqData);
+ $isUserAssetIdRequest = array_key_exists('userassetid', $reqData);
$validatorRuleSet = 'assetRegularValidator';
- if(array_key_exists('assetversionid', $reqData))
+ if($isVersionIdRequest)
$validatorRuleSet = 'assetVersionValidator';
- elseif(array_key_exists('userassetid', $reqData))
- return response('todo');
+ elseif($isUserAssetIdRequest)
+ $validatorRuleSet = 'userAssetValidator';
$validator = Validator::make($reqData, $this->{$validatorRuleSet}());
@@ -68,20 +80,23 @@ class ClientController extends Controller
$valid = $validator->valid();
$asset = null;
- if(array_key_exists('assetversionid', $reqData)) {
+ if($isVersionIdRequest) {
$assetVersion = AssetVersion::where('id', $valid['assetversionid'])->first();
$asset = $assetVersion->asset;
$valid['version'] = $assetVersion->localVersion;
+ } elseif($isUserAssetIdRequest) {
+ $userAsset = UserAsset::where('id', $valid['userassetid'])->first();
+ $asset = $userAsset->asset;
} else {
$asset = Asset::where('id', $valid['id'])->first();
-
- if(!array_key_exists('version', $valid))
- $valid['version'] = 0;
}
+ if(!$isVersionIdRequest && !array_key_exists('version', $valid))
+ $valid['version'] = 0;
+
if($asset == null) {
- $validator->errors()->add('version', 'Unknown asset version.');
+ $validator->errors()->add('version', 'Unknown asset' . ($isVersionIdRequest ? ' version' : null) . '.');
return ValidationHelper::generateValidatorError($validator);
}
diff --git a/web/app/Http/Kernel.php b/web/app/Http/Kernel.php
index e07d5f9..47fa205 100644
--- a/web/app/Http/Kernel.php
+++ b/web/app/Http/Kernel.php
@@ -51,6 +51,8 @@ class Kernel extends HttpKernel
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
+ \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
+
\App\Http\Middleware\UserPunishmentMiddleware::class
],
];
diff --git a/web/app/Models/Punishment.php b/web/app/Models/Punishment.php
index 9cc3281..2e30c6a 100644
--- a/web/app/Models/Punishment.php
+++ b/web/app/Models/Punishment.php
@@ -72,7 +72,7 @@ class Punishment extends Model
if(!$this->expiration)
return 'Never';
- return $this->created_at->isoFormat('lll');
+ return $this->expiration->isoFormat('lll');
}
public static function activeFor($userId)
diff --git a/web/app/Models/Transaction.php b/web/app/Models/Transaction.php
new file mode 100644
index 0000000..01bc7b8
--- /dev/null
+++ b/web/app/Models/Transaction.php
@@ -0,0 +1,56 @@
+ TransactionType::where('name', 'Purchases')->first()->id
+ ]));
+ }
+
+ protected static function createSale($transaction)
+ {
+ return self::create(array_merge($transaction, [
+ 'transaction_type_id' => TransactionType::where('name', 'Sales')->first()->id
+ ]));
+ }
+
+ public static function createAssetSale($user, $asset, $placeId = null)
+ {
+ $transaction = [
+ 'asset_id' => $asset->id,
+ 'place_id' => $placeId,
+ 'user_id' => $user->id,
+ 'delta' => $asset->priceInTokens,
+ 'seller_id' => $asset->creatorId
+ ];
+
+ // XlXi: Assets have a 30% tax.
+ return [
+ self::createPurchase($transaction),
+ self::createSale(array_merge($transaction, ['delta' => $transaction['delta'] * (1-.3)]))
+ ];
+ }
+}
diff --git a/web/app/Models/TransactionType.php b/web/app/Models/TransactionType.php
new file mode 100644
index 0000000..6edd82d
--- /dev/null
+++ b/web/app/Models/TransactionType.php
@@ -0,0 +1,11 @@
+hasMany(Punishment::class, 'user_id');
}
+ public function hasAsset($assetId)
+ {
+ return UserAsset::where('asset_id', $assetId)
+ ->where('owner_id', $this->id)
+ ->exists();
+ }
+
+ public function removeFunds($delta)
+ {
+ $this->tokens -= $delta;
+ $this->save();
+ }
+
+ public function addFunds($delta)
+ {
+ $this->tokens += $delta;
+ $this->save();
+ }
+
public function _hasRolesetInternal($roleName)
{
$roleset = Roleset::where('Name', $roleName)->first();
diff --git a/web/app/Models/UserAsset.php b/web/app/Models/UserAsset.php
index 17c1ce1..a190090 100644
--- a/web/app/Models/UserAsset.php
+++ b/web/app/Models/UserAsset.php
@@ -8,4 +8,29 @@ use Illuminate\Database\Eloquent\Model;
class UserAsset extends Model
{
use HasFactory;
+
+ /**
+ * The attributes that are mass assignable.
+ *
+ * @var array
+ */
+ protected $fillable = [
+ 'owner_id',
+ 'asset_id',
+ 'serial'
+ ];
+
+ public function asset()
+ {
+ return $this->belongsTo(Asset::class, 'asset_id');
+ }
+
+ public static function createSerialed($ownerId, $assetId)
+ {
+ return self::create([
+ 'owner_id' => $ownerId,
+ 'asset_id' => $assetId,
+ 'serial' => self::where('asset_id', $assetId)->count()+1
+ ]);
+ }
}
diff --git a/web/database/migrations/2022_06_05_140351_create_assets_table.php b/web/database/migrations/2022_06_05_140351_create_assets_table.php
index 2fcc40d..d92ef31 100644
--- a/web/database/migrations/2022_06_05_140351_create_assets_table.php
+++ b/web/database/migrations/2022_06_05_140351_create_assets_table.php
@@ -29,7 +29,6 @@ return new class extends Migration
$table->unsignedBigInteger('downVotes')->default(0);
$table->unsignedBigInteger('priceInTokens')->default(15);
- $table->unsignedBigInteger('sales')->default(0);
$table->boolean('onSale')->default(false);
$table->unsignedSmallInteger('assetTypeId');
diff --git a/web/database/migrations/2023_01_07_221314_create_transactions_table.php b/web/database/migrations/2023_01_07_221314_create_transactions_table.php
new file mode 100644
index 0000000..29ea2e0
--- /dev/null
+++ b/web/database/migrations/2023_01_07_221314_create_transactions_table.php
@@ -0,0 +1,39 @@
+id();
+
+ $table->unsignedSmallInteger('transaction_type_id');
+ $table->unsignedBigInteger('asset_id')->nullable();
+ $table->unsignedBigInteger('place_id')->nullable();
+ $table->unsignedBigInteger('user_id');
+ $table->bigInteger('delta');
+ $table->unsignedBigInteger('seller_id');
+
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('transactions');
+ }
+};
diff --git a/web/database/migrations/2023_01_07_221336_create_transaction_types_table.php b/web/database/migrations/2023_01_07_221336_create_transaction_types_table.php
new file mode 100644
index 0000000..107f453
--- /dev/null
+++ b/web/database/migrations/2023_01_07_221336_create_transaction_types_table.php
@@ -0,0 +1,36 @@
+id();
+
+ $table->string('name');
+ $table->string('format');
+ $table->boolean('hidden')->default(false);
+
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('transaction_types');
+ }
+};
diff --git a/web/database/seeders/DatabaseSeeder.php b/web/database/seeders/DatabaseSeeder.php
index 96d6c36..6c50b19 100644
--- a/web/database/seeders/DatabaseSeeder.php
+++ b/web/database/seeders/DatabaseSeeder.php
@@ -19,7 +19,8 @@ class DatabaseSeeder extends Seeder
AssetTypeSeeder::class,
UsageCounterSeeder::class,
RolesetSeeder::class,
- PunishmentTypeSeeder::class
+ PunishmentTypeSeeder::class,
+ TransactionTypeSeeder::class
//FFlagSeeder::class
]);
}
diff --git a/web/database/seeders/TransactionTypeSeeder.php b/web/database/seeders/TransactionTypeSeeder.php
new file mode 100644
index 0000000..6007d60
--- /dev/null
+++ b/web/database/seeders/TransactionTypeSeeder.php
@@ -0,0 +1,25 @@
+ 'Purchases', 'format' => 'Purchased ']);
+ TransactionType::create(['name' => 'Sales', 'format' => 'Sold ']);
+ TransactionType::create(['name' => 'Commissions', 'format' => 'Sold ']); // third party sales
+ TransactionType::create(['name' => 'Group Payouts', 'format' => '']);
+ TransactionType::create(['name' => 'Admin Adjustment', 'format' => 'Tokens adjusted by administrator.', 'hidden' => true]);
+ }
+}
diff --git a/web/resources/js/components/PurchaseButton.js b/web/resources/js/components/PurchaseButton.js
index aa789e6..c85cbf2 100644
--- a/web/resources/js/components/PurchaseButton.js
+++ b/web/resources/js/components/PurchaseButton.js
@@ -3,12 +3,198 @@
*/
import { createRef, Component } from 'react';
+import axios from 'axios';
import classNames from 'classnames/bind';
+import ProgressiveImage from './ProgressiveImage';
+import { buildGenericApiUrl } from '../util/HTTP.js';
+
+axios.defaults.withCredentials = true;
const itemId = 'vb-item';
class PurchaseConfirmationModal extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ buttonDisabled: true
+ };
+
+ this.ModalRef = createRef();
+ this.Modal = null;
+
+ this.purchaseAsset = this.purchaseAsset.bind(this);
+ }
+
+ componentDidMount() {
+ let itemElement = document.getElementById(itemId);
+ if(itemElement) {
+ this.setState({
+ assetId: itemElement.getAttribute('data-asset-id'),
+ assetName: itemElement.getAttribute('data-asset-name'),
+ assetCreator: itemElement.getAttribute('data-asset-creator'),
+ assetType: itemElement.getAttribute('data-asset-type'),
+ assetPrice: parseInt(itemElement.getAttribute('data-asset-price')),
+ assetThumbnail: itemElement.getAttribute('data-asset-thumbnail-2d'),
+ userTokens: parseInt(itemElement.getAttribute('data-user-currency'))
+ });
+ }
+
+ this.Modal = new Bootstrap.Modal(this.ModalRef.current);
+ this.Modal.show();
+
+ this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
+ this.props.setModal(null);
+ })
+
+ setTimeout(function(){
+ this.setState({buttonDisabled: false});
+ }.bind(this), 1000);
+ }
+
+ componentWillUnmount() {
+ this.Modal.dispose();
+ }
+
+ purchaseAsset() {
+ console.log('hi');
+ if(this.state.buttonDisabled) return;
+
+ this.setState({buttonDisabled: true});
+
+ axios.post(buildGenericApiUrl('api', `shop/v1/purchase/${this.state.assetId}?expectedPrice=${this.state.assetPrice}`))
+ .then(res => {
+ if(res.data.success)
+ {
+ this.props.setModal( { res.data.userFacingMessage } This item's price has changed from { oldPrice } to { res.data.priceInTokens }. {newTokens >= 0 ? 'Would you still like to purchase?' : null } An unexpected error occurred while purchasing this item.
Would you like to purchase the { this.state.assetType } { this.state.assetName } from { this.state.assetCreator } for { this.state.assetPrice }?
+You will have { Math.max(0, (this.state.userTokens - this.state.assetPrice)) } after this purchase.
+You will have { this.props.newTokens } after this purchase.
+ > + : + + } +Would you like to purchase the { this.state.assetType } { this.state.assetName } from { this.state.assetCreator } for { this.state.assetPrice }?
-
- You will have { Math.max(0, (this.state.userTokens - this.state.assetPrice)) } after this purchase.
+You have successfully purchased { this.state.assetName } for { this.state.assetPrice }!
Your account has been {{ $punishment->isDeletion() ? 'closed ' : 'temporarily restricted' }} for violating our Terms of Service. diff --git a/web/resources/views/web/shop/asset.blade.php b/web/resources/views/web/shop/asset.blade.php index fd677d6..aee52eb 100644 --- a/web/resources/views/web/shop/asset.blade.php +++ b/web/resources/views/web/shop/asset.blade.php @@ -35,6 +35,9 @@ @section('content')
By {{ $asset->user->username }}
++ By {{ $asset->user->username }} + @if(Auth::user()->hasAsset($asset->id)) + + + + + Item Owned + + @endif +