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"); } }
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); }
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); } } }
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); }
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); }
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()); }
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); } } } }
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"); } }
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)); }
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) }); }