Feat: Adds multi currency support (#88)
This commit is contained in:
@@ -23,23 +23,44 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
|
||||
public function quote(string $symbol): Quote
|
||||
{
|
||||
|
||||
$search = Alphavantage::core()->search($symbol);
|
||||
$search = Arr::get($search, 'bestMatches.0', null);
|
||||
|
||||
if (Arr::get($search, '9. matchScore') !== '1.0000') {
|
||||
throw new \Exception('Could not find ticker on Alphavantage');
|
||||
}
|
||||
|
||||
$quote = Alphavantage::core()->quoteEndpoint($symbol);
|
||||
$quote = Arr::get($quote, 'Global Quote', []);
|
||||
|
||||
$fundamental = cache()->remember(
|
||||
'av-symbol-'.$symbol,
|
||||
1440,
|
||||
function () use ($symbol) {
|
||||
return Alphavantage::fundamentals()->overview($symbol);
|
||||
function () use ($symbol, $search) {
|
||||
if (Arr::get($search, '3. type') === 'Equity') {
|
||||
|
||||
$fundamental = (array) Alphavantage::fundamentals()->overview($symbol);
|
||||
} else {
|
||||
|
||||
$fundamental = (array) Alphavantage::fundamentals()->etfProfile($symbol);
|
||||
|
||||
Arr::set($fundamental, 'DividendYield', Arr::get($fundamental, 'dividend_yield'));
|
||||
Arr::set($fundamental, 'MarketCapitalization', Arr::get($fundamental, 'net_assets'));
|
||||
Arr::set($fundamental, 'InceptionDate', Arr::get($fundamental, 'inception_date'));
|
||||
}
|
||||
|
||||
return $fundamental;
|
||||
}
|
||||
);
|
||||
|
||||
return new Quote([
|
||||
'name' => Arr::get($fundamental, 'Name'),
|
||||
'name' => Arr::get($search, '2. name'),
|
||||
'symbol' => $symbol,
|
||||
'market_value' => Arr::get($quote, '05. price'),
|
||||
'fifty_two_week_high' => Arr::get($fundamental, '52WeekHigh'),
|
||||
'fifty_two_week_low' => Arr::get($fundamental, '52WeekLow'),
|
||||
'market_value' => (float) Arr::get($quote, '05. price'),
|
||||
'currency' => Arr::get($search, '8. currency'),
|
||||
'fifty_two_week_high' => (float) Arr::get($fundamental, '52WeekHigh'),
|
||||
'fifty_two_week_low' => (float) Arr::get($fundamental, '52WeekLow'),
|
||||
'forward_pe' => Arr::get($fundamental, 'ForwardPE'),
|
||||
'trailing_pe' => Arr::get($fundamental, 'TrailingPE'),
|
||||
'market_cap' => Arr::get($fundamental, 'MarketCapitalization'),
|
||||
@@ -48,8 +69,20 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
? Arr::get($fundamental, 'DividendDate')
|
||||
: null,
|
||||
'dividend_yield' => Arr::get($fundamental, 'DividendYield') != 'None'
|
||||
? Arr::get($fundamental, 'DividendYield')
|
||||
? Arr::get($fundamental, 'DividendYield') * 100
|
||||
: null,
|
||||
'meta_data' => [
|
||||
'industry' => Arr::get($fundamental, 'Industry'),
|
||||
'country' => Arr::get($search, '4. region'),
|
||||
'exchange' => Arr::get($fundamental, 'Exchange'),
|
||||
'description' => Arr::get($fundamental, 'Description'),
|
||||
'asset_type' => Arr::get($search, '3. type'),
|
||||
'sector' => Arr::get($fundamental, 'Sector'),
|
||||
'first_trade_year' => Arr::get($fundamental, 'InceptionDate')
|
||||
? Carbon::parse(Arr::get($fundamental, 'InceptionDate'))->format('Y')
|
||||
: null,
|
||||
'source' => 'alphavantage',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -107,7 +140,7 @@ class AlphaVantageMarketData implements MarketDataInterface
|
||||
})
|
||||
->mapWithKeys(function ($history, $date) use ($symbol) {
|
||||
|
||||
$date = Carbon::parse($date)->format('Y-m-d');
|
||||
$date = Carbon::parse($date)->toDateString();
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use Carbon\CarbonPeriod;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -25,6 +26,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
return new Quote([
|
||||
'name' => 'ACME Company Ltd',
|
||||
'symbol' => $symbol,
|
||||
'currency' => 'USD',
|
||||
'market_value' => 230.19,
|
||||
'fifty_two_week_high' => 512.90,
|
||||
'fifty_two_week_low' => 341.20,
|
||||
@@ -34,6 +36,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
'book_value' => 4.7,
|
||||
'last_dividend_date' => now()->subDays(45),
|
||||
'dividend_yield' => 0.033,
|
||||
'meta_data' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -65,7 +68,7 @@ class FakeMarketData implements MarketDataInterface
|
||||
return collect([
|
||||
new Split([
|
||||
'symbol' => $symbol,
|
||||
'date' => now()->subMonths(36),
|
||||
'date' => now()->subMonths(12),
|
||||
'split_amount' => 10,
|
||||
]),
|
||||
]);
|
||||
@@ -73,16 +76,27 @@ class FakeMarketData implements MarketDataInterface
|
||||
|
||||
public function history(string $symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$numDays = Carbon::parse($startDate)->diffInDays($endDate, true);
|
||||
$endDate = now()->isBefore(Carbon::parse(config('investbrain.daily_change_time_of_day')))
|
||||
? now()->subDay()
|
||||
: now();
|
||||
|
||||
for ($i = 0; $i < $numDays; $i++) {
|
||||
$days = CarbonPeriod::create($startDate, $endDate)->filter('isWeekday');
|
||||
|
||||
$date = now()->subDays($i)->format('Y-m-d');
|
||||
$countOfDays = $days->count();
|
||||
|
||||
foreach ($days as $index => $date) {
|
||||
|
||||
$date = $date->toDateString();
|
||||
|
||||
$series[$date] = new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
'date' => $date,
|
||||
'close' => rand(150, 400),
|
||||
'open' => rand(150, 400),
|
||||
'high' => rand(150, 400),
|
||||
'low' => rand(150, 400),
|
||||
'close' => $index == $countOfDays - 1
|
||||
? 230.19 // most recent close should match current market value
|
||||
: rand(150, 400),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ class FallbackInterface
|
||||
foreach ($providers as $provider) {
|
||||
|
||||
$provider = trim($provider);
|
||||
$symbol = $arguments[0];
|
||||
|
||||
try {
|
||||
Log::warning("Calling method {$method} ({$provider})");
|
||||
Log::info("Calling method {$method} for {$symbol} ({$provider})");
|
||||
|
||||
if (! in_array($provider, array_keys(config('investbrain.interfaces', [])))) {
|
||||
|
||||
@@ -35,17 +36,17 @@ class FallbackInterface
|
||||
|
||||
$this->latest_error = $e->getMessage();
|
||||
|
||||
Log::warning("Failed calling method {$method} ({$provider}): {$this->latest_error}");
|
||||
Log::error("Failed calling method {$method} for {$symbol} ({$provider}): {$this->latest_error}");
|
||||
}
|
||||
}
|
||||
|
||||
// don't need to throw error if calling exists
|
||||
// don't need to throw error if calling exists method...
|
||||
if ($method == 'exists') {
|
||||
|
||||
// symbol prob just doesn't exist
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new \Exception("Could not get market data: {$this->latest_error}");
|
||||
throw new \Exception("Could not get market data calling method {$method}: {$this->latest_error}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use Finnhub\ObjectSerializer;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -35,32 +36,46 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
{
|
||||
$quote = $this->client->quote($symbol);
|
||||
|
||||
if (is_null(Arr::get($quote, 'd'))) {
|
||||
throw new \Exception('Could not find ticker on Finnhub');
|
||||
}
|
||||
|
||||
$fundamental = cache()->remember(
|
||||
'fh-symbol-'.$symbol,
|
||||
1440,
|
||||
function () use ($symbol) {
|
||||
return $this->client->companyBasicFinancials($symbol, 'all');
|
||||
|
||||
return array_merge(
|
||||
(array) ObjectSerializer::sanitizeForSerialization($this->client->companyProfile2($symbol)),
|
||||
(array) ObjectSerializer::sanitizeForSerialization($this->client->companyBasicFinancials($symbol, 'all')),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return new Quote([
|
||||
'name' => Arr::get($fundamental, 'metric.name'),
|
||||
'name' => Arr::get($fundamental, 'name'),
|
||||
'symbol' => $symbol,
|
||||
'currency' => Arr::get($fundamental, 'currency'),
|
||||
'market_value' => Arr::get($quote, 'c'),
|
||||
'fifty_two_week_high' => Arr::get($fundamental, 'metric.52WeekHigh'),
|
||||
'fifty_two_week_low' => Arr::get($fundamental, 'metric.52WeekLow'),
|
||||
'forward_pe' => Arr::get($fundamental, 'metric.forwardPE'), // confirm
|
||||
'trailing_pe' => Arr::get($fundamental, 'metric.trailingPE'), // confirm
|
||||
'market_cap' => Arr::get($fundamental, 'metric.marketCapitalization'), // confirm
|
||||
'book_value' => Arr::get($fundamental, 'metric.bookValuePerShare'), // confirm
|
||||
'last_dividend_date' => Arr::get($fundamental, 'metric.lastDivDate'), // confirm
|
||||
'dividend_yield' => Arr::get($fundamental, 'metric.dividendYield'), // confirm
|
||||
'forward_pe' => Arr::get($fundamental, 'metric.peAnnual'),
|
||||
'trailing_pe' => Arr::get($fundamental, 'metric.peTTM'),
|
||||
'market_cap' => Arr::get($fundamental, 'metric.marketCapitalization', 0) * 1000000,
|
||||
'book_value' => Arr::get($fundamental, 'metric.bookValuePerShareAnnual'),
|
||||
'dividend_yield' => Arr::get($fundamental, 'metric.dividendYieldIndicatedAnnual'),
|
||||
'meta_data' => [
|
||||
'country' => Arr::get($fundamental, 'country'),
|
||||
'exchange' => Arr::get($fundamental, 'exchange'),
|
||||
'first_trade_year' => Arr::get($fundamental, 'ipo') ? Carbon::parse(Arr::get($fundamental, 'ipo'))->format('Y') : null,
|
||||
'source' => 'finnhub',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function dividends($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
$dividends = $this->client->stockDividends($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
|
||||
$dividends = $this->client->stockDividends($symbol, $startDate->toDateString(), $endDate->toDateString());
|
||||
|
||||
return collect($dividends)->map(function ($dividend) use ($symbol) {
|
||||
|
||||
@@ -75,7 +90,7 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
public function splits($symbol, $startDate, $endDate): Collection
|
||||
{
|
||||
|
||||
$splits = $this->client->stockSplits($symbol, $startDate->format('Y-m-d'), $endDate->format('Y-m-d'));
|
||||
$splits = $this->client->stockSplits($symbol, $startDate->toDateString(), $endDate->toDateString());
|
||||
|
||||
return collect($splits)->map(function ($split) use ($symbol) {
|
||||
|
||||
@@ -96,7 +111,7 @@ class FinnhubMarketData implements MarketDataInterface
|
||||
$closes = Arr::get($history, 'c', []);
|
||||
|
||||
return collect($timestamps)->mapWithKeys(function ($timestamp, $index) use ($symbol, $closes) {
|
||||
$date = Carbon::createFromTimestamp($timestamp)->format('Y-m-d');
|
||||
$date = Carbon::createFromTimestamp($timestamp)->toDateString();
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
|
||||
@@ -21,7 +21,7 @@ class Dividend extends MarketDataType
|
||||
return $this->items['symbol'] ?? '';
|
||||
}
|
||||
|
||||
public function setDividendAmount($dividendAmount): self
|
||||
public function setDividendAmount(int|float $dividendAmount): self
|
||||
{
|
||||
$this->items['dividend_amount'] = (float) $dividendAmount;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -12,24 +13,79 @@ class MarketDataType extends Collection
|
||||
public function __construct($items = [])
|
||||
{
|
||||
|
||||
foreach ($this->getArrayableItems($items) as $key => $value) {
|
||||
$items = $this->getArrayableItems($items);
|
||||
|
||||
$this->{$key} = $value;
|
||||
foreach ($items as $key => $value) {
|
||||
|
||||
$this->validateRequiredTypes($key, $value);
|
||||
|
||||
if (! is_null($value)) {
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function __set($key, $value)
|
||||
{
|
||||
$this->{'set'.Str::studly($key)}($value);
|
||||
|
||||
$this->{$this->getSetMethodName($key)}($value);
|
||||
}
|
||||
|
||||
public function __get($key)
|
||||
{
|
||||
return $this->items[$key] ?? null;
|
||||
}
|
||||
|
||||
protected function getSetMethodName($key): string
|
||||
{
|
||||
return 'set'.Str::studly($key);
|
||||
}
|
||||
|
||||
protected function validateRequiredTypes($key, $value, $type = null): void
|
||||
{
|
||||
$method = new \ReflectionMethod($this, $this->getSetMethodName($key));
|
||||
$params = $method->getParameters();
|
||||
|
||||
// no required type
|
||||
if (is_null($type) && is_null($type = $params[0]->getType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// can`t validate a mixed type
|
||||
if ($type == 'mixed') {
|
||||
return;
|
||||
}
|
||||
|
||||
// has a union type, let's iterate
|
||||
if ($type instanceof \ReflectionUnionType) {
|
||||
|
||||
foreach ($type->getTypes() as $subType) {
|
||||
$expected[] = $subType;
|
||||
|
||||
try {
|
||||
$this->validateRequiredTypes($key, $value, $subType);
|
||||
|
||||
return;
|
||||
} catch (\InvalidArgumentException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check type
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$expected = $type->getName();
|
||||
|
||||
if (get_debug_type($value) == $expected || ($type->allowsNull() && $value === null)) {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (class_exists($expected) && is_subclass_of(get_debug_type($value), $expected)) {
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException("Invalid type for {$key}. Expected ".implode('|', array_map(fn ($t) => $t, Arr::wrap($expected))).' but got '.get_debug_type($value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class Ohlc extends MarketDataType
|
||||
return $this->items['symbol'] ?? '';
|
||||
}
|
||||
|
||||
public function setOpen($open): self
|
||||
public function setOpen(int|float $open): self
|
||||
{
|
||||
$this->items['open'] = (float) $open;
|
||||
|
||||
@@ -33,7 +33,7 @@ class Ohlc extends MarketDataType
|
||||
return $this->items['open'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setHigh($high): self
|
||||
public function setHigh(int|float $high): self
|
||||
{
|
||||
$this->items['high'] = (float) $high;
|
||||
|
||||
@@ -45,7 +45,7 @@ class Ohlc extends MarketDataType
|
||||
return $this->items['high'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setLow($low): self
|
||||
public function setLow(int|float $low): self
|
||||
{
|
||||
$this->items['low'] = (float) $low;
|
||||
|
||||
@@ -57,7 +57,7 @@ class Ohlc extends MarketDataType
|
||||
return $this->items['low'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setClose($close): self
|
||||
public function setClose(int|float $close): self
|
||||
{
|
||||
$this->items['close'] = (float) $close;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Interfaces\MarketData\Types;
|
||||
|
||||
use DateTime;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class Quote extends MarketDataType
|
||||
@@ -35,7 +36,19 @@ class Quote extends MarketDataType
|
||||
return $this->items['symbol'] ?? '';
|
||||
}
|
||||
|
||||
public function setMarketValue($marketValue): self
|
||||
public function setCurrency(string $currency): self
|
||||
{
|
||||
$this->items['currency'] = strtoupper((string) $currency);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrency(): string
|
||||
{
|
||||
return $this->items['currency'] ?? '';
|
||||
}
|
||||
|
||||
public function setMarketValue(int|float $marketValue): self
|
||||
{
|
||||
$this->items['market_value'] = (float) $marketValue;
|
||||
|
||||
@@ -97,6 +110,7 @@ class Quote extends MarketDataType
|
||||
|
||||
public function setMarketCap($cap): self
|
||||
{
|
||||
// return $this;
|
||||
$this->items['market_cap'] = (int) $cap;
|
||||
|
||||
return $this;
|
||||
@@ -119,6 +133,18 @@ class Quote extends MarketDataType
|
||||
return $this->items['book_value'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setLastDividendAmount($value): self
|
||||
{
|
||||
$this->items['last_dividend_amount'] = (float) $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastDividendAmount(): float
|
||||
{
|
||||
return $this->items['last_dividend_amount'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setLastDividendDate(mixed $date): self
|
||||
{
|
||||
$this->items['last_dividend_date'] = is_null($date) ? null : Carbon::parse($date)->format('Y-m-d H:i:s');
|
||||
@@ -142,4 +168,28 @@ class Quote extends MarketDataType
|
||||
{
|
||||
return $this->items['dividend_yield'] ?? 0.0;
|
||||
}
|
||||
|
||||
public function setMetaData(array $meta_data): self
|
||||
{
|
||||
$defaults = [
|
||||
'sector' => null,
|
||||
'industry' => null,
|
||||
'country' => null,
|
||||
'exchange' => null,
|
||||
'description' => null,
|
||||
'asset_type' => null,
|
||||
'first_trade_year' => null,
|
||||
'source' => null,
|
||||
];
|
||||
|
||||
// merges the NEW values with highest priority over previous values and defaults
|
||||
$this->items['meta_data'] = array_merge($defaults, $this->items['meta_data'] ?? [], Arr::skipEmptyValues($meta_data));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMetaData(): array
|
||||
{
|
||||
return $this->items['meta_data'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class Split extends MarketDataType
|
||||
return $this->items['symbol'] ?? '';
|
||||
}
|
||||
|
||||
public function setSplitAmount($splitAmount): self
|
||||
public function setSplitAmount(int|float $splitAmount): self
|
||||
{
|
||||
$this->items['split_amount'] = (float) $splitAmount;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Interfaces\MarketData\Types\Dividend;
|
||||
use App\Interfaces\MarketData\Types\Ohlc;
|
||||
use App\Interfaces\MarketData\Types\Quote;
|
||||
use App\Interfaces\MarketData\Types\Split;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Scheb\YahooFinanceApi\ApiClient;
|
||||
use Scheb\YahooFinanceApi\ApiClientFactory as YahooFinance;
|
||||
@@ -34,9 +35,14 @@ class YahooMarketData implements MarketDataInterface
|
||||
|
||||
$quote = $this->client->getQuote($symbol);
|
||||
|
||||
if (is_null($quote?->getRegularMarketPrice())) {
|
||||
throw new \Exception('Could not find ticker on Yahoo');
|
||||
}
|
||||
|
||||
return new Quote([
|
||||
'name' => $quote?->getLongName() ?? $quote?->getShortName(),
|
||||
'symbol' => $symbol,
|
||||
'currency' => $quote?->getCurrency(),
|
||||
'market_value' => $quote?->getRegularMarketPrice(),
|
||||
'fifty_two_week_high' => $quote?->getFiftyTwoWeekHigh(),
|
||||
'fifty_two_week_low' => $quote?->getFiftyTwoWeekLow(),
|
||||
@@ -46,6 +52,11 @@ class YahooMarketData implements MarketDataInterface
|
||||
'book_value' => $quote?->getBookValue(),
|
||||
'last_dividend_date' => $quote?->getDividendDate(),
|
||||
'dividend_yield' => $quote?->getTrailingAnnualDividendYield() * 100,
|
||||
'meta_data' => [
|
||||
'exchange' => $quote?->getExchange(),
|
||||
'asset_type' => $quote?->getQuoteType(),
|
||||
'source' => 'yahoo',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -84,7 +95,7 @@ class YahooMarketData implements MarketDataInterface
|
||||
return collect($this->client->getHistoricalQuoteData($symbol, ApiClient::INTERVAL_1_DAY, $startDate, $endDate))
|
||||
->mapWithKeys(function ($history) use ($symbol) {
|
||||
|
||||
$date = $history->getDate()->format('Y-m-d');
|
||||
$date = Carbon::parse($history->getDate())->toDateString();
|
||||
|
||||
return [$date => new Ohlc([
|
||||
'symbol' => $symbol,
|
||||
|
||||
Reference in New Issue
Block a user