Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
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; }
}
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,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,107 @@
using System;
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);

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 = CustomerOrderResponseGroup.Full.ToString();
//if order not found by order number search by order id
var orders = await customerOrderSearchService.SearchAsync(searchCriteria);
var customerOrder = orders.Results.FirstOrDefault() ?? await customerOrderService.GetByIdAsync(paymentParameters.OrderId, CustomerOrderResponseGroup.Full.ToString());

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

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

//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(paymentParameters.PaymentMethodCode) || x.GatewayCode.EqualsIgnoreCase(paymentParameters.PaymentMethodCode))))
{
//Each payment method must check that these parameters are addressed to it
var result = inPayment.PaymentMethod.ValidatePostProcessRequest(paymentParameters.Parameters);
if (result.IsSuccess)
{

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

// order Number is required
retVal.OrderId = customerOrder.Number;
}
return retVal;
}
}
return new PostProcessPaymentRequestResult { ErrorMessage = "Payment method not found" };
}

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
@@ -1,5 +1,4 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using DinkToPdf;
Expand Down Expand Up @@ -30,6 +29,8 @@
using VirtoCommerce.OrdersModule.Data.Authorization;
using VirtoCommerce.OrdersModule.Data.Caching;
using VirtoCommerce.OrdersModule.Data.Extensions;
using VirtoCommerce.OrdersModule.Data.Model;
using VirtoCommerce.OrdersModule.Web.Converters;
using VirtoCommerce.OrdersModule.Web.Model;
using VirtoCommerce.PaymentModule.Core.Model;
using VirtoCommerce.PaymentModule.Data;
Expand Down Expand Up @@ -68,7 +69,9 @@ public class OrderModuleController(
IOptions<HtmlToPdfOptions> htmlToPdfOptions,
IOptions<OutputJsonSerializerSettings> outputJsonSerializerSettings,
IValidator<CustomerOrder> customerOrderValidator,
ISettingsManager settingsManager)
ISettingsManager settingsManager,
IPaymentParametersConverter paymentParametersConverter,
ICustomerOrderPaymentService customerOrderPaymentService)
: Controller
{
/// <summary>
Expand Down Expand Up @@ -521,78 +524,51 @@ 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 = paymentParametersConverter.GetPaymentParameters(callback);

//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 result = await customerOrderPaymentService.PostProcessPaymentAsync(parameters);

if (customerOrder == null)
if (result is PostProcessPaymentRequestNotValidResult notValidResult)
{
throw new InvalidOperationException($"Cannot find order with ID {orderId}");
return BadRequest(new
{
Message = notValidResult.ErrorMessage,
notValidResult.Errors
});
}

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

var paymentMethodCode = parameters.Get("code");
/// <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 = paymentParametersConverter.GetPaymentParameters(await GetRequestBody(), Request.QueryString.Value);

var result = await customerOrderPaymentService.PostProcessPaymentAsync(parameters);

//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))))
if (result is PostProcessPaymentRequestNotValidResult notValidResult)
{
//Each payment method must check that these parameters are addressed to it
var result = inPayment.PaymentMethod.ValidatePostProcessRequest(parameters);
if (result.IsSuccess)
return BadRequest(new
{
Message = notValidResult.ErrorMessage,
notValidResult.Errors
});
}

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

private async Task<string> GetRequestBody()
{
using (var reader = new System.IO.StreamReader(Request.Body, System.Text.Encoding.UTF8, true, 1024, true))
{
return await reader.ReadToEndAsync();
}
return Ok(new PostProcessPaymentRequestResult { ErrorMessage = "Payment method not found" });
}

[HttpGet]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Web.Model;

namespace VirtoCommerce.OrdersModule.Web.Converters;

public interface IPaymentParametersConverter
{
PaymentParameters GetPaymentParameters(PaymentCallbackParameters request);
PaymentParameters GetPaymentParameters(string requestBody, string requestQuery);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System;
using System.Collections.Specialized;
using Newtonsoft.Json;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Web.Model;

namespace VirtoCommerce.OrdersModule.Web.Converters;

public class PaymentParametersDefaultConverter : IPaymentParametersConverter
{
public virtual PaymentParameters GetPaymentParameters(PaymentCallbackParameters request)
{
var result = new PaymentParameters();

result.Parameters = new NameValueCollection();
foreach (var parameter in request?.Parameters ?? Array.Empty<KeyValuePair>())
{
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);
}
}
3 changes: 3 additions & 0 deletions src/VirtoCommerce.OrdersModule.Web/Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using VirtoCommerce.OrdersModule.Data.Services;
using VirtoCommerce.OrdersModule.Data.SqlServer;
using VirtoCommerce.OrdersModule.Web.Authorization;
using VirtoCommerce.OrdersModule.Web.Converters;
using VirtoCommerce.OrdersModule.Web.Extensions;
using VirtoCommerce.OrdersModule.Web.JsonConverters;
using VirtoCommerce.Platform.Core.Common;
Expand Down Expand Up @@ -92,6 +93,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 +103,7 @@ public void Initialize(IServiceCollection serviceCollection)
serviceCollection.AddTransient<SendNotificationsOrderChangedEventHandler>();
serviceCollection.AddTransient<PolymorphicOperationJsonConverter>();
serviceCollection.AddTransient<IAuthorizationHandler, OrderAuthorizationHandler>();
serviceCollection.AddTransient<IPaymentParametersConverter, PaymentParametersDefaultConverter>();

serviceCollection.AddTransient<IPurchasedProductsService, PurchasedProductsService>();
serviceCollection.AddTransient<PurchasedProductsChangesProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Moq;
using VirtoCommerce.AssetsModule.Core.Assets;
using VirtoCommerce.CoreModule.Core.Common;
using VirtoCommerce.OrdersModule.Core.Model;
using VirtoCommerce.OrdersModule.Core.Model.Search;
Expand Down Expand Up @@ -54,6 +55,7 @@ public class CustomerOrderServiceImplIntegrationTests
private readonly ICustomerOrderSearchService _customerOrderSearchService;
private readonly Mock<ILogger<PlatformMemoryCache>> _logMock;
private readonly Mock<ILogger<InProcessBus>> _logEventMock;
private readonly Mock<IBlobUrlResolver> _blobUrlResolver;

public CustomerOrderServiceImplIntegrationTests()
{
Expand All @@ -72,6 +74,7 @@ public CustomerOrderServiceImplIntegrationTests()
_changeLogServiceMock = new Mock<IChangeLogService>();
_logMock = new Mock<ILogger<PlatformMemoryCache>>();
_logEventMock = new Mock<ILogger<InProcessBus>>();
_blobUrlResolver = new Mock<IBlobUrlResolver>();
var cachingOptions = new OptionsWrapper<CachingOptions>(new CachingOptions { CacheEnabled = true });
var memoryCache = new MemoryCache(new MemoryCacheOptions()
{
Expand All @@ -97,6 +100,7 @@ public CustomerOrderServiceImplIntegrationTests()
container.AddSingleton(x => _platformMemoryCache);
container.AddSingleton(x => _changeLogServiceMock.Object);
container.AddSingleton(x => _logEventMock.Object);
container.AddSingleton(x => _blobUrlResolver.Object);
container.AddOptions<CrudOptions>();

var serviceProvider = container.BuildServiceProvider();
Expand Down
Loading
Loading