Ejemplo n.º 1
0
            /// <summary>
            /// High-level calculation method to get charges from the calculation logic
            /// and attach them to the transaction and it's lines.
            /// </summary>
            /// <param name="request">The request containing context and transaction to update.</param>
            /// <returns>
            /// Response with updated transaction.
            /// </returns>
            private static GetChargesServiceResponse CalculateCharges(GetChargesServiceRequest request)
            {
                // extract transaction we'll be populating
                var transaction = request.Transaction;

                if (request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.OnlineStore ||
                    request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.SharePointOnlineStore)
                {
                    // clear all the non-manual charges
                    ClearNonManualCharges(transaction);
                }

                // total transaction so that we have order totals and line net amounts for percentage charges and tiered fixed charges
                SalesTransactionTotaler.CalculateTotals(request.RequestContext, transaction);

                // put charges on the transaction lines
                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                foreach (var salesLine in transaction.ChargeCalculableSalesLines)
                {
                    var priceChargesPerLine = CalculatePriceCharges(salesLine, request);
                    if (priceChargesPerLine != null)
                    {
                        salesLine.ChargeLines.Add(priceChargesPerLine);
                    }
                }

                // Auto-Charges are only supported for Online stores.
                if (request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.OnlineStore ||
                    request.RequestContext.GetChannelConfiguration().ChannelType == RetailChannelType.SharePointOnlineStore)
                {
                    CalculateAutoCharges(request, transaction);
                }

                return(new GetChargesServiceResponse(transaction));
            }
Ejemplo n.º 2
0
            /// <summary>
            /// Static entry point to calculate amount paid and due.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The sales transaction.</param>
            public static void CalculateAmountPaidAndDue(RequestContext context, SalesTransaction salesTransaction)
            {
                ThrowIf.Null(context, "context");
                ThrowIf.Null(salesTransaction, "salesTransaction");

                decimal paymentRequiredAmount;

                salesTransaction.AmountPaid = SalesTransactionTotaler.GetPaymentsSum(salesTransaction.TenderLines);

                // decides what is expected to be paid for this transaction
                switch (salesTransaction.CartType)
                {
                case CartType.CustomerOrder:
                    paymentRequiredAmount = SalesTransactionTotaler.CalculateRequiredPaymentAmount(context, salesTransaction);
                    break;

                case CartType.Shopping:
                case CartType.Checkout:
                case CartType.AccountDeposit:
                    paymentRequiredAmount = salesTransaction.TotalAmount;
                    break;

                case CartType.IncomeExpense:
                    paymentRequiredAmount = salesTransaction.IncomeExpenseTotalAmount;
                    break;

                default:
                    throw new DataValidationException(
                              DataValidationErrors.Microsoft_Dynamics_Commerce_Runtime_InvalidRequest,
                              string.Format("SalesTransactionTotaler::CalculateAmountPaidAndDue: CartType '{0}' not supported.", salesTransaction.CartType));
                }

                salesTransaction.SalesPaymentDifference = paymentRequiredAmount - salesTransaction.AmountPaid;
                salesTransaction.AmountDue = paymentRequiredAmount - salesTransaction.AmountPaid;

                TenderLine lastTenderLine = null;

                if (!salesTransaction.TenderLines.IsNullOrEmpty())
                {
                    lastTenderLine = salesTransaction.ActiveTenderLines.LastOrDefault();
                }

                if (lastTenderLine != null)
                {
                    // Calculate the expected (rounded) amount due for last payment.
                    decimal amountDueBeforeLastPayment = paymentRequiredAmount - salesTransaction.ActiveTenderLines.Take(salesTransaction.ActiveTenderLines.Count - 1).Sum(t => t.Amount);
                    GetPaymentRoundedValueServiceRequest roundAmountDueBeforeLastPaymentRequest  = new GetPaymentRoundedValueServiceRequest(amountDueBeforeLastPayment, lastTenderLine.TenderTypeId, isChange: false);
                    GetRoundedValueServiceResponse       roundAmountDueBeforeLastPaymentResponse = context.Execute <GetRoundedValueServiceResponse>(roundAmountDueBeforeLastPaymentRequest);

                    // Set amont due to zero if payment amount equals to expected rounded payment amount. Otherwise another payment should be required (that could use different rounding settings).
                    if (roundAmountDueBeforeLastPaymentResponse.RoundedValue == lastTenderLine.Amount)
                    {
                        salesTransaction.AmountDue = decimal.Zero;
                    }
                }

                // When required amount is positive, amount due must be zero or negative (overtender), otherwise (e.g. for refunds or exchanges) exact amount has to refunded (zero balance).
                salesTransaction.IsRequiredAmountPaid = (paymentRequiredAmount > 0 && salesTransaction.AmountDue <= 0) ||
                                                        (paymentRequiredAmount <= 0 && salesTransaction.AmountDue == 0);
            }
