예제 #1
0
        public async Task HandleExpiredAsync()
        {
            DateTime dateTo = DateTime.UtcNow;

            DateTime dateFrom = dateTo.Add(-_expirationPeriods.WalletExtra);

            IReadOnlyList <IPaymentRequest> expired = await _paymentRequestRepository.GetByDueDate(dateFrom, dateTo);

            IEnumerable <IPaymentRequest> eligibleForTransition =
                expired.Where(x => x.StatusValidForPastDueTransition()).ToList();

            if (eligibleForTransition.Any())
            {
                _log.Info($"Found payment requests eligible to move to Past Due: {eligibleForTransition.Count()}");
            }

            foreach (IPaymentRequest paymentRequest in eligibleForTransition)
            {
                _log.Info(
                    $"Payment request with id {paymentRequest.Id} and merchant id {paymentRequest.MerchantId} is about to be moved to Past Due");

                await UpdateStatusAsync(paymentRequest.WalletAddress, PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.PaymentExpired));

                _log.Info($"Payment request with id {paymentRequest.Id} was moved to Past Due");
            }
        }
예제 #2
0
        public async Task UpdateStatusAsync(string walletAddress, PaymentRequestStatusInfo statusInfo = null)
        {
            IPaymentRequest paymentRequest = await _paymentRequestRepository.FindAsync(walletAddress);

            if (paymentRequest == null)
            {
                throw new PaymentRequestNotFoundException(walletAddress);
            }

            PaymentRequestStatusInfo newStatusInfo =
                statusInfo ?? await _paymentRequestStatusResolver.GetStatus(walletAddress);

            paymentRequest.Status          = newStatusInfo.Status;
            paymentRequest.PaidDate        = newStatusInfo.Date;
            paymentRequest.PaidAmount      = newStatusInfo.Amount;
            paymentRequest.ProcessingError = paymentRequest.Status == PaymentRequestStatus.Error
                ? newStatusInfo.ProcessingError
                : PaymentRequestProcessingError.None;

            await _paymentRequestRepository.UpdateAsync(paymentRequest);

            //todo: move to separate builder service
            PaymentRequestRefund refundInfo = await GetRefundInfoAsync(paymentRequest.WalletAddress);

            await _paymentRequestPublisher.PublishAsync(paymentRequest, refundInfo);
        }
예제 #3
0
        private async Task UpdateStatusAsync(IPaymentRequest paymentRequest, PaymentRequestStatusInfo statusInfo = null)
        {
            PaymentRequestStatusInfo newStatusInfo =
                statusInfo ?? await _paymentRequestStatusResolver.GetStatus(paymentRequest.WalletAddress);

            PaymentRequestStatus          previousStatus          = paymentRequest.Status;
            PaymentRequestProcessingError previousProcessingError = paymentRequest.ProcessingError;

            paymentRequest.Status   = newStatusInfo.Status;
            paymentRequest.PaidDate = newStatusInfo.Date;

            if (newStatusInfo.Amount.HasValue)
            {
                paymentRequest.PaidAmount = newStatusInfo.Amount.Value;
            }

            paymentRequest.ProcessingError = (paymentRequest.Status == PaymentRequestStatus.Error || paymentRequest.Status == PaymentRequestStatus.SettlementError)
                ? newStatusInfo.ProcessingError
                : PaymentRequestProcessingError.None;

            await _paymentRequestRepository.UpdateAsync(paymentRequest);

            // if we are updating status from "InProcess" to any other - we have to release the lock
            if (previousStatus == PaymentRequestStatus.InProcess)
            {
                await _paymentLocksService.ReleaseLockAsync(paymentRequest.Id, paymentRequest.MerchantId);
            }

            PaymentRequestRefund refundInfo = await GetRefundInfoAsync(paymentRequest.WalletAddress);

            if (paymentRequest.Status != previousStatus ||
                (paymentRequest.Status == PaymentRequestStatus.Error &&
                 paymentRequest.ProcessingError != previousProcessingError))
            {
                await _paymentRequestPublisher.PublishAsync(paymentRequest, refundInfo);

                IAssetGeneralSettings assetSettings =
                    await _assetSettingsService.GetGeneralAsync(paymentRequest.PaymentAssetId);

                // doing auto settlement only once
                // Some flows assume we can get updates from blockchain multiple times for the same transaction
                // which leads to the same payment request status
                if (paymentRequest.StatusValidForSettlement() && (assetSettings?.AutoSettle ?? false))
                {
                    if (paymentRequest.Status != PaymentRequestStatus.Confirmed &&
                        !_autoSettleSettingsResolver.AllowToMakePartialAutoSettle(paymentRequest.PaymentAssetId))
                    {
                        return;
                    }

                    await SettleAsync(paymentRequest.MerchantId, paymentRequest.Id);
                }
            }
        }
