diff --git a/app/Http/Controllers/CollectionController.php b/app/Http/Controllers/CollectionController.php index 04ae98259..c8adb505f 100644 --- a/app/Http/Controllers/CollectionController.php +++ b/app/Http/Controllers/CollectionController.php @@ -21,6 +21,7 @@ use App\Enums\Chain; use App\Enums\CurrencyCode; use App\Enums\NftTransferType; +use App\Enums\Period; use App\Enums\TokenType; use App\Enums\TraitDisplayType; use App\Http\Controllers\Traits\HasCollectionFilters; @@ -149,15 +150,15 @@ private function getPopularCollections(Request $request): Paginator $currency = $user ? $user->currency() : CurrencyCode::USD; - $volumeColumn = match ($request->query('period')) { - '7d' => 'avg_volume_7d', - '30d' => 'avg_volume_30d', - default => 'avg_volume_1d', + $period = match ($request->query('period')) { + '7d' => Period::WEEK, + '30d' => Period::MONTH, + default => Period::DAY, }; /** @var Paginator $collections */ $collections = Collection::query() - ->when($request->query('sort') !== 'floor-price', fn ($q) => $q->orderByWithNulls($volumeColumn.'::numeric', 'desc')) + ->when($request->query('sort') !== 'floor-price', fn ($q) => $q->orderByVolume($period)) ->filterByChainId($chainId) ->orderByFloorPrice('desc', $currency) ->with([ diff --git a/app/Http/Controllers/PopularCollectionController.php b/app/Http/Controllers/PopularCollectionController.php index e131705c8..22e718b9a 100644 --- a/app/Http/Controllers/PopularCollectionController.php +++ b/app/Http/Controllers/PopularCollectionController.php @@ -8,6 +8,7 @@ use App\Data\Collections\CollectionStatsData; use App\Enums\Chain; use App\Enums\CurrencyCode; +use App\Enums\Period; use App\Http\Controllers\Traits\HasCollectionFilters; use App\Models\Collection; use App\Models\Nft; @@ -42,9 +43,15 @@ public function index(Request $request): Response|JsonResponse|RedirectResponse $sortDirection = in_array($request->query('direction'), ['asc', 'desc']) ? $request->query('direction') : $defaultSortDirection; + $period = match ($request->query('period')) { + '7d' => Period::WEEK, + '30d' => Period::MONTH, + default => Period::DAY, + }; + $collections = Collection::query() ->searchByName($request->get('query')) - ->when($sortBy === null, fn ($q) => $q->orderBy('volume', 'desc')) // @TODO handle top sorting + ->when($sortBy === null, fn ($q) => $q->orderByVolume($period)) ->when($sortBy === 'name', fn ($q) => $q->orderByName($sortDirection)) ->when($sortBy === 'value', fn ($q) => $q->orderByValue(null, $sortDirection, $currency)) ->when($sortBy === 'floor-price', fn ($q) => $q->orderByFloorPrice($sortDirection, $currency)) diff --git a/app/Models/Collection.php b/app/Models/Collection.php index 1be14691e..14c4c54cc 100644 --- a/app/Models/Collection.php +++ b/app/Models/Collection.php @@ -9,11 +9,11 @@ use App\Enums\TokenType; use App\Models\Traits\BelongsToNetwork; use App\Models\Traits\HasFloorPriceHistory; +use App\Models\Traits\HasVolume; use App\Models\Traits\HasWalletVotes; use App\Models\Traits\Reportable; use App\Notifications\CollectionReport; use App\Support\BlacklistedCollections; -use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -43,7 +43,8 @@ */ class Collection extends Model { - use BelongsToNetwork, HasEagerLimit, HasFactory, HasFloorPriceHistory, HasSlug, HasWalletVotes, Reportable, SoftDeletes; + use BelongsToNetwork, HasEagerLimit, HasFactory, HasFloorPriceHistory, + HasSlug, HasVolume, HasWalletVotes, Reportable, SoftDeletes; const TWITTER_URL = 'https://x.com/'; @@ -702,22 +703,6 @@ public static function getFiatValueSum(): array ); } - /** - * @return HasMany - */ - public function volumes(): HasMany - { - return $this->hasMany(TradingVolume::class); - } - - public function averageVolumeSince(Carbon $date): string - { - return $this->volumes() - ->selectRaw('avg(volume::numeric) as aggregate') - ->where('created_at', '>', $date) - ->value('aggregate'); - } - /** * Modify the query to apply ORDER BY, but with respect to NULL values. * If direction is ascending, the query will order NULL values first, diff --git a/app/Models/Traits/HasVolume.php b/app/Models/Traits/HasVolume.php new file mode 100644 index 000000000..0af84abe6 --- /dev/null +++ b/app/Models/Traits/HasVolume.php @@ -0,0 +1,65 @@ + + */ + public function volumes(): HasMany + { + return $this->hasMany(TradingVolume::class); + } + + /** + * Calculate the average collection volume since the given time period. + */ + public function averageVolumeSince(Carbon $date): string + { + return $this->volumes() + ->selectRaw('avg(volume::numeric) as aggregate') + ->where('created_at', '>', $date) + ->value('aggregate'); + } + + /** + * Get the collection volume, but respecting the volume in the given period. + * If no period is provided, get the total collection volume. + */ + public function getVolume(?Period $period = null): ?string + { + return match ($period) { + Period::DAY => $this->avg_volume_1d, + Period::WEEK => $this->avg_volume_7d, + Period::MONTH => $this->avg_volume_30d, + default => $this->total_volume, + }; + } + + /** + * Scope the query to order the collections by the volume, but respecting the volume in the given period. + * + * @param Builder $query + * @return Builder + */ + public function scopeOrderByVolume(Builder $query, ?Period $period = null): Builder + { + $column = match ($period) { + Period::DAY => 'avg_volume_1d', + Period::WEEK => 'avg_volume_7d', + Period::MONTH => 'avg_volume_30d', + default => 'total_volume', + }; + + return $query->orderByWithNulls($column.'::numeric', 'desc'); + } +} diff --git a/tests/App/Models/CollectionTest.php b/tests/App/Models/CollectionTest.php index 02b33072e..202c1046b 100644 --- a/tests/App/Models/CollectionTest.php +++ b/tests/App/Models/CollectionTest.php @@ -4,6 +4,7 @@ use App\Enums\CurrencyCode; use App\Enums\NftTransferType; +use App\Enums\Period; use App\Jobs\ResetCollectionRanking; use App\Models\Article; use App\Models\Collection; @@ -1525,6 +1526,20 @@ ]); }); +it('can get volume based on the period', function () { + $collection = Collection::factory()->create([ + 'total_volume' => '1', + 'avg_volume_1d' => '2', + 'avg_volume_7d' => '3', + 'avg_volume_30d' => '4', + ]); + + expect($collection->getVolume())->toBe('1'); + expect($collection->getVolume(Period::DAY))->toBe('2'); + expect($collection->getVolume(Period::WEEK))->toBe('3'); + expect($collection->getVolume(Period::MONTH))->toBe('4'); +}); + it('can get native token for a network', function () { $token = Token::factory()->matic()->create([ 'is_native_token' => true,