Ejemplo n.º 3
0
            /// <summary>
            /// Calculates the tax for the last item.
            /// </summary>
            /// <param name="request">The request.</param>
            /// <returns>The response.</returns>
            private static CalculateTaxServiceResponse CalculateTax(CalculateTaxServiceRequest request)
            {
                ThrowIf.Null(request, "request");

                TaxHelpers.SetSalesTaxGroup(request.RequestContext, request.Transaction);
                SalesTaxOverrideHelper.CalculateTaxOverrides(request.RequestContext, request.Transaction);

                // Consider active (non-void) lines for tax.
                // Need to recalculate tax on return-by-receipt lines because we cannot reconstruct tax lines from return transaction lines alone.
                // A few key information like IsExempt, IsTaxInclusive, TaxCode are not available on return transaction line.
                foreach (var saleItem in request.Transaction.ActiveSalesLines)
                {
                    saleItem.TaxRatePercent = 0;
                    saleItem.TaxLines.Clear();
                }

                var totaler = new SalesTransactionTotaler(request.Transaction);

                totaler.CalculateTotals(request.RequestContext);

                ClearChargeTaxLines(request.Transaction);

                TaxContext taxContext = new TaxContext(request.RequestContext);

                TaxCodeProvider defaultProvider = GetTaxProvider(request.RequestContext, taxContext);

                defaultProvider.CalculateTax(request.RequestContext, request.Transaction);

                return(new CalculateTaxServiceResponse(request.Transaction));
            }
Ejemplo n.º 4
0
            /// <summary>
            /// Calculates the deposit and updates the sales transaction deposit amount.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The sales transaction.</param>
            public static void CalculateDeposit(RequestContext context, SalesTransaction salesTransaction)
            {
                ThrowIf.Null(context, "context");
                ThrowIf.Null(salesTransaction, "salesTransaction");

                salesTransaction.CalculatedDepositAmount = SalesTransactionTotaler.CalculateDepositAmount(context, salesTransaction);

                salesTransaction.RequiredDepositAmount = salesTransaction.IsDepositOverridden ?
                                                         RoundCurrencyAmount(context, salesTransaction.OverriddenDepositAmount.Value) :
                                                         salesTransaction.CalculatedDepositAmount;

                // the deposit available on the pickup operation is the DepositAvailableAmount
                // but we cannot use more deposit than the DepositRequiredAmount for the transaction
                if (salesTransaction.CustomerOrderMode == CustomerOrderMode.Pickup)
                {
                    bool hasAnyQuantityRemainingForPickup = salesTransaction.SalesLines.Any(line => (line.QuantityInvoiced + line.Quantity) < line.QuantityOrdered);
                    if (hasAnyQuantityRemainingForPickup)
                    {
                        salesTransaction.PrepaymentAmountAppliedOnPickup = Math.Min(salesTransaction.AvailableDepositAmount, salesTransaction.RequiredDepositAmount);
                    }
                    else
                    {
                        salesTransaction.PrepaymentAmountAppliedOnPickup = salesTransaction.AvailableDepositAmount;
                    }
                }
                else
                {
                    salesTransaction.PrepaymentAmountAppliedOnPickup = decimal.Zero;
                }
            }
Ejemplo n.º 5
0
            /// <summary>
            /// Gets the calculated deposit amount.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The sales transaction.</param>
            /// <returns>Calculated deposit amount.</returns>
            private static decimal CalculateDepositAmount(RequestContext context, SalesTransaction salesTransaction)
            {
                ThrowIf.Null(context, "context");
                ThrowIf.Null(salesTransaction, "salesTransaction");

                decimal calculatedDepositAmount = decimal.Zero;

                if (salesTransaction.CartType == CartType.CustomerOrder &&
                    (salesTransaction.CustomerOrderMode == CustomerOrderMode.CustomerOrderCreateOrEdit ||
                     salesTransaction.CustomerOrderMode == CustomerOrderMode.Pickup))
                {
                    decimal minimumDepositMultiplier = context.GetChannelConfiguration().MinimumDepositPercentage / 100M;

                    if (minimumDepositMultiplier > decimal.Zero)
                    {
                        // get cancellation charge total amount
                        decimal cancellationChargeAmount = SalesTransactionTotaler.GetCancellationChargeAmount(context, salesTransaction, context.GetChannelConfiguration());

                        // take the transaction total minus the cancellation charges
                        decimal transactionTotalAmountWithoutCancellationCharges = salesTransaction.TotalAmount - cancellationChargeAmount;

                        // charges are added before deposit calculation - we need to make sure we do not calculate the deposit over cancellation charges
                        // deposit required = (order total - any cancellation charges) * minimum deposit multiplier
                        calculatedDepositAmount = transactionTotalAmountWithoutCancellationCharges * minimumDepositMultiplier;
                    }
                }

                return(RoundCurrencyAmount(context, calculatedDepositAmount));
            }