예제 #4
0
        public async Task UpdateStatusAsync(string walletAddress, PaymentRequestStatusInfo statusInfo = null)
        {
            IPaymentRequest paymentRequest = await _paymentRequestRepository.FindAsync(walletAddress);

            if (paymentRequest == null)
            {
                throw new PaymentRequestNotFoundException(walletAddress);
            }

            await UpdateStatusAsync(paymentRequest, statusInfo);
        }
예제 #5
0
        public async Task UpdateStatusAsync(string merchanttId, string paymentRequestId,
                                            PaymentRequestStatusInfo statusInfo = null)
        {
            IPaymentRequest paymentRequest = await _paymentRequestRepository.GetAsync(merchanttId, paymentRequestId);

            if (paymentRequest == null)
            {
                throw new PaymentRequestNotFoundException(merchanttId, paymentRequestId);
            }

            await UpdateStatusAsync(paymentRequest, statusInfo);
        }
예제 #6
0
        private async Task <PaymentRequestStatusInfo> GetStatusForRefund(IPaymentRequest paymentRequest)
        {
            IReadOnlyList <IPaymentRequestTransaction> txs =
                (await _transactionsService.GetByWalletAsync(paymentRequest.WalletAddress)).Where(x => x.IsRefund()).ToList();

            if (txs.All(x => x.Confirmed(_transactionConfirmationCount)))
            {
                return(PaymentRequestStatusInfo.Refunded());
            }

            return(txs.Any(x => !x.Confirmed(_transactionConfirmationCount) && x.Expired())
                ? PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.RefundNotConfirmed)
                : PaymentRequestStatusInfo.RefundInProgress());
        }
예제 #7
0
        public async Task FailOutgoingAsync(FailOutTxCommand cmd)
        {
            var txs = (await _transactionsService.GetByBcnIdentityAsync(
                           cmd.Blockchain,
                           cmd.IdentityType,
                           cmd.Identity)).ToList();

            if (!txs.Any())
            {
                throw new OutboundTransactionsNotFound(cmd.Blockchain, cmd.IdentityType, cmd.Identity);
            }

            foreach (var tx in txs)
            {
                _log.Info($"Failing outgoing transaction [type={tx.TransactionType}]", cmd.ToJson());

                IUpdateTransactionCommand updateCommand = MapToUpdateCommand(cmd, tx.TransactionType);

                await _transactionsService.UpdateAsync(updateCommand);

                if (tx.TransactionType == TransactionType.Payment)
                {
                    await _paymentRequestService.UpdateStatusAsync(tx.WalletAddress,
                                                                   PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.UnknownPayment));
                }

                if (tx.TransactionType == TransactionType.Exchange)
                {
                    var context = tx.ContextData.DeserializeJson <ExchangeTransactonContext>();
                    if (context != null)
                    {
                        await _walletHistoryService.RemoveAsync(context.HistoryOperationId);
                    }
                }

                if (tx.TransactionType == TransactionType.CashOut)
                {
                    var context = tx.ContextData.DeserializeJson <CashoutTransactionContext>();
                    if (context != null)
                    {
                        await _walletHistoryService.RemoveAsync(context.HistoryOperationId);
                    }
                }
            }
        }
