add version 0.19.22.39 . fix zarinpall error , add sitemap test

release
Amir Hossein Khademi 2024-04-10 20:52:32 +03:30
parent d8605fa884
commit 3118de0509
19 changed files with 352 additions and 43 deletions

View File

@ -1 +1 @@
0.19.22.38
0.19.22.39

View File

@ -19,6 +19,7 @@
"SiteSettings": {
"BaseUrl": "https://api.vesmeh.com",
"AdminPanelBaseUrl": "https://admin.vesmeh.com",
"StorageBaseUrl": "https://storage.vesmeh.ir",
"KaveNegarApiKey": "3735494B4143727A794346457461576A2B4B6668414973424E333561505A694B",
"UserSetting": {
"Username": "09214802813",

View File

@ -19,6 +19,7 @@
"SiteSettings": {
"BaseUrl": "http://localhost:32770",
"AdminPanelBaseUrl": "https://admin.vesmeh.com",
"StorageBaseUrl": "https://storage.vesmeh.ir",
"KaveNegarApiKey": "3735494B4143727A794346457461576A2B4B6668414973424E333561505A694B",
"UserSetting": {
"Username": "netinashop",

View File

@ -13,8 +13,9 @@ public class HealthController : ICarterModule
.HasApiVersion(1.0);
}
public IResult GetHealth()
public async Task<IResult> GetHealth([FromServices]ISiteMapService siteMapService)
{
await siteMapService.CreateSiteMapAsync();
var version = typeof(Program)?.Assembly.GetName()?.Version?.ToString();
var check = new HealthCheck
{

View File

@ -34,24 +34,16 @@ public class PaymentController : ICarterModule
=> TypedResults.Ok(await mediator.Send(new GetShippingQuery(id), cancellationToken));
// POST:Create Entity
public async Task<IResult> VerifyPaymentAsync([FromQuery] string Authority, [FromQuery] string Status, IPaymentService paymentService,ILogger<PaymentController> logger, CancellationToken cancellationToken)
public async Task<IResult> VerifyPaymentAsync([FromQuery] string Authority, [FromQuery] string Status, IPaymentService paymentService, ILogger<PaymentController> logger, CancellationToken cancellationToken)
{
try
if (Status == "OK")
{
if (Status == "OK")
{
var result = await paymentService.VerifyPaymentAsync(authority: Authority, cancellationToken);
return TypedResults.Redirect($"https://vesmeh.com/purchase-callback?refid={result.Item1}&paymentStatus=true&factorNumber={result.Item2}", true);
}
else
{
return TypedResults.Redirect($"https://vesmeh.com/purchase-callback?refid=0&paymentStatus=false&factorNumber=0", true);
}
var result = await paymentService.VerifyPaymentAsync(authority: Authority, cancellationToken);
return TypedResults.Redirect($"https://vesmeh.com/purchase-callback?refid={result.Item1}&paymentStatus=true&factorNumber={result.Item2}", true);
}
catch (Exception e)
else
{
logger.LogError(e.Message);
return TypedResults.Redirect("");
return TypedResults.Redirect($"https://vesmeh.com/purchase-callback?refid=0&paymentStatus=false&factorNumber=0", true);
}
}

View File

@ -6,8 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<AssemblyVersion>0.19.22.38</AssemblyVersion>
<FileVersion>0.19.22.38</FileVersion>
<AssemblyVersion>0.19.22.39</AssemblyVersion>
<FileVersion>0.19.22.39</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -8,10 +8,13 @@
Handout,
[Display(Name = "Videos")]
Video,
[Display(Name = "site-maps")]
SiteMap
}
public class FileUploadRequest
{
public string StringBaseFile { get; set; }
public byte[] FileBytes { get; set; }
public string FileName { get; set; }
public string ContentType { get; set; }
public FileUploadType FileUploadType { get; set; }

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -12,9 +12,9 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.4.1" />
</ItemGroup>
</ItemGroup>-->
<!--<PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -25,7 +25,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.3.1" />
</ItemGroup>-->
</ItemGroup>
<ItemGroup>
<Using Include="MD.PersianDateTime.Standard" />

View File

@ -2,7 +2,7 @@
public interface IStorageService : IScopedDependency
{
Task<string> UploadObjectFromFileAsync(string fileName, string filePath, string contentType, byte[] fileBytes);
Task<string> UploadObjectFromFileAsync(string fileName, string filePath, string contentType, byte[] fileBytes, bool fixName = true);
Task<string> UploadObjectFromFileAsync(string fileName, string filePath, string contentType, Stream fileStream, bool fixName = true);
Task<List<StorageFileSDto>> GetStorageFiles(StorageFileType fileType);

View File

@ -3,4 +3,5 @@
public interface IUploadFileService : IScopedDependency
{
Task<FileUploadResponse> UploadImageAsync(FileUploadRequest uploadRequest);
Task<FileUploadResponse> UploadFileByteAsync(FileUploadRequest uploadRequest);
}

View File

@ -0,0 +1,273 @@
using System.IO;
using System.IO.Compression;
using System.IO.Pipes;
using System.Xml;
using NetinaShop.Core.Models;
using NetinaShop.Domain.Entities.Blogs;
using NetinaShop.Domain.Entities.ProductCategories;
namespace NetinaShop.Core.BaseServices.Abstracts;
public interface ISiteMapService : IScopedDependency
{
public Task CreateSiteMapAsync();
}
public class SiteMapService : ISiteMapService
{
private readonly IUploadFileService _uploadFileService;
private readonly IRepositoryWrapper _repositoryWrapper;
private readonly SiteSettings _siteSetting;
public SiteMapService(IOptionsSnapshot<SiteSettings> snapshot, IUploadFileService uploadFileService,IRepositoryWrapper repositoryWrapper)
{
_uploadFileService = uploadFileService;
_repositoryWrapper = repositoryWrapper;
_siteSetting = snapshot.Value;
}
public async Task CreateSiteMapAsync()
{
XmlDocument doc = new XmlDocument();
// XML declaration
XmlNode declaration = doc.CreateNode(XmlNodeType.XmlDeclaration, "sitemap.xml", null);
doc.AppendChild(declaration);
// Root element: Catalog
XmlElement root = doc.CreateElement("sitemapindex", "http://www.sitemaps.org/schemas/sitemap/0.9");
doc.AppendChild(root);
foreach (var siteMapsUId in SiteMapUIds.AllSiteMapsUIds)
{
XmlElement siteMapElement = doc.CreateElement("sitemap", doc.DocumentElement?.NamespaceURI);
root.AppendChild(siteMapElement);
XmlElement id = doc.CreateElement("loc", doc.DocumentElement?.NamespaceURI);
id.InnerText = Path.Combine(_siteSetting.StorageBaseUrl, "site-maps", $"{siteMapsUId}.gz");
siteMapElement.AppendChild(id);
XmlElement lastmod = doc.CreateElement("lastmod", doc.DocumentElement?.NamespaceURI);
lastmod.InnerText = DateTime.Today.ToString("yyyy-MM-dd");
siteMapElement.AppendChild(lastmod);
}
System.IO.MemoryStream stream = new System.IO.MemoryStream();
XmlTextWriter writer = new XmlTextWriter(stream, System.Text.Encoding.UTF8);
doc.WriteTo(writer);
writer.Flush();
byte[] byteArray = stream.ToArray();
await _uploadFileService.UploadFileByteAsync(new FileUploadRequest
{
FileBytes = byteArray,
ContentType = "text/xml",
FileName = "site-map.xml",
FileUploadType = FileUploadType.SiteMap,
});
await CreateCategoriesSiteMapsAsync();
await CreateProductsSiteMapsAsync();
await CreateBlogsSiteMapsAsync();
}
private async Task CreateCategoriesSiteMapsAsync()
{
var siteMapsUId = SiteMapUIds.Categories;
var categories = await _repositoryWrapper.SetRepository<ProductCategory>()
.TableNoTracking
.ToListAsync();
XmlDocument doc = new XmlDocument();
XmlNode declaration = doc.CreateNode(XmlNodeType.XmlDeclaration, "sitemap.xml", null);
doc.AppendChild(declaration);
XmlElement root = doc.CreateElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
root.SetAttribute("xmlns:image", "http://www.google.com/schemas/sitemap-image/1.1");
doc.AppendChild(root);
foreach (var productCategory in categories)
{
XmlElement urlElement = doc.CreateElement("url", doc.DocumentElement?.NamespaceURI);
root.AppendChild(urlElement);
XmlElement loc = doc.CreateElement("loc", doc.DocumentElement?.NamespaceURI);
loc.InnerText = Path.Combine(productCategory.Name);
urlElement.AppendChild(loc);
XmlElement lastmod = doc.CreateElement("lastmod", doc.DocumentElement?.NamespaceURI);
lastmod.InnerText = productCategory.ModifiedAt == DateTime.MinValue ? productCategory.CreatedAt.ToString("yyyy-MM-dd") : productCategory.ModifiedAt.ToString("yyyy-MM-dd");
urlElement.AppendChild(lastmod);
XmlElement changeFeq = doc.CreateElement("changefreq", doc.DocumentElement?.NamespaceURI);
changeFeq.InnerText = "daily";
urlElement.AppendChild(changeFeq);
XmlElement priority = doc.CreateElement("priority", doc.DocumentElement?.NamespaceURI);
priority.InnerText = "0.9";
urlElement.AppendChild(priority);
}
using var siteMapStream = new MemoryStream();
await using var siteMapWriter = new XmlTextWriter(siteMapStream, Encoding.UTF8);
doc.WriteTo(siteMapWriter);
siteMapWriter.Flush();
byte[] unZipBytes = siteMapStream.ToArray();
using (var compressedStream = new MemoryStream())
await using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
zipStream.Write(unZipBytes, 0, unZipBytes.Length);
zipStream.Close();
var siteMapArray = compressedStream.ToArray();
await _uploadFileService.UploadFileByteAsync(new FileUploadRequest
{
FileBytes = siteMapArray,
ContentType = "text/plain",
FileName = $"{siteMapsUId}.gz",
FileUploadType = FileUploadType.SiteMap,
});
}
}
private async Task CreateProductsSiteMapsAsync()
{
var siteMapsUId = SiteMapUIds.Products;
var products = await _repositoryWrapper.SetRepository<Product>()
.TableNoTracking
.ToListAsync();
XmlDocument doc = new XmlDocument();
XmlNode declaration = doc.CreateNode(XmlNodeType.XmlDeclaration, "sitemap.xml", null);
doc.AppendChild(declaration);
XmlElement root = doc.CreateElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
root.SetAttribute("xmlns:image", "http://www.google.com/schemas/sitemap-image/1.1");
doc.AppendChild(root);
foreach (var product in products)
{
XmlElement urlElement = doc.CreateElement("url", doc.DocumentElement?.NamespaceURI);
root.AppendChild(urlElement);
XmlElement loc = doc.CreateElement("loc", doc.DocumentElement?.NamespaceURI);
loc.InnerText = Path.Combine(product.PersianName);
urlElement.AppendChild(loc);
XmlElement lastmod = doc.CreateElement("lastmod", doc.DocumentElement?.NamespaceURI);
lastmod.InnerText = product.ModifiedAt == DateTime.MinValue ? product.CreatedAt.ToString("yyyy-MM-dd") : product.ModifiedAt.ToString("yyyy-MM-dd");
urlElement.AppendChild(lastmod);
XmlElement changeFeq = doc.CreateElement("changefreq", doc.DocumentElement?.NamespaceURI);
changeFeq.InnerText = "daily";
urlElement.AppendChild(changeFeq);
XmlElement priority = doc.CreateElement("priority", doc.DocumentElement?.NamespaceURI);
priority.InnerText = "0.9";
urlElement.AppendChild(priority);
}
using var siteMapStream = new MemoryStream();
await using var siteMapWriter = new XmlTextWriter(siteMapStream, Encoding.UTF8);
doc.WriteTo(siteMapWriter);
siteMapWriter.Flush();
byte[] unZipBytes = siteMapStream.ToArray();
using (var compressedStream = new MemoryStream())
await using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
zipStream.Write(unZipBytes, 0, unZipBytes.Length);
zipStream.Close();
var siteMapArray = compressedStream.ToArray();
await _uploadFileService.UploadFileByteAsync(new FileUploadRequest
{
FileBytes = siteMapArray,
ContentType = "text/plain",
FileName = $"{siteMapsUId}.gz",
FileUploadType = FileUploadType.SiteMap,
});
}
}
private async Task CreateBlogsSiteMapsAsync()
{
var siteMapsUId = SiteMapUIds.Blogs;
var categories = await _repositoryWrapper.SetRepository<Blog>()
.TableNoTracking
.ToListAsync();
XmlDocument doc = new XmlDocument();
XmlNode declaration = doc.CreateNode(XmlNodeType.XmlDeclaration, "sitemap.xml", null);
doc.AppendChild(declaration);
XmlElement root = doc.CreateElement("urlset", "http://www.sitemaps.org/schemas/sitemap/0.9");
root.SetAttribute("xmlns:image", "http://www.google.com/schemas/sitemap-image/1.1");
doc.AppendChild(root);
foreach (var productCategory in categories)
{
XmlElement urlElement = doc.CreateElement("url", doc.DocumentElement?.NamespaceURI);
root.AppendChild(urlElement);
XmlElement loc = doc.CreateElement("loc", doc.DocumentElement?.NamespaceURI);
loc.InnerText = Path.Combine(productCategory.Title);
urlElement.AppendChild(loc);
XmlElement lastmod = doc.CreateElement("lastmod", doc.DocumentElement?.NamespaceURI);
lastmod.InnerText = productCategory.ModifiedAt == DateTime.MinValue ? productCategory.CreatedAt.ToString("yyyy-MM-dd") : productCategory.ModifiedAt.ToString("yyyy-MM-dd");
urlElement.AppendChild(lastmod);
XmlElement changeFeq = doc.CreateElement("changefreq", doc.DocumentElement?.NamespaceURI);
changeFeq.InnerText = "daily";
urlElement.AppendChild(changeFeq);
XmlElement priority = doc.CreateElement("priority", doc.DocumentElement?.NamespaceURI);
priority.InnerText = "0.9";
urlElement.AppendChild(priority);
}
using var siteMapStream = new MemoryStream();
await using var siteMapWriter = new XmlTextWriter(siteMapStream, Encoding.UTF8);
doc.WriteTo(siteMapWriter);
siteMapWriter.Flush();
byte[] unZipBytes = siteMapStream.ToArray();
using (var compressedStream = new MemoryStream())
await using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
{
zipStream.Write(unZipBytes, 0, unZipBytes.Length);
zipStream.Close();
var siteMapArray = compressedStream.ToArray();
await _uploadFileService.UploadFileByteAsync(new FileUploadRequest
{
FileBytes = siteMapArray,
ContentType = "text/plain",
FileName = $"{siteMapsUId}.gz",
FileUploadType = FileUploadType.SiteMap,
});
}
}
}

View File

@ -16,17 +16,14 @@ public class SubmitOrderPaymentCommandHandler : IRequestHandler<SubmitOrderPayme
{
await _mediator.Send(new CalculateOrderCommand(request.OrderId, true), cancellationToken);
var orderSDto = await _repositoryWrapper.SetRepository<Order>()
var order = await _repositoryWrapper.SetRepository<Order>()
.TableNoTracking
.Where(o => o.Id == request.OrderId)
.Select(OrderMapper.ProjectToSDto)
.FirstOrDefaultAsync(cancellationToken);
if (orderSDto == null)
if (order == null)
throw new AppException("Order not found", ApiResultStatusCode.NotFound);
var order = orderSDto.AdaptToOrder();
var response = new SubmitOrderPaymentResponseDto();
if (request.PaymentMethod == OrderPaymentMethod.OnlinePayment)
@ -41,6 +38,13 @@ public class SubmitOrderPaymentCommandHandler : IRequestHandler<SubmitOrderPayme
else
{
response.NeedToPayOnline = true;
var orderSDto = await _repositoryWrapper.SetRepository<Order>()
.TableNoTracking
.Where(o => o.Id == request.OrderId)
.Select(OrderMapper.ProjectToSDto)
.FirstOrDefaultAsync(cancellationToken);
if (orderSDto == null)
throw new AppException("Order not found", ApiResultStatusCode.NotFound);
response.PaymentUrl = await _paymentService.GetPaymentLinkAsync(orderSDto.TotalPrice, orderSDto.FactorCode, orderSDto.Id, orderSDto.UserId, orderSDto.UserPhoneNumber, orderSDto.UserFullName, cancellationToken);
}
}

View File

@ -0,0 +1,13 @@
namespace NetinaShop.Core.Models;
public static class SiteMapUIds
{
public const string Categories = "5709ACC29A4D42E5B6F2DFFAD2FB0018";
public const string Blogs = "4C2F0C2A7A3E41268702D12FDDDB837F";
public const string Products = "E95AB3C0C4DD44FA82D77D55BD91696F";
public static List<string> AllSiteMapsUIds => new List<string>
{
Categories,
Blogs, Products
};
}

View File

@ -5,6 +5,7 @@ public class SiteSettings
public JwtSettings JwtSettings { get; set; } = new JwtSettings();
public string BaseUrl { get; set; } = string.Empty;
public string AdminPanelBaseUrl { get; set; } = string.Empty;
public string StorageBaseUrl { get; set; } = string.Empty;
public RedisSettings MasterRedisConfiguration { get; set; } = new RedisSettings();
public UserSetting UserSetting { get; set; } = new UserSetting();
public string KaveNegarApiKey { get; set; } = string.Empty;

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@ -14,9 +14,9 @@
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.3" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>
</ItemGroup>-->
<!--<PropertyGroup>
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<LangVersion>10</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
@ -32,7 +32,7 @@
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.0" />
<PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
</ItemGroup>-->
</ItemGroup>
<ItemGroup>

View File

@ -6,7 +6,7 @@ public interface IZarinpalRestApi
{
[Post("/v4/payment/request.json")]
Task<ZarinaplPaymentLinkResponse> GetPaymentLinkAsync([Body] ZarinaplPaymentLinkRequest request);
Task<string> GetPaymentLinkAsync([Body] ZarinaplPaymentLinkRequest request);
[Post("/v4/payment/verify.json")]
Task<ZarinaplPaymentVerifyResponse> VerifyPaymentAsync([Body] ZarinaplVerifyPaymentRequest request);
}

View File

@ -15,16 +15,21 @@ public class StorageService : IStorageService
return _s3Client;
}
public async Task<string> UploadObjectFromFileAsync(string fileName, string filePath, string contentType, byte[] fileBytes)
public async Task<string> UploadObjectFromFileAsync(string fileName, string filePath, string contentType, byte[] fileBytes, bool fixName = true)
{
using var memorySteam = new MemoryStream(fileBytes);
var client = GetClientAsync();
var fileEx = fileName.Split('.').Last();
fileName = fileName.Split('.').First();
if (fixName)
{
var fileEx = fileName.Split('.').Last();
fileName = fileName.Split('.').First();
fileName = fileName + "_" + StringExtensions.GetId(5) + "_" + DateTime.Today.ToString("yyyy-MM-dd-HH-mm-ss") + "." + fileEx;
fileName = fileName + "_" + StringExtensions.GetId(5) + "_" + DateTime.Today.ToString("yyyy-MM-dd-HH-mm-ss") + "." + fileEx;
}
var putRequest = new PutObjectRequest
{
@ -32,7 +37,7 @@ public class StorageService : IStorageService
Key = Path.Combine(filePath, fileName),
ContentType = contentType,
InputStream = memorySteam,
CannedACL = S3CannedACL.PublicRead
CannedACL = S3CannedACL.PublicRead,
};
putRequest.Metadata.Add("x-amz-meta-title", fileName);

View File

@ -30,4 +30,17 @@ public class UploadFileService : IUploadFileService
};
return response;
}
public async Task<FileUploadResponse> UploadFileByteAsync(FileUploadRequest uploadRequest)
{
var medFileName = await _storageService.UploadObjectFromFileAsync(uploadRequest.FileName, $"{uploadRequest.FileUploadType.ToDisplay()}", uploadRequest.ContentType, uploadRequest.FileBytes,false);
var response = new FileUploadResponse
{
FileName = medFileName,
FileLocation = $"{uploadRequest.FileUploadType.ToDisplay()}/{medFileName}",
FileUrl = $"https://storage.vesmook.com/{uploadRequest.FileUploadType.ToDisplay()}/{medFileName}"
};
return response;
}
}

View File

@ -6,6 +6,7 @@ using NetinaShop.Domain.CommandQueries.Queries;
using NetinaShop.Domain.Enums;
using NetinaShop.Domain.MartenEntities.Settings;
using NetinaShop.Infrastructure.Models.RestApi.Zarinpal;
using Newtonsoft.Json;
namespace NetinaShop.Infrastructure.Services;
@ -38,7 +39,8 @@ public class ZarinpalService : IPaymentService
merchant_id = paymentSetting.ZarinPalApiKey,
metadata = new ZarinaplPaymentLinkRequestMetadata { mobile = phoneNumber }
};
var response = await _restApiWrapper.ZarinpalRestApi.GetPaymentLinkAsync(request);
var responseJson = await _restApiWrapper.ZarinpalRestApi.GetPaymentLinkAsync(request);
var response = JsonConvert.DeserializeObject<ZarinaplPaymentLinkResponse>(responseJson);
if (response.data.code != 100)
throw new AppException($"Exception in get link from zarinpal | {response.data.message}");
@ -67,15 +69,14 @@ public class ZarinpalService : IPaymentService
merchant_id = paymentSetting.ZarinPalApiKey
};
var response = await _restApiWrapper.ZarinpalRestApi.VerifyPaymentAsync(request);
if (response.data.code != 100)
throw new AppException($"Exception in get link from zarinpal | {response.data.message}");
//if (response.data.code != 100)
// throw new AppException($"Exception in get link from zarinpal | {response.data.message}");
payment.Status = PaymentStatus.Paid;
payment.CardPan = response.data.card_pan;
payment.TransactionCode = response.data.ref_id.ToString();
await _mediator.Send(
new CreateOrUpdatePaymentCommand(payment.Id, payment.FactorNumber, payment.Amount, payment.Description,
await _mediator.Send(new CreateOrUpdatePaymentCommand(payment.Id, payment.FactorNumber, payment.Amount, payment.Description,
payment.TransactionCode, payment.CardPan, payment.Authority, payment.Type, payment.Status,
payment.OrderId, payment.UserId), cancellationToken);