Ejemplo n.º 6
0
            /// <summary>
            /// Calculates all of the discounts for the transactions.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="transaction">The sales transaction.</param>
            /// <param name="discountCalculationMode">Discount calculation mode.</param>
            public void CalculateDiscount(RequestContext context, SalesTransaction transaction, DiscountCalculationMode discountCalculationMode)
            {
                if (transaction == null)
                {
                    throw new ArgumentNullException("transaction");
                }

                // take a snapshot of discount amount coming from previous computations, if there is any
                Dictionary <string, decimal> previousLineDiscounts = new Dictionary <string, decimal>(StringComparer.OrdinalIgnoreCase);

                // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                if (transaction.PriceCalculableSalesLines.Any())
                {
                    // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                    foreach (var line in transaction.PriceCalculableSalesLines)
                    {
                        // if it is zero, means this is the first time we are calculating, there is no previous state
                        if (line.DiscountAmount != 0)
                        {
                            previousLineDiscounts.Add(line.LineId, line.DiscountAmount);
                        }
                    }
                }

                var channelConfiguration = context.GetChannelConfiguration();

                var customer = GetCustomer(context, transaction.CustomerId);

                PE.PricingEngine.CalculateDiscountsForLines(
                    this.pricingDataManager,
                    transaction,
                    new ChannelCurrencyOperations(context),
                    channelConfiguration.Currency,
                    customer.LineDiscountGroup,
                    customer.MultilineDiscountGroup,
                    customer.TotalDiscountGroup,
                    false,
                    discountCalculationMode,
                    context.GetNowInChannelTimeZone());

                // check whether any discount discrepancy occurred after calculation
                if (transaction.PriceCalculableSalesLines.Any())
                {
                    // Consider calculable lines only. Ignore voided or return-by-receipt lines.
                    foreach (var line in transaction.PriceCalculableSalesLines)
                    {
                        // calculate on a clone so that we don't modify the actual sales line
                        var clonedLine = line.Clone <SalesLine>();
                        SalesTransactionTotaler.CalculateLine(context, transaction, clonedLine);

                        Discount.RaiseNotificationIfDiscountWasInvalidated(context, previousLineDiscounts, clonedLine);
                    }
                }

                transaction.IsDiscountFullyCalculated = discountCalculationMode.HasFlag(DiscountCalculationMode.CalculateAll);
            }
Ejemplo n.º 7
0
            /// <summary>
            /// Gets the cancellation total charge amount (include taxes) on a sales transaction.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The sales transaction to get the cancellation charge amount from.</param>
            /// <param name="channelConfiguration">The channel configuration object.</param>
            /// <returns>The cancellation total charge amount.</returns>
            private static decimal GetCancellationChargeAmount(RequestContext context, SalesTransaction salesTransaction, ChannelConfiguration channelConfiguration)
            {
                ThrowIf.Null(salesTransaction, "salesTransaction");
                ThrowIf.Null(channelConfiguration, "channelConfiguration");

                decimal cancellationChargeAmount = decimal.Zero;

                // sum up all cancellation charges in the order
                foreach (ChargeLine chargeLine in salesTransaction.ChargeLines)
                {
                    if (chargeLine.ChargeCode.Equals(channelConfiguration.CancellationChargeCode, StringComparison.OrdinalIgnoreCase))
                    {
                        cancellationChargeAmount += chargeLine.CalculatedAmount + chargeLine.TaxAmountExclusive;
                    }
                }

                return(SalesTransactionTotaler.RoundCurrencyAmount(context, cancellationChargeAmount));
            }
