fixed bff compilation
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||||
|
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ func PaymentQuotesResponse(logger mlogger.Logger, resp *quotationv1.QuotePayment
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Payments wraps a list of payments with refreshed access token.
|
// Payments wraps a list of payments with refreshed access token.
|
||||||
func PaymentsResponse(logger mlogger.Logger, payments []*orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
func PaymentsResponse(logger mlogger.Logger, payments []*sharedv1.Payment, token *TokenData) http.HandlerFunc {
|
||||||
return response.Ok(logger, paymentsResponse{
|
return response.Ok(logger, paymentsResponse{
|
||||||
Payments: toPayments(payments),
|
Payments: toPayments(payments),
|
||||||
authResponse: authResponse{AccessToken: *token},
|
authResponse: authResponse{AccessToken: *token},
|
||||||
@@ -127,7 +128,7 @@ func PaymentsListResponse(logger mlogger.Logger, resp *orchestratorv1.ListPaymen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Payment wraps a payment with refreshed access token.
|
// Payment wraps a payment with refreshed access token.
|
||||||
func PaymentResponse(logger mlogger.Logger, payment *orchestratorv1.Payment, token *TokenData) http.HandlerFunc {
|
func PaymentResponse(logger mlogger.Logger, payment *sharedv1.Payment, token *TokenData) http.HandlerFunc {
|
||||||
return response.Ok(logger, paymentResponse{
|
return response.Ok(logger, paymentResponse{
|
||||||
Payment: toPayment(payment),
|
Payment: toPayment(payment),
|
||||||
authResponse: authResponse{AccessToken: *token},
|
authResponse: authResponse{AccessToken: *token},
|
||||||
@@ -230,7 +231,7 @@ func toPaymentQuotes(resp *quotationv1.QuotePaymentsResponse) *PaymentQuotes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPayments(items []*orchestratorv1.Payment) []Payment {
|
func toPayments(items []*sharedv1.Payment) []Payment {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -246,7 +247,7 @@ func toPayments(items []*orchestratorv1.Payment) []Payment {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func toPayment(p *orchestratorv1.Payment) *Payment {
|
func toPayment(p *sharedv1.Payment) *Payment {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (a *PaymentAPI) listPayments(r *http.Request, account *model.Account, token
|
|||||||
req.FilterStates = states
|
req.FilterStates = states
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.ListPayments(ctx, req)
|
resp, err := a.execution.ListPayments(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
a.logger.Warn("Failed to list payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||||
return response.Auto(a.logger, a.Name(), err)
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
|
|||||||
Metadata: payload.Metadata,
|
Metadata: payload.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.InitiatePayment(ctx, req)
|
resp, err := a.execution.InitiatePayment(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to initiate payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
a.logger.Warn("Failed to initiate payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
|
||||||
return response.Auto(a.logger, a.Name(), err)
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
|
|||||||
Metadata: payload.Metadata,
|
Metadata: payload.Metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.InitiatePayments(ctx, req)
|
resp, err := a.execution.InitiatePayments(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
return response.Auto(a.logger, a.Name(), err)
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||||
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
|
||||||
"github.com/tech/sendico/server/interface/api/srequest"
|
"github.com/tech/sendico/server/interface/api/srequest"
|
||||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||||
@@ -59,7 +60,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
|
|||||||
Intent: intent,
|
Intent: intent,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.QuotePayment(ctx, req)
|
resp, err := a.quotation.QuotePayment(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
return response.Auto(a.logger, a.Name(), err)
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
@@ -107,7 +108,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
|||||||
intents = append(intents, intent)
|
intents = append(intents, intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := "ationv1.QuotePaymentResponse{
|
req := "ationv1.QuotePaymentsRequest{
|
||||||
Meta: &sharedv1.RequestMeta{
|
Meta: &sharedv1.RequestMeta{
|
||||||
OrganizationRef: orgRef.Hex(),
|
OrganizationRef: orgRef.Hex(),
|
||||||
},
|
},
|
||||||
@@ -116,7 +117,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
|
|||||||
PreviewOnly: payload.PreviewOnly,
|
PreviewOnly: payload.PreviewOnly,
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.QuotePayments(ctx, req)
|
resp, err := a.quotation.QuotePayments(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
|
||||||
return response.Auto(a.logger, a.Name(), err)
|
return response.Auto(a.logger, a.Name(), err)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package paymentapiimp
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -18,24 +19,33 @@ import (
|
|||||||
"github.com/tech/sendico/pkg/mlogger"
|
"github.com/tech/sendico/pkg/mlogger"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1"
|
||||||
|
quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1"
|
||||||
eapi "github.com/tech/sendico/server/interface/api"
|
eapi "github.com/tech/sendico/server/interface/api"
|
||||||
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
mutil "github.com/tech/sendico/server/internal/mutil/param"
|
||||||
"go.mongodb.org/mongo-driver/v2/bson"
|
"go.mongodb.org/mongo-driver/v2/bson"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
)
|
)
|
||||||
|
|
||||||
type paymentClient interface {
|
type executionClient interface {
|
||||||
QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentResponse, error)
|
|
||||||
QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentsResponse, error)
|
|
||||||
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
InitiatePayments(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) (*orchestratorv1.InitiatePaymentsResponse, error)
|
||||||
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error)
|
||||||
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
|
ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error)
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type quotationClient interface {
|
||||||
|
QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentResponse, error)
|
||||||
|
QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentsRequest) (*quotationv1.QuotePaymentsResponse, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
type PaymentAPI struct {
|
type PaymentAPI struct {
|
||||||
logger mlogger.Logger
|
logger mlogger.Logger
|
||||||
client paymentClient
|
execution executionClient
|
||||||
|
quotation quotationClient
|
||||||
enf auth.Enforcer
|
enf auth.Enforcer
|
||||||
oph mutil.ParamHelper
|
oph mutil.ParamHelper
|
||||||
discovery *discovery.Client
|
discovery *discovery.Client
|
||||||
@@ -49,11 +59,16 @@ type PaymentAPI struct {
|
|||||||
func (a *PaymentAPI) Name() mservice.Type { return mservice.Payments }
|
func (a *PaymentAPI) Name() mservice.Type { return mservice.Payments }
|
||||||
|
|
||||||
func (a *PaymentAPI) Finish(ctx context.Context) error {
|
func (a *PaymentAPI) Finish(ctx context.Context) error {
|
||||||
if a.client != nil {
|
if a.execution != nil {
|
||||||
if err := a.client.Close(); err != nil {
|
if err := a.execution.Close(); err != nil {
|
||||||
a.logger.Warn("Failed to close payment orchestrator client", zap.Error(err))
|
a.logger.Warn("Failed to close payment orchestrator client", zap.Error(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if a.quotation != nil {
|
||||||
|
if err := a.quotation.Close(); err != nil {
|
||||||
|
a.logger.Warn("Failed to close payment quotation client", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
if a.discovery != nil {
|
if a.discovery != nil {
|
||||||
a.discovery.Close()
|
a.discovery.Close()
|
||||||
}
|
}
|
||||||
@@ -103,15 +118,15 @@ func (a *PaymentAPI) initPaymentClient(cfg *eapi.PaymentOrchestratorConfig, quot
|
|||||||
return merrors.InvalidArgument("payment orchestrator configuration is not provided")
|
return merrors.InvalidArgument("payment orchestrator configuration is not provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
address := strings.TrimSpace(cfg.Address)
|
address, err := resolveClientAddress("payment orchestrator", cfg)
|
||||||
if address == "" {
|
if err != nil {
|
||||||
address = strings.TrimSpace(os.Getenv(cfg.AddressEnv))
|
return err
|
||||||
}
|
|
||||||
if address == "" {
|
|
||||||
return merrors.InvalidArgument(fmt.Sprintf("payment orchestrator address is not specified and address env %s is empty", cfg.AddressEnv))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteAddress := address
|
quoteAddress := address
|
||||||
|
quoteInsecure := cfg.Insecure
|
||||||
|
quoteDialTimeout := cfg.DialTimeoutSeconds
|
||||||
|
quoteCallTimeout := cfg.CallTimeoutSeconds
|
||||||
if quoteCfg != nil {
|
if quoteCfg != nil {
|
||||||
if addr := strings.TrimSpace(quoteCfg.Address); addr != "" {
|
if addr := strings.TrimSpace(quoteCfg.Address); addr != "" {
|
||||||
quoteAddress = addr
|
quoteAddress = addr
|
||||||
@@ -120,25 +135,133 @@ func (a *PaymentAPI) initPaymentClient(cfg *eapi.PaymentOrchestratorConfig, quot
|
|||||||
quoteAddress = resolved
|
quoteAddress = resolved
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
quoteInsecure = quoteCfg.Insecure
|
||||||
|
quoteDialTimeout = quoteCfg.DialTimeoutSeconds
|
||||||
|
quoteCallTimeout = quoteCfg.CallTimeoutSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
clientCfg := orchestratorclient.Config{
|
clientCfg := orchestratorclient.Config{
|
||||||
Address: address,
|
Address: address,
|
||||||
QuoteAddress: quoteAddress,
|
|
||||||
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
|
DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second,
|
||||||
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
|
CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second,
|
||||||
Insecure: cfg.Insecure,
|
Insecure: cfg.Insecure,
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := orchestratorclient.New(context.Background(), clientCfg)
|
execution, err := orchestratorclient.New(context.Background(), clientCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.client = client
|
quotation, err := newQuotationClient(context.Background(), quotationClientConfig{
|
||||||
|
Address: quoteAddress,
|
||||||
|
DialTimeout: time.Duration(quoteDialTimeout) * time.Second,
|
||||||
|
CallTimeout: time.Duration(quoteCallTimeout) * time.Second,
|
||||||
|
Insecure: quoteInsecure,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
_ = execution.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.execution = execution
|
||||||
|
a.quotation = quotation
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveClientAddress(service string, cfg *eapi.PaymentOrchestratorConfig) (string, error) {
|
||||||
|
if cfg == nil {
|
||||||
|
return "", merrors.InvalidArgument(strings.TrimSpace(service) + " configuration is not provided")
|
||||||
|
}
|
||||||
|
address := strings.TrimSpace(cfg.Address)
|
||||||
|
if address != "" {
|
||||||
|
return address, nil
|
||||||
|
}
|
||||||
|
if env := strings.TrimSpace(cfg.AddressEnv); env != "" {
|
||||||
|
if resolved := strings.TrimSpace(os.Getenv(env)); resolved != "" {
|
||||||
|
return resolved, nil
|
||||||
|
}
|
||||||
|
return "", merrors.InvalidArgument(fmt.Sprintf("%s address is not specified and address env %s is empty", strings.TrimSpace(service), env))
|
||||||
|
}
|
||||||
|
return "", merrors.InvalidArgument(strings.TrimSpace(service) + " address is not specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
type quotationClientConfig struct {
|
||||||
|
Address string
|
||||||
|
DialTimeout time.Duration
|
||||||
|
CallTimeout time.Duration
|
||||||
|
Insecure bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *quotationClientConfig) setDefaults() {
|
||||||
|
if c.DialTimeout <= 0 {
|
||||||
|
c.DialTimeout = 5 * time.Second
|
||||||
|
}
|
||||||
|
if c.CallTimeout <= 0 {
|
||||||
|
c.CallTimeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type grpcQuotationClient struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
client quotationv1.QuotationServiceClient
|
||||||
|
callTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) {
|
||||||
|
cfg.setDefaults()
|
||||||
|
if strings.TrimSpace(cfg.Address) == "" {
|
||||||
|
return nil, merrors.InvalidArgument("payment quotation: address is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
|
||||||
|
dialOpts = append(dialOpts, opts...)
|
||||||
|
if cfg.Insecure {
|
||||||
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||||
|
} else {
|
||||||
|
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-quotation: dial %s", cfg.Address))
|
||||||
|
}
|
||||||
|
return &grpcQuotationClient{
|
||||||
|
conn: conn,
|
||||||
|
client: quotationv1.NewQuotationServiceClient(conn),
|
||||||
|
callTimeout: cfg.CallTimeout,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcQuotationClient) Close() error {
|
||||||
|
if c == nil || c.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcQuotationClient) QuotePayment(ctx context.Context, req *quotationv1.QuotePaymentRequest) (*quotationv1.QuotePaymentResponse, error) {
|
||||||
|
callCtx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.QuotePayment(callCtx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcQuotationClient) QuotePayments(ctx context.Context, req *quotationv1.QuotePaymentsRequest) (*quotationv1.QuotePaymentsResponse, error) {
|
||||||
|
callCtx, cancel := c.callContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return c.client.QuotePayments(callCtx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
timeout := c.callTimeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = 3 * time.Second
|
||||||
|
}
|
||||||
|
return context.WithTimeout(ctx, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
|
func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
|
||||||
if cfg == nil || cfg.Mw == nil {
|
if cfg == nil || cfg.Mw == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ tmp_dir = "tmp"
|
|||||||
rerun = false
|
rerun = false
|
||||||
rerun_delay = 500
|
rerun_delay = 500
|
||||||
send_interrupt = false
|
send_interrupt = false
|
||||||
stop_on_error = false
|
stop_on_error = true
|
||||||
|
|
||||||
[color]
|
[color]
|
||||||
app = ""
|
app = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user