refactor rule settings
This commit is contained in:
parent
c47578bd3e
commit
9834c52739
@ -0,0 +1,3 @@
|
||||
export interface RuleSettings {
|
||||
isActive: boolean;
|
||||
}
|
@ -1,15 +1,17 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { EvaluationResult } from './evaluation-result.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export interface RuleInterface {
|
||||
export interface RuleInterface<T extends RuleSettings> {
|
||||
evaluate(
|
||||
aPortfolioPositionMap: {
|
||||
[symbol: string]: PortfolioPosition;
|
||||
},
|
||||
aFees: number,
|
||||
aRuleSettingsMap: {
|
||||
[key: string]: any;
|
||||
}
|
||||
aRuleSettings: T
|
||||
): EvaluationResult;
|
||||
|
||||
getSettings(aUserSettings: UserSettings): T;
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
export interface UserSettings {
|
||||
baseCurrency: Currency;
|
||||
}
|
@ -445,10 +445,13 @@ export class Portfolio implements PortfolioInterface {
|
||||
};
|
||||
}
|
||||
|
||||
const fees = this.getFees();
|
||||
|
||||
return {
|
||||
rules: {
|
||||
accountClusterRisk: await this.rulesService.evaluate(
|
||||
this,
|
||||
details,
|
||||
fees,
|
||||
[
|
||||
new AccountClusterRiskInitialInvestment(
|
||||
this.exchangeRateDataService
|
||||
@ -461,7 +464,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
),
|
||||
currencyClusterRisk: await this.rulesService.evaluate(
|
||||
this,
|
||||
details,
|
||||
fees,
|
||||
[
|
||||
new CurrencyClusterRiskBaseCurrencyInitialInvestment(
|
||||
this.exchangeRateDataService
|
||||
@ -479,7 +483,8 @@ export class Portfolio implements PortfolioInterface {
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
),
|
||||
fees: await this.rulesService.evaluate(
|
||||
this,
|
||||
details,
|
||||
fees,
|
||||
[new FeeRatioInitialInvestment(this.exchangeRateDataService)],
|
||||
{ baseCurrency: this.user.Settings.currency }
|
||||
)
|
||||
|
@ -5,8 +5,10 @@ import { Currency } from '@prisma/client';
|
||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||
import { RuleInterface } from './interfaces/rule.interface';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export abstract class Rule implements RuleInterface {
|
||||
export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
|
||||
private name: string;
|
||||
|
||||
public constructor(
|
||||
@ -25,11 +27,11 @@ export abstract class Rule implements RuleInterface {
|
||||
[symbol: string]: PortfolioPosition;
|
||||
},
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
aRuleSettings: T
|
||||
): EvaluationResult;
|
||||
|
||||
public abstract getSettings(aUserSettings: UserSettings): T;
|
||||
|
||||
public getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||
export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Current Investment'
|
||||
@ -13,13 +15,8 @@ export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings?: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[AccountClusterRiskCurrentInvestment.name];
|
||||
|
||||
const accounts: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||
investment: number;
|
||||
@ -78,4 +75,17 @@ export class AccountClusterRiskCurrentInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
threshold: number;
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class AccountClusterRiskInitialInvestment extends Rule {
|
||||
export class AccountClusterRiskInitialInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Initial Investment'
|
||||
@ -13,13 +15,8 @@ export class AccountClusterRiskInitialInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings?: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[AccountClusterRiskInitialInvestment.name];
|
||||
|
||||
const platforms: {
|
||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & {
|
||||
investment: number;
|
||||
@ -78,4 +75,18 @@ export class AccountClusterRiskInitialInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: string;
|
||||
isActive: boolean;
|
||||
threshold: number;
|
||||
}
|
||||
|
@ -2,8 +2,10 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class AccountClusterRiskSingleAccount extends Rule {
|
||||
export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Single Account'
|
||||
@ -33,4 +35,10 @@ export class AccountClusterRiskSingleAccount extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): RuleSettings {
|
||||
return {
|
||||
isActive: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule {
|
||||
export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Current Investment: Base Currency'
|
||||
@ -13,13 +16,8 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[CurrencyClusterRiskBaseCurrencyCurrentInvestment.name];
|
||||
|
||||
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
|
||||
aPositions,
|
||||
'currency',
|
||||
@ -61,4 +59,15 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: Currency;
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule {
|
||||
export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Initial Investment: Base Currency'
|
||||
@ -13,13 +16,8 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[CurrencyClusterRiskBaseCurrencyInitialInvestment.name];
|
||||
|
||||
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
|
||||
aPositions,
|
||||
'currency',
|
||||
@ -62,4 +60,15 @@ export class CurrencyClusterRiskBaseCurrencyInitialInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: Currency;
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class CurrencyClusterRiskCurrentInvestment extends Rule {
|
||||
export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Current Investment'
|
||||
@ -13,13 +16,8 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[CurrencyClusterRiskCurrentInvestment.name];
|
||||
|
||||
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
|
||||
aPositions,
|
||||
'currency',
|
||||
@ -61,4 +59,17 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: Currency;
|
||||
threshold: number;
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class CurrencyClusterRiskInitialInvestment extends Rule {
|
||||
export class CurrencyClusterRiskInitialInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Initial Investment'
|
||||
@ -13,13 +16,8 @@ export class CurrencyClusterRiskInitialInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings: Settings
|
||||
) {
|
||||
const ruleSettings =
|
||||
aRuleSettingsMap[CurrencyClusterRiskInitialInvestment.name];
|
||||
|
||||
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
|
||||
aPositions,
|
||||
'currency',
|
||||
@ -61,4 +59,17 @@ export class CurrencyClusterRiskInitialInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: Currency;
|
||||
threshold: number;
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
import { UserSettings } from '@ghostfolio/api/models/interfaces/user-settings.interface';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
export class FeeRatioInitialInvestment extends Rule {
|
||||
export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||
public constructor(public exchangeRateDataService: ExchangeRateDataService) {
|
||||
super(exchangeRateDataService, {
|
||||
name: 'Initial Investment'
|
||||
@ -13,12 +16,8 @@ export class FeeRatioInitialInvestment extends Rule {
|
||||
public evaluate(
|
||||
aPositions: { [symbol: string]: PortfolioPosition },
|
||||
aFees: number,
|
||||
aRuleSettingsMap?: {
|
||||
[key: string]: any;
|
||||
}
|
||||
ruleSettings: Settings
|
||||
) {
|
||||
const ruleSettings = aRuleSettingsMap[FeeRatioInitialInvestment.name];
|
||||
|
||||
const positionsGroupedByCurrency = this.groupPositionsByAttribute(
|
||||
aPositions,
|
||||
'currency',
|
||||
@ -50,4 +49,17 @@ export class FeeRatioInitialInvestment extends Rule {
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getSettings(aUserSettings: UserSettings): Settings {
|
||||
return {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.01
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface Settings extends RuleSettings {
|
||||
baseCurrency: Currency;
|
||||
threshold: number;
|
||||
}
|
||||
|
@ -1,78 +1,30 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Portfolio } from '../models/portfolio';
|
||||
import { Rule } from '../models/rule';
|
||||
import { AccountClusterRiskCurrentInvestment } from '../models/rules/account-cluster-risk/current-investment';
|
||||
import { AccountClusterRiskInitialInvestment } from '../models/rules/account-cluster-risk/initial-investment';
|
||||
import { AccountClusterRiskSingleAccount } from '../models/rules/account-cluster-risk/single-account';
|
||||
import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '../models/rules/currency-cluster-risk/base-currency-current-investment';
|
||||
import { CurrencyClusterRiskBaseCurrencyInitialInvestment } from '../models/rules/currency-cluster-risk/base-currency-initial-investment';
|
||||
import { CurrencyClusterRiskCurrentInvestment } from '../models/rules/currency-cluster-risk/current-investment';
|
||||
import { CurrencyClusterRiskInitialInvestment } from '../models/rules/currency-cluster-risk/initial-investment';
|
||||
import { FeeRatioInitialInvestment } from '../models/rules/fees/fee-ratio-initial-investment';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
|
||||
@Injectable()
|
||||
export class RulesService {
|
||||
public constructor() {}
|
||||
|
||||
public async evaluate(
|
||||
aPortfolio: Portfolio,
|
||||
aRules: Rule[],
|
||||
aUserSettings: { baseCurrency: string }
|
||||
public async evaluate<T extends RuleSettings>(
|
||||
details: { [p: string]: PortfolioPosition },
|
||||
fees: number,
|
||||
aRules: Rule<T>[],
|
||||
aUserSettings: { baseCurrency: Currency }
|
||||
) {
|
||||
const defaultSettings = this.getDefaultRuleSettings(aUserSettings);
|
||||
const details = await aPortfolio.getDetails();
|
||||
|
||||
return aRules
|
||||
.filter((rule) => {
|
||||
return defaultSettings[rule.constructor.name]?.isActive;
|
||||
return rule.getSettings(aUserSettings)?.isActive;
|
||||
})
|
||||
.map((rule) => {
|
||||
const evaluationResult = rule.evaluate(
|
||||
details,
|
||||
aPortfolio.getFees(),
|
||||
defaultSettings
|
||||
fees,
|
||||
rule.getSettings(aUserSettings)
|
||||
);
|
||||
return { ...evaluationResult, name: rule.getName() };
|
||||
});
|
||||
}
|
||||
|
||||
private getDefaultRuleSettings(aUserSettings: { baseCurrency: string }) {
|
||||
return {
|
||||
[AccountClusterRiskCurrentInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[AccountClusterRiskInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[AccountClusterRiskSingleAccount.name]: { isActive: true },
|
||||
[CurrencyClusterRiskBaseCurrencyInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true
|
||||
},
|
||||
[CurrencyClusterRiskBaseCurrencyCurrentInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true
|
||||
},
|
||||
[CurrencyClusterRiskCurrentInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[CurrencyClusterRiskInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.5
|
||||
},
|
||||
[FeeRatioInitialInvestment.name]: {
|
||||
baseCurrency: aUserSettings.baseCurrency,
|
||||
isActive: true,
|
||||
threshold: 0.01
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user