예제 #8
0
        private async Task <PaymentRequestStatusInfo> GetStatusForPayment(IPaymentRequest paymentRequest)
        {
            IReadOnlyList <IPaymentRequestTransaction> txs =
                (await _transactionsService.GetByWalletAsync(paymentRequest.WalletAddress)).Where(x => x.IsPayment()).ToList();

            if (!txs.Any())
            {
                return((paymentRequest.DueDate < DateTime.UtcNow)
                    ? PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.PaymentExpired)
                    : PaymentRequestStatusInfo.New());
            }

            decimal btcPaid;

            var assetId = txs.GetAssetId();

            switch (assetId)
            {
            case LykkeConstants.SatoshiAsset:
                btcPaid = txs.GetTotal().SatoshiToBtc();
                break;

            default:
                btcPaid = txs.GetTotal();
                break;
            }

            bool allConfirmed = txs.All(x => x.Confirmed(_transactionConfirmationCount));

            var paidDate = txs.GetLatestDate();

            if (paidDate > paymentRequest.DueDate)
            {
                if (allConfirmed)
                {
                    return(PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.LatePaid, btcPaid, paidDate));
                }

                return(paymentRequest.GetCurrentStatusInfo());
            }

            IOrder actualOrder = await _orderService.GetActualAsync(paymentRequest.Id, paidDate) ??
                                 await _orderService.GetLatestOrCreateAsync(paymentRequest);

            if (!allConfirmed)
            {
                return(PaymentRequestStatusInfo.InProcess());
            }

            decimal btcToBePaid = actualOrder.PaymentAmount;

            var fulfillment = await _calculationService.CalculateBtcAmountFullfillmentAsync(btcToBePaid, btcPaid);

            switch (fulfillment)
            {
            case AmountFullFillmentStatus.Below:
                return(PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.PaymentAmountBelow, btcPaid, paidDate));

            case AmountFullFillmentStatus.Exact:
                return(PaymentRequestStatusInfo.Confirmed(btcPaid, paidDate));

            case AmountFullFillmentStatus.Above:
                return(PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.PaymentAmountAbove, btcPaid, paidDate));

            default: throw new Exception("Unexpected amount fullfillment status");
            }
        }
예제 #9
0
        public async Task <RefundResult> ExecuteAsync(string merchantId, string paymentRequestId,
                                                      string destinationWalletAddress)
        {
            IPaymentRequest paymentRequest =
                await _paymentRequestService.GetAsync(merchantId, paymentRequestId);

            if (paymentRequest == null)
            {
                throw new RefundValidationException(RefundErrorType.PaymentRequestNotFound);
            }

            if (!paymentRequest.StatusValidForRefund())
            {
                throw new RefundValidationException(RefundErrorType.NotAllowedInStatus);
            }

            IEnumerable <IPaymentRequestTransaction> paymentTxs =
                (await _transactionsService.GetByWalletAsync(paymentRequest.WalletAddress)).Where(x => x.IsPayment()).ToList();

            if (!paymentTxs.Any())
            {
                throw new RefundValidationException(RefundErrorType.NoPaymentTransactions);
            }

            if (paymentTxs.MoreThanOne())
            {
                throw new RefundValidationException(RefundErrorType.MultitransactionNotSupported);
            }

            IPaymentRequestTransaction tx = paymentTxs.Single();

            bool isValidAddress = string.IsNullOrWhiteSpace(destinationWalletAddress) ||
                                  await _blockchainAddressValidator.Execute(destinationWalletAddress, tx.Blockchain);

            if (!isValidAddress)
            {
                throw new RefundValidationException(RefundErrorType.InvalidDestinationAddress);
            }

            if (!tx.SourceWalletAddresses.Any())
            {
                throw new RefundValidationException(RefundErrorType.InvalidDestinationAddress);
            }

            if (string.IsNullOrWhiteSpace(destinationWalletAddress))
            {
                if (tx.SourceWalletAddresses.MoreThanOne())
                {
                    throw new RefundValidationException(RefundErrorType.InvalidDestinationAddress);
                }
            }

            //validation finished, refund request accepted
            await _paymentRequestService.UpdateStatusAsync(paymentRequest.WalletAddress,
                                                           PaymentRequestStatusInfo.RefundInProgress());

            TransferResult transferResult;

            DateTime refundDueDate;

            try
            {
                TransferCommand refundTransferCommand = Mapper.Map <TransferCommand>(tx,
                                                                                     opts => opts.Items["destinationAddress"] = destinationWalletAddress);

                transferResult = await _transferService.ExecuteAsync(refundTransferCommand);

                refundDueDate = transferResult.Timestamp.Add(_refundExpirationPeriod);

                foreach (var transferResultTransaction in transferResult.Transactions)
                {
                    if (!string.IsNullOrEmpty(transferResultTransaction.Error))
                    {
                        await _log.WriteWarningAsync(nameof(RefundService), nameof(ExecuteAsync),
                                                     transferResultTransaction.ToJson(), "Transaction failed");

                        continue;
                    }

                    IPaymentRequestTransaction refundTransaction = await _transactionsService.CreateTransactionAsync(
                        new CreateTransactionCommand
                    {
                        Amount        = transferResultTransaction.Amount,
                        AssetId       = transferResultTransaction.AssetId,
                        Confirmations = 0,
                        Hash          = transferResultTransaction.Hash,
                        WalletAddress = paymentRequest.WalletAddress,
                        Type          = TransactionType.Refund,
                        Blockchain    = transferResult.Blockchain,
                        FirstSeen     = null,
                        DueDate       = refundDueDate,
                        TransferId    = transferResult.Id,
                        IdentityType  = transferResultTransaction.IdentityType,
                        Identity      = transferResultTransaction.Identity
                    });

                    await _transactionPublisher.PublishAsync(refundTransaction);
                }

                if (transferResult.Transactions.All(x => x.HasError))
                {
                    throw new RefundOperationFailedException {
                              TransferErrors = transferResult.Transactions.Select(x => x.Error)
                    }
                }
                ;

                IEnumerable <TransferTransactionResult> errorTransactions =
                    transferResult.Transactions.Where(x => x.HasError).ToList();

                if (errorTransactions.Any())
                {
                    throw new RefundOperationPartiallyFailedException(errorTransactions.Select(x => x.Error));
                }
            }
            catch (Exception)
            {
                await _paymentRequestService.UpdateStatusAsync(paymentRequest.WalletAddress,
                                                               PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.UnknownRefund));

                throw;
            }

            return(await PrepareRefundResult(paymentRequest, transferResult, refundDueDate));
        }