Ejemplo n.º 8
0
            /// <summary>
            /// Calculates the required amount that must be paid for a customer order transaction.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The transaction that must be used for the calculation.</param>
            /// <returns>The amount that must be paid for this transaction.</returns>
            public static decimal CalculateRequiredPaymentAmount(RequestContext context, SalesTransaction salesTransaction)
            {
                ThrowIf.Null(context, "context");
                ThrowIf.Null(salesTransaction, "salesTransaction");

                if (salesTransaction.CartType != CartType.CustomerOrder)
                {
                    throw new InvalidOperationException("Transaction must be a customer order.");
                }

                switch (salesTransaction.CustomerOrderMode)
                {
                case CustomerOrderMode.CustomerOrderCreateOrEdit:
                    return(salesTransaction.RequiredDepositAmount - salesTransaction.PrepaymentAmountPaid);

                case CustomerOrderMode.QuoteCreateOrEdit:
                    return(decimal.Zero);

                case CustomerOrderMode.Cancellation:
                    decimal cancellationChargeAmount = SalesTransactionTotaler.GetCancellationChargeAmount(
                        context,
                        salesTransaction,
                        context.GetChannelConfiguration());

                    // We need to refund the amount paid for the order minus cancellation charges
                    // For refunding, we use a negative amount due
                    return(cancellationChargeAmount - salesTransaction.PrepaymentAmountPaid);

                case CustomerOrderMode.Return:
                    return(salesTransaction.TotalAmount);

                case CustomerOrderMode.Pickup:
                    // customer needs to pay the total for the transaction minus the credit he has due to prepayments
                    // in case he has more credit than what the transaction is worth, we need to refund him
                    return(salesTransaction.TotalAmount - salesTransaction.PrepaymentAmountAppliedOnPickup);

                case CustomerOrderMode.None:
                case CustomerOrderMode.OrderRecalled:
                    return(decimal.Zero);

                default:
                    throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Customer order mode '{0}' not supported.", salesTransaction.CustomerOrderMode));
                }
            }
Ejemplo n.º 9
0
            private static void CalculateAmountDue(RequestContext context, SalesTransaction salesTransaction)
            {
                if (salesTransaction == null)
                {
                    throw new ArgumentNullException("salesTransaction");
                }

                ChannelConfiguration channelConfiguration = context.GetChannelConfiguration();
                string cancellationcode = channelConfiguration.CancellationChargeCode;
                string currencyCode     = channelConfiguration.Currency;

                RoundingRule roundingRule = amountToRound => RoundWithPricesRounding(context, amountToRound, currencyCode);

                SalesTransactionTotaler.ClearTotalAmounts(salesTransaction);
                salesTransaction.NumberOfItems = 0m;

                // initialize with header-level charges list
                var charges = new List <ChargeLine>();

                if (salesTransaction.ChargeLines.Any())
                {
                    charges.AddRange(salesTransaction.ChargeLines);
                }

                // Calculate totals for sale items , which might also include line-level misc charge in it.
                foreach (SalesLine saleLineItem in salesTransaction.SalesLines)
                {
                    if (saleLineItem.IsVoided == false)
                    {
                        // Calculate the sum of items
                        salesTransaction.NumberOfItems += Math.Abs(saleLineItem.Quantity);

                        // calculate the line item cost excluding charges and tax on charges.
                        SalesLineTotaller.CalculateLine(salesTransaction, saleLineItem, roundingRule);

                        UpdateTotalAmounts(salesTransaction, saleLineItem);
                    }
                    else
                    {
                        saleLineItem.PeriodicDiscountPossibilities.Clear();
                        SalesLineTotaller.CalculateLine(salesTransaction, saleLineItem, roundingRule);
                    }

                    saleLineItem.WasChanged = false;
                }

                // Add eligible charges on sales lines
                foreach (SalesLine salesLine in salesTransaction.ChargeCalculableSalesLines)
                {
                    charges.AddRange(salesLine.ChargeLines);
                }

                decimal incomeExpenseTotalAmount = 0m;

                foreach (IncomeExpenseLine incomeExpense in salesTransaction.IncomeExpenseLines)
                {
                    if (incomeExpense.AccountType != IncomeExpenseAccountType.None)
                    {
                        salesTransaction.NetAmountWithTax         += incomeExpense.Amount;
                        salesTransaction.NetAmountWithNoTax       += incomeExpense.Amount;
                        salesTransaction.GrossAmount              += incomeExpense.Amount;
                        salesTransaction.IncomeExpenseTotalAmount += incomeExpense.Amount;

                        // The total is done to calculate the payment amount.
                        incomeExpenseTotalAmount += incomeExpense.Amount;
                    }
                }

                foreach (ChargeLine charge in charges)
                {
                    AddToTaxItems(salesTransaction, charge);

                    // Calculate tax on the charge item.
                    CalculateTaxForCharge(charge);

                    if (charge.ChargeCode.Equals(cancellationcode, StringComparison.OrdinalIgnoreCase) && IsSeparateTaxInCancellationCharge(context))
                    {
                        salesTransaction.TaxOnCancellationCharge += charge.TaxAmount;
                    }
                    else
                    {
                        salesTransaction.TaxAmount += charge.TaxAmount;
                    }

                    // Later there is "TotalAmount = NetAmountWithTax + ChargeAmount", so we should add TaxAmountExclusive here
                    salesTransaction.NetAmountWithTax += charge.TaxAmountExclusive;
                }

                salesTransaction.DiscountAmount = salesTransaction.PeriodicDiscountAmount + salesTransaction.LineDiscount + salesTransaction.TotalDiscount;
                salesTransaction.ChargeAmount   = salesTransaction.ChargesTotal();

                // Subtotal is the net amount for the transaction (which includes the discounts) and optionally the tax amount if tax inclusive
                salesTransaction.SubtotalAmount = roundingRule(salesTransaction.NetAmountWithNoTax + salesTransaction.TaxAmountInclusive);

                salesTransaction.SubtotalAmountWithoutTax = context.GetChannelConfiguration().PriceIncludesSalesTax
                                                                ? salesTransaction.SubtotalAmount - salesTransaction.TaxAmount
                                                                : salesTransaction.SubtotalAmount;

                // Net amount when saved should include charges, it should be done after Subtotal amount calc because Subtotal does not include charge amount.
                salesTransaction.NetAmountWithNoTax = roundingRule(salesTransaction.NetAmountWithNoTax + salesTransaction.ChargeAmount);

                if (salesTransaction.IncomeExpenseLines.Any())
                {
                    // Setting the total amount sames as Payment amount for Income/ expense accounts.
                    salesTransaction.TotalAmount = incomeExpenseTotalAmount;
                }
                else if (salesTransaction.TransactionType == SalesTransactionType.CustomerAccountDeposit && salesTransaction.CustomerAccountDepositLines.Any())
                {
                    CustomerAccountDepositLine customerAccountDepositLine = salesTransaction.CustomerAccountDepositLines.SingleOrDefault();
                    salesTransaction.SubtotalAmountWithoutTax = customerAccountDepositLine.Amount;
                    salesTransaction.TotalAmount = customerAccountDepositLine.Amount;
                }
                else
                {
                    // NetAmountWithTax already includes the discounts
                    salesTransaction.TotalAmount = roundingRule(salesTransaction.NetAmountWithTax + salesTransaction.ChargeAmount);
                }
            }
