Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace VirtoCommerce.OrdersModule.Web.Model
namespace VirtoCommerce.OrdersModule.Core.Model
{
public class KeyValuePair
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace VirtoCommerce.OrdersModule.Web.Model
namespace VirtoCommerce.OrdersModule.Core.Model
{
public class PaymentCallbackParameters
{
Expand Down
10 changes: 10 additions & 0 deletions src/VirtoCommerce.OrdersModule.Core/Model/PaymentParameters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Specialized;

namespace VirtoCommerce.OrdersModule.Core.Model;

public class PaymentParameters
{
public string OrderId { get; set; }
public string PaymentMethodCode { get; set; }
public NameValueCollection Parameters { get; set; } = new();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.PaymentModule.Model.Requests;

namespace VirtoCommerce.OrdersModule.Core.Services
{
public interface ICustomerOrderPaymentService
{
Task<PostProcessPaymentRequestResult> PostProcessPaymentAsync(PaymentParameters paymentParameters);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.PaymentModule.Model.Requests;

namespace VirtoCommerce.OrdersModule.Core.Services;

public interface IPaymentRequestConverter
{
PaymentParameters GetPaymentParameters(PaymentCallbackParameters request);
PaymentParameters GetPaymentParameters(string requestBody, string requestQuery);
(object, bool) GetResponse(PostProcessPaymentRequestResult result);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using FluentValidation.Results;
using VirtoCommerce.PaymentModule.Model.Requests;

namespace VirtoCommerce.OrdersModule.Data.Model;

public class PostProcessPaymentRequestNotValidResult : PostProcessPaymentRequestResult
{
public IList<ValidationFailure> Errors { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentValidation;
using FluentValidation.Results;
using VirtoCommerce.OrdersModule.Core;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Core.Model.Search;
using VirtoCommerce.OrdersModule.Core.Services;
using VirtoCommerce.OrdersModule.Data.Model;
using VirtoCommerce.PaymentModule.Model.Requests;
using VirtoCommerce.Platform.Core.Common;
using VirtoCommerce.Platform.Core.Settings;
using VirtoCommerce.StoreModule.Core.Model;
using VirtoCommerce.StoreModule.Core.Services;

namespace VirtoCommerce.OrdersModule.Data.Services;

public class CustomerOrderPaymentService(
IStoreService storeService,
ICustomerOrderService customerOrderService,
ICustomerOrderSearchService customerOrderSearchService,
IValidator<CustomerOrder> customerOrderValidator,
ISettingsManager settingsManager)
: ICustomerOrderPaymentService
{
public virtual async Task<PostProcessPaymentRequestResult> PostProcessPaymentAsync(PaymentParameters paymentParameters)
{
ArgumentNullException.ThrowIfNull(paymentParameters);

var customerOrder = await GetCustomerOrder(paymentParameters);
if (customerOrder == null)
{
throw new InvalidOperationException($"Cannot find order with ID {paymentParameters.OrderId}");
}

var store = await storeService.GetByIdAsync(customerOrder.StoreId, nameof(StoreResponseGroup.StoreInfo));
if (store == null)
{
throw new InvalidOperationException($"Cannot find store with ID {customerOrder.StoreId}");
}

var inPayments = GetInPayments(customerOrder, paymentParameters);

foreach (var inPayment in inPayments)
{
// Each payment method must check that these parameters are addressed to it
var paymentMethodValidationResult = inPayment.PaymentMethod.ValidatePostProcessRequest(paymentParameters.Parameters);
if (!paymentMethodValidationResult.IsSuccess)
{
continue;
}

var paymentMethodRequest = new PostProcessPaymentRequest
{
OrderId = customerOrder.Id,
Order = customerOrder,
PaymentId = inPayment.Id,
Payment = inPayment,
StoreId = customerOrder.StoreId,
Store = store,
OuterId = paymentMethodValidationResult.OuterId,
Parameters = paymentParameters.Parameters,
};

var paymentMethodPostProcessResult = inPayment.PaymentMethod.PostProcessPayment(paymentMethodRequest);
if (paymentMethodPostProcessResult == null)
{
continue;
}

var customerOrderValidationResult = await ValidateAsync(customerOrder);
if (!customerOrderValidationResult.IsValid)
{
return new PostProcessPaymentRequestNotValidResult
{
Errors = customerOrderValidationResult.Errors,
ErrorMessage = string.Join(" ", customerOrderValidationResult.Errors.Select(x => x.ErrorMessage)),
};
}

await customerOrderService.SaveChangesAsync([customerOrder]);

// Order number is required
paymentMethodPostProcessResult.OrderId = customerOrder.Number;

return paymentMethodPostProcessResult;
}

return new PostProcessPaymentRequestResult { ErrorMessage = "Payment method not found" };
}

protected virtual async Task<CustomerOrder> GetCustomerOrder(PaymentParameters paymentParameters)
{
if (string.IsNullOrEmpty(paymentParameters.OrderId))
{
throw new InvalidOperationException("The 'orderid' parameter must be passed");
}

// Some payment method require customer number to be passed and returned. First search customer order by number
var searchCriteria = AbstractTypeFactory<CustomerOrderSearchCriteria>.TryCreateInstance();
searchCriteria.Number = paymentParameters.OrderId;
searchCriteria.ResponseGroup = nameof(CustomerOrderResponseGroup.Full);

// If order is not found by order number, search by order id
var orders = await customerOrderSearchService.SearchAsync(searchCriteria);

return orders.Results.FirstOrDefault() ?? await customerOrderService.GetByIdAsync(paymentParameters.OrderId, nameof(CustomerOrderResponseGroup.Full));
}

protected virtual IList<PaymentIn> GetInPayments(CustomerOrder customerOrder, PaymentParameters paymentParameters)
{
// Need to use concrete payment method if its code has been passed, otherwise use all order payment methods
return customerOrder.InPayments
.Where(x => x.PaymentMethod != null && (string.IsNullOrEmpty(paymentParameters.PaymentMethodCode) || x.GatewayCode.EqualsIgnoreCase(paymentParameters.PaymentMethodCode)))
.ToList();
}

protected virtual async Task<ValidationResult> ValidateAsync(CustomerOrder customerOrder)
{
if (await settingsManager.GetValueAsync<bool>(ModuleConstants.Settings.General.CustomerOrderValidation))
{
return await customerOrderValidator.ValidateAsync(customerOrder);
}

return new ValidationResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Newtonsoft.Json;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Core.Services;
using VirtoCommerce.OrdersModule.Data.Model;
using VirtoCommerce.PaymentModule.Model.Requests;
using VirtoCommerce.Platform.Core.Common;

namespace VirtoCommerce.OrdersModule.Data.Services;

public class PaymentRequestDefaultConverter : IPaymentRequestConverter
{
public virtual PaymentParameters GetPaymentParameters(PaymentCallbackParameters request)
{
var result = AbstractTypeFactory<PaymentParameters>.TryCreateInstance();

foreach (var parameter in request?.Parameters ?? [])
{
result.Parameters.Add(parameter.Key, parameter.Value);
}

result.OrderId = result.Parameters.Get("orderid");
result.PaymentMethodCode = result.Parameters.Get("code");

return result;
}

public virtual PaymentParameters GetPaymentParameters(string requestBody, string requestQuery)
{
var paymentCallbackParameters = JsonConvert.DeserializeObject<PaymentCallbackParameters>(requestBody);
return GetPaymentParameters(paymentCallbackParameters);
}

public virtual (object, bool) GetResponse(PostProcessPaymentRequestResult result)
{
if (result is PostProcessPaymentRequestNotValidResult notValidResult)
{
var response = new
{
Message = notValidResult.ErrorMessage,
Errors = notValidResult.Errors,
};

return (response, false);
}

return (result, true);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DinkToPdf;
using DinkToPdf.Contracts;
Expand Down Expand Up @@ -30,7 +31,6 @@
using VirtoCommerce.OrdersModule.Data.Authorization;
using VirtoCommerce.OrdersModule.Data.Caching;
using VirtoCommerce.OrdersModule.Data.Extensions;
using VirtoCommerce.OrdersModule.Web.Model;
using VirtoCommerce.PaymentModule.Core.Model;
using VirtoCommerce.PaymentModule.Data;
using VirtoCommerce.PaymentModule.Model.Requests;
Expand Down Expand Up @@ -68,7 +68,9 @@ public class OrderModuleController(
IOptions<HtmlToPdfOptions> htmlToPdfOptions,
IOptions<OutputJsonSerializerSettings> outputJsonSerializerSettings,
IValidator<CustomerOrder> customerOrderValidator,
ISettingsManager settingsManager)
ISettingsManager settingsManager,
IPaymentRequestConverter paymentRequestConverter,
ICustomerOrderPaymentService customerOrderPaymentService)
: Controller
{
/// <summary>
Expand Down Expand Up @@ -521,78 +523,37 @@ public async Task<ActionResult<DashboardStatisticsResult>> GetDashboardStatistic
[Route("~/api/paymentcallback")]
public async Task<ActionResult<PostProcessPaymentRequestResult>> PostProcessPayment([FromBody] PaymentCallbackParameters callback)
{
var parameters = new NameValueCollection();
foreach (var param in callback?.Parameters ?? Array.Empty<KeyValuePair>())
{
parameters.Add(param.Key, param.Value);
}
var orderId = parameters.Get("orderid");
if (string.IsNullOrEmpty(orderId))
{
throw new InvalidOperationException("the 'orderid' parameter must be passed");
}
var parameters = paymentRequestConverter.GetPaymentParameters(callback);
var result = await customerOrderPaymentService.PostProcessPaymentAsync(parameters);

//some payment method require customer number to be passed and returned. First search customer order by number
var searchCriteria = AbstractTypeFactory<CustomerOrderSearchCriteria>.TryCreateInstance();
searchCriteria.Number = orderId;
searchCriteria.ResponseGroup = CustomerOrderResponseGroup.Full.ToString();
//if order not found by order number search by order id
var orders = await searchService.SearchAsync(searchCriteria);
var customerOrder = orders.Results.FirstOrDefault() ?? await customerOrderService.GetByIdAsync(orderId, CustomerOrderResponseGroup.Full.ToString());
var (response, succeeded) = paymentRequestConverter.GetResponse(result);

if (customerOrder == null)
{
throw new InvalidOperationException($"Cannot find order with ID {orderId}");
}
return succeeded
? Ok(response)
: BadRequest(response);
}

var store = await storeService.GetByIdAsync(customerOrder.StoreId, StoreResponseGroup.StoreInfo.ToString());
if (store == null)
{
throw new InvalidOperationException($"Cannot find store with ID {customerOrder.StoreId}");
}
/// <summary>
/// Payment callback operation used by external payment services to inform post process payment in our system
/// </summary>
[HttpPost]
[Route("~/api/paymentcallback-raw")]
public async Task<ActionResult<PostProcessPaymentRequestResult>> PostProcessPaymentRaw()
{
var parameters = paymentRequestConverter.GetPaymentParameters(await GetRequestBody(), Request.QueryString.Value);
var result = await customerOrderPaymentService.PostProcessPaymentAsync(parameters);

var paymentMethodCode = parameters.Get("code");
var (response, succeeded) = paymentRequestConverter.GetResponse(result);

//Need to use concrete payment method if it code passed otherwise use all order payment methods
foreach (var inPayment in customerOrder.InPayments.Where(x => x.PaymentMethod != null && (string.IsNullOrEmpty(paymentMethodCode) || x.GatewayCode.EqualsIgnoreCase(paymentMethodCode))))
{
//Each payment method must check that these parameters are addressed to it
var result = inPayment.PaymentMethod.ValidatePostProcessRequest(parameters);
if (result.IsSuccess)
{
return succeeded
? Ok(response)
: BadRequest(response);
}

var request = new PostProcessPaymentRequest
{
OrderId = customerOrder.Id,
Order = customerOrder,
PaymentId = inPayment.Id,
Payment = inPayment,
StoreId = customerOrder.StoreId,
Store = store,
OuterId = result.OuterId,
Parameters = parameters
};
var retVal = inPayment.PaymentMethod.PostProcessPayment(request);
if (retVal != null)
{
var validationResult = await ValidateAsync(customerOrder);
if (!validationResult.IsValid)
{
return BadRequest(new
{
Message = string.Join(" ", validationResult.Errors.Select(x => x.ErrorMessage)),
validationResult.Errors
});
}
await customerOrderService.SaveChangesAsync(new[] { customerOrder });

// order Number is required
retVal.OrderId = customerOrder.Number;
}
return Ok(retVal);
}
}
return Ok(new PostProcessPaymentRequestResult { ErrorMessage = "Payment method not found" });
private async Task<string> GetRequestBody()
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, 1024, leaveOpen: true);
return await reader.ReadToEndAsync();
}

[HttpGet]
Expand Down
2 changes: 2 additions & 0 deletions src/VirtoCommerce.OrdersModule.Web/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public void Initialize(IServiceCollection serviceCollection)
serviceCollection.AddTransient<IShipmentSearchService, ShipmentSearchService>();
serviceCollection.AddTransient<ICustomerOrderBuilder, CustomerOrderBuilder>();
serviceCollection.AddTransient<ICustomerOrderTotalsCalculator, DefaultCustomerOrderTotalsCalculator>();
serviceCollection.AddTransient<ICustomerOrderPaymentService, CustomerOrderPaymentService>();
serviceCollection.AddTransient<OrderExportImport>();
serviceCollection.AddTransient<AdjustInventoryOrderChangedEventHandler>();
serviceCollection.AddTransient<CancelPaymentOrderChangedEventHandler>();
Expand All @@ -101,6 +102,7 @@ public void Initialize(IServiceCollection serviceCollection)
serviceCollection.AddTransient<SendNotificationsOrderChangedEventHandler>();
serviceCollection.AddTransient<PolymorphicOperationJsonConverter>();
serviceCollection.AddTransient<IAuthorizationHandler, OrderAuthorizationHandler>();
serviceCollection.AddTransient<IPaymentRequestConverter, PaymentRequestDefaultConverter>();

serviceCollection.AddTransient<IPurchasedProductsService, PurchasedProductsService>();
serviceCollection.AddTransient<PurchasedProductsChangesProvider>();
Expand Down
Loading
Loading