public async Task <StripeTransactionResult> HandleAsync(
            UserId userId,
            DateTime timestamp,
            TransactionReference transactionReference,
            TaxamoTransactionResult taxamoTransaction,
            UserPaymentOriginResult origin,
            UserType userType)
        {
            userId.AssertNotNull("userId");
            transactionReference.AssertNotNull("transactionReference");
            taxamoTransaction.AssertNotNull("taxamoTransaction");
            origin.AssertNotNull("origin");

            // Perform stripe transaction.
            if (origin.PaymentOriginKeyType != PaymentOriginKeyType.Stripe)
            {
                throw new InvalidOperationException("Unexpected payment origin: " + origin.PaymentOriginKeyType);
            }

            var stripeChargeId = await this.performStripeCharge.ExecuteAsync(
                origin.PaymentOriginKey,
                taxamoTransaction.TotalAmount,
                userId,
                transactionReference,
                taxamoTransaction.Key,
                userType);

            return(new StripeTransactionResult(timestamp, transactionReference, stripeChargeId));
        }
        public async Task <CreateTransactionRefundResult> ExecuteAsync(
            UserId enactingUserId,
            TransactionReference transactionReference,
            DateTime timestamp,
            string comment)
        {
            enactingUserId.AssertNotNull("enactingUserId");
            transactionReference.AssertNotNull("transactionReference");

            var formattedComment = string.IsNullOrWhiteSpace(comment)
                ? string.Format("Performed by {0}", enactingUserId)
                : string.Format("Performed by {0} - {1}", enactingUserId, comment);

            var records = await this.getRecordsForTransaction.ExecuteAsync(transactionReference);

            if (records.Count == 0)
            {
                throw new InvalidOperationException("No records found for transaction reference: " + transactionReference);
            }

            UserId subscriberId = null;
            UserId creatorId    = null;

            foreach (var record in records)
            {
                LedgerTransactionType refundType;
                switch (record.TransactionType)
                {
                case LedgerTransactionType.SubscriptionPayment:
                    refundType = LedgerTransactionType.SubscriptionRefund;
                    break;

                default:
                    // This is a safety against both refunding other transaction types,
                    // and refunding transactions which has already been refunded.
                    throw new InvalidOperationException(
                              "Cannot refund transaction of type " + record.TransactionType);
                }

                if (record.AccountType == LedgerAccountType.FifthweekCredit)
                {
                    subscriberId = new UserId(record.AccountOwnerId);
                    creatorId    = new UserId(record.CounterpartyId.Value);
                }

                record.Id              = this.guidCreator.CreateSqlSequential();
                record.Timestamp       = timestamp;
                record.TransactionType = refundType;
                record.Amount          = -record.Amount;
                record.Comment         = formattedComment;
            }

            await this.persistCommittedRecords.ExecuteAsync(records);

            return(new CreateTransactionRefundResult(subscriberId, creatorId));
        }
        public async Task <IReadOnlyList <AppendOnlyLedgerRecord> > ExecuteAsync(TransactionReference transactionReference)
        {
            transactionReference.AssertNotNull("transactionReference");

            using (var connection = this.connectionFactory.CreateConnection())
            {
                var records = (await connection.QueryAsync <AppendOnlyLedgerRecord>(
                                   GetTransactionRowsSql,
                                   new { TransactionReference = transactionReference.Value })).ToList();

                return(records);
            }
        }
        public async Task <GetCreditTransactionResult> ExecuteAsync(TransactionReference transactionReference)
        {
            transactionReference.AssertNotNull("transactionReference");

            var records = await this.getRecordsForTransaction.ExecuteAsync(transactionReference);

            if (records.Count == 0)
            {
                return(null);
            }

            if (records.All(v => v.TransactionType != LedgerTransactionType.CreditAddition))
            {
                throw new InvalidOperationException(
                          "Transaction reference " + transactionReference + " was not a CreditAddition record.");
            }

            var firstRecord = records.First();

            var userId               = firstRecord.AccountOwnerId;
            var stripeChargeId       = firstRecord.StripeChargeId;
            var taxamoTransactionKey = firstRecord.TaxamoTransactionKey;

            var totalCreditAmount = records.First(
                v => v.TransactionType == LedgerTransactionType.CreditAddition &&
                v.AccountType == LedgerAccountType.FifthweekCredit).Amount;

            var existingRefundAmounts = records.Where(
                v => v.TransactionType == LedgerTransactionType.CreditRefund &&
                v.AccountType == LedgerAccountType.FifthweekCredit);

            var creditAmountAvailableForRefund = totalCreditAmount + existingRefundAmounts.Sum(v => v.Amount);

            return(new GetCreditTransactionResult(
                       new UserId(userId),
                       stripeChargeId,
                       taxamoTransactionKey,
                       totalCreditAmount,
                       creditAmountAvailableForRefund));
        }
        public async Task ExecuteAsync(
            UserId userId,
            DateTime timestamp,
            TransactionReference transactionReference,
            PositiveInt amount,
            PositiveInt expectedTotalAmount,
            UserType userType)
        {
            userId.AssertNotNull("userId");
            transactionReference.AssertNotNull("transactionReference");
            amount.AssertNotNull("amount");

            // We split this up into three phases that have individual retry handlers.
            // The first phase can be retried without issue if there are transient failures.
            var initializeResult = await this.retryOnTransientFailure.HandleAsync(
                () => this.initializeCreditRequest.HandleAsync(userId, amount, expectedTotalAmount, userType));

            // This phase could be put at the end of the first phase, but it runs the risk of someone inserting
            // a statement afterwards that causes a transient failure, so for safety it has been isolated.
            var stripeTransactionResult = await this.retryOnTransientFailure.HandleAsync(
                () => this.performCreditRequest.HandleAsync(
                    userId,
                    timestamp,
                    transactionReference,
                    initializeResult.TaxamoTransaction,
                    initializeResult.Origin,
                    userType));

            try
            {
                Task commitToDatabaseTask;
                if (userType == UserType.StandardUser)
                {
                    // Finally we commit to the local database...
                    commitToDatabaseTask = this.retryOnTransientFailure.HandleAsync(
                        () => this.commitCreditToDatabase.HandleAsync(
                            userId,
                            initializeResult.TaxamoTransaction,
                            initializeResult.Origin,
                            stripeTransactionResult));
                }
                else
                {
                    commitToDatabaseTask = this.retryOnTransientFailure.HandleAsync(
                        () => this.commitTestUserCreditToDatabase.HandleAsync(
                            userId,
                            amount));
                }

                // ... and commit taxamo transaction.
                var commitToTaxamoTask = this.retryOnTransientFailure.HandleAsync(
                    () => this.commitTaxamoTransaction.ExecuteAsync(
                        initializeResult.TaxamoTransaction,
                        stripeTransactionResult,
                        userType));

                // We run the two committing tasks in parallel as even if one fails we would like the other to try and succeed.
                await Task.WhenAll(commitToDatabaseTask, commitToTaxamoTask);
            }
            catch (Exception t)
            {
                var json = JsonConvert.SerializeObject(
                    new
                {
                    UserId                  = userId,
                    InitializeResult        = initializeResult,
                    StripeTransactionResult = stripeTransactionResult,
                });

                throw new FailedToApplyCreditException(json, t);
            }
        }