Ejemplo n.º 10
0
            /// <summary>
            /// Populates the fields for total amount, total discount, and total taxes on the sales line.
            /// </summary>
            /// <param name="context">The request context.</param>
            /// <param name="salesTransaction">The sales transaction.</param>
            /// <param name="salesLine">The sales line.</param>
            public static void CalculateLine(RequestContext context, SalesTransaction salesTransaction, SalesLine salesLine)
            {
                RoundingRule roundingRule = SalesTransactionTotaler.GetRoundingRule(context);

                SalesLineTotaller.CalculateLine(salesTransaction, salesLine, roundingRule);
            }
Ejemplo n.º 11
0
 /// <summary>
 /// Calculates the amount paid and due on the sales transaction.
 /// </summary>
 /// <param name="calculateAmountPaidAndDueRequest">The request.</param>
 /// <returns>The service response.</returns>
 private static Response CalculateAmountPaidAndDue(CalculateAmountPaidAndDueServiceRequest calculateAmountPaidAndDueRequest)
 {
     SalesTransactionTotaler.CalculateAmountPaidAndDue(calculateAmountPaidAndDueRequest.RequestContext, calculateAmountPaidAndDueRequest.Transaction);
     return(new CalculateAmountPaidAndDueServiceResponse(calculateAmountPaidAndDueRequest.Transaction));
 }
Ejemplo n.º 12
0
 /// <summary>
 /// Calculates the totals on the sales transaction.
 /// </summary>
 /// <param name="calculateTotalsRequest">The request.</param>
 /// <returns>The service response.</returns>
 private static Response CalculateTotals(CalculateTotalsServiceRequest calculateTotalsRequest)
 {
     SalesTransactionTotaler.CalculateTotals(calculateTotalsRequest.RequestContext, calculateTotalsRequest.Transaction);
     return(new CalculateTotalsServiceResponse(calculateTotalsRequest.Transaction));
 }