예제 #10
0
        public async Task <PaymentResult> PayAsync(PaymentCommand cmd)
        {
            IPaymentRequest paymentRequest = await _paymentRequestRepository.GetAsync(cmd.MerchantId, cmd.PaymentRequestId);

            if (paymentRequest == null)
            {
                throw new PaymentRequestNotFoundException(cmd.MerchantId, cmd.PaymentRequestId);
            }

            string payerWalletAddress = (await _merchantWalletService.GetDefaultAsync(
                                             cmd.PayerMerchantId,
                                             paymentRequest.PaymentAssetId,
                                             PaymentDirection.Outgoing)).WalletAddress;

            string destinationWalletAddress = await _walletsManager.ResolveBlockchainAddressAsync(
                paymentRequest.WalletAddress,
                paymentRequest.PaymentAssetId);

            bool locked = await _paymentLocksService.TryAcquireLockAsync(
                paymentRequest.Id,
                cmd.MerchantId,
                paymentRequest.DueDate);

            if (!locked)
            {
                throw new DistributedLockAcquireException(paymentRequest.Id);
            }

            TransferResult transferResult;

            try
            {
                await UpdateStatusAsync(paymentRequest.WalletAddress, PaymentRequestStatusInfo.InProcess());

                transferResult = await _paymentRetryPolicy
                                 .ExecuteAsync(() => _transferService.PayThrowFail(
                                                   paymentRequest.PaymentAssetId,
                                                   payerWalletAddress,
                                                   destinationWalletAddress,
                                                   cmd.Amount));

                foreach (var transferResultTransaction in transferResult.Transactions)
                {
                    IPaymentRequestTransaction paymentTx = await _transactionsService.CreateTransactionAsync(
                        new CreateTransactionCommand
                    {
                        Amount                = transferResultTransaction.Amount,
                        Blockchain            = transferResult.Blockchain,
                        AssetId               = transferResultTransaction.AssetId,
                        WalletAddress         = paymentRequest.WalletAddress,
                        DueDate               = paymentRequest.DueDate,
                        IdentityType          = transferResultTransaction.IdentityType,
                        Identity              = transferResultTransaction.Identity,
                        Confirmations         = 0,
                        Hash                  = transferResultTransaction.Hash,
                        TransferId            = transferResult.Id,
                        Type                  = TransactionType.Payment,
                        SourceWalletAddresses = transferResultTransaction.Sources.ToArray()
                    });

                    await _transactionPublisher.PublishAsync(paymentTx);
                }
            }
            catch (Exception e)
            {
                PaymentRequestStatusInfo newStatus = e is InsufficientFundsException
                    ? PaymentRequestStatusInfo.New()
                    : PaymentRequestStatusInfo.Error(PaymentRequestProcessingError.UnknownPayment);

                await UpdateStatusAsync(paymentRequest.WalletAddress, newStatus);

                await _paymentLocksService.ReleaseLockAsync(paymentRequest.Id, cmd.MerchantId);

                throw;
            }

            return(new PaymentResult
            {
                PaymentRequestId = paymentRequest.Id,
                PaymentRequestWalletAddress = paymentRequest.WalletAddress,
                AssetId = transferResult.Transactions.Unique(x => x.AssetId).Single(),
                Amount = transferResult.GetSuccedeedTxs().Sum(x => x.Amount)
            });
        }