示例#6
0
        public async Task ExecuteAsync(
            UserId enactingUserId,
            UserId userId,
            DateTime timestamp,
            PositiveInt totalAmount,
            PositiveInt creditAmount,
            TransactionReference transactionReference,
            string stripeChargeId,
            string taxamoTransactionKey,
            string comment)
        {
            enactingUserId.AssertNotNull("enactingUserId");
            userId.AssertNotNull("userId");
            totalAmount.AssertNotNull("totalAmount");
            creditAmount.AssertNotNull("creditAmount");
            transactionReference.AssertNotNull("transactionReference");
            stripeChargeId.AssertNotNull("stripeChargeId");
            taxamoTransactionKey.AssertNotNull("taxamoTransactionKey");
            comment.AssertNotNull("comment");

            if (totalAmount.Value < creditAmount.Value)
            {
                throw new InvalidOperationException(string.Format(
                                                        "The total charged amount ({0}) cannot be less than the amount credited ({1}).",
                                                        totalAmount.Value,
                                                        creditAmount.Value));
            }

            var formattedComment = string.Format("{0} (Performed By {1})", comment, enactingUserId);

            var committedRecords = new List <AppendOnlyLedgerRecord>();

            committedRecords.Add(new AppendOnlyLedgerRecord(
                                     this.guidCreator.CreateSqlSequential(),
                                     userId.Value,
                                     null,
                                     timestamp,
                                     totalAmount.Value,
                                     LedgerAccountType.Stripe,
                                     LedgerTransactionType.CreditRefund,
                                     transactionReference.Value,
                                     null,
                                     formattedComment,
                                     stripeChargeId,
                                     taxamoTransactionKey));

            committedRecords.Add(new AppendOnlyLedgerRecord(
                                     this.guidCreator.CreateSqlSequential(),
                                     userId.Value,
                                     null,
                                     timestamp,
                                     -creditAmount.Value,
                                     LedgerAccountType.FifthweekCredit,
                                     LedgerTransactionType.CreditRefund,
                                     transactionReference.Value,
                                     null,
                                     formattedComment,
                                     stripeChargeId,
                                     taxamoTransactionKey));

            var tax = totalAmount.Value - creditAmount.Value;

            if (tax > 0)
            {
                committedRecords.Add(new AppendOnlyLedgerRecord(
                                         this.guidCreator.CreateSqlSequential(),
                                         userId.Value,
                                         null,
                                         timestamp,
                                         -tax,
                                         LedgerAccountType.SalesTax,
                                         LedgerTransactionType.CreditRefund,
                                         transactionReference.Value,
                                         null,
                                         formattedComment,
                                         stripeChargeId,
                                         taxamoTransactionKey));
            }

            await this.persistCommittedRecords.ExecuteAsync(committedRecords);
        }
        public async Task <UserId> ExecuteAsync(
            UserId enactingUserId,
            TransactionReference transactionReference,
            DateTime timestamp,
            PositiveInt refundCreditAmount,
            RefundCreditReason reason,
            string comment)
        {
            enactingUserId.AssertNotNull("enactingUserId");
            transactionReference.AssertNotNull("transactionReference");
            refundCreditAmount.AssertNotNull("refundCreditAmount");

            var transaction = await this.retryOnTransientFailure.HandleAsync(
                () => this.getCreditTransaction.ExecuteAsync(transactionReference));

            if (transaction == null)
            {
                throw new BadRequestException(
                          "Transaction reference " + transactionReference + " was not a valid transaction, or the user is not a standard user.");
            }

            if (refundCreditAmount.Value > transaction.CreditAmountAvailableForRefund)
            {
                throw new BadRequestException(
                          "Requested refund amount was greater than amount available for refund.");
            }

            var userId = transaction.UserId;

            // Refund on taxamo, get tax refund amount.
            var taxamoResult = await this.retryOnTransientFailure.HandleAsync(
                () => this.createTaxamoRefund.ExecuteAsync(transaction.TaxamoTransactionKey, refundCreditAmount, UserType.StandardUser));

            try
            {
                // Call CreateCreditRefundDbStatement
                await this.retryOnTransientFailure.HandleAsync(
                    () => this.persistCreditRefund.ExecuteAsync(
                        enactingUserId,
                        userId,
                        timestamp,
                        taxamoResult.TotalRefundAmount,
                        refundCreditAmount,
                        transactionReference,
                        transaction.StripeChargeId,
                        transaction.TaxamoTransactionKey,
                        comment));

                // refund on stripe.
                await this.retryOnTransientFailure.HandleAsync(
                    () => this.createStripeRefund.ExecuteAsync(enactingUserId, transaction.StripeChargeId, taxamoResult.TotalRefundAmount, reason, UserType.StandardUser));
            }
            catch (Exception t)
            {
                var json = JsonConvert.SerializeObject(
                    new
                {
                    TransactionReference = transactionReference,
                    Transaction          = transaction,
                    TaxamoResult         = taxamoResult,
                });

                throw new FailedToRefundCreditException(json, t);
            }

            return(userId);
        }