add hangfire for sending order to sap

This commit is contained in:
dibakor 2026-07-20 18:30:13 +06:00
parent 23f07386ba
commit 4f89ed32a7
15 changed files with 356 additions and 36 deletions

View File

@ -754,4 +754,11 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models
[Description("Sent To SAP")]
SentToSAP = 7
}
public enum SAPOrderStatusEnum : short
{
Pending = 0,
Sent = 1,
Error = 2
}
}

View File

@ -130,19 +130,20 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Global
/// <summary>
/// Database connection Hangfire
/// </summary>
private string _hangfireDb;
public string HangfireDb
{
get
{
if (string.IsNullOrEmpty(_hangfireDb) || string.IsNullOrEmpty(PwdSecretKey))
return string.Empty;
public string HangfireDb { get; set; }
//private string _hangfireDb;
//public string HangfireDb
//{
// get
// {
// if (string.IsNullOrEmpty(_hangfireDb) || string.IsNullOrEmpty(PwdSecretKey))
// return string.Empty;
string secretKey = GlobalFunctions.ConvertFromBase64String(PwdSecretKey);
return Ease.NetCore.Utility.Global.CipherFunctions.DecryptByAES(privateKey: secretKey, publicKey: secretKey, data: _hangfireDb, input: 2);
}
set { _hangfireDb = value; }
}
// string secretKey = GlobalFunctions.ConvertFromBase64String(PwdSecretKey);
// return Ease.NetCore.Utility.Global.CipherFunctions.DecryptByAES(privateKey: secretKey, publicKey: secretKey, data: _hangfireDb, input: 2);
// }
// set { _hangfireDb = value; }
//}

View File

@ -78,3 +78,13 @@ public class GetOrderSummaryRequest : PagedRequest
public DateTime? EndDate { get; set; }
public OrderStatusEnum? OrderStatus { get; set; }
}
public class UpdateSapDetailsRequest
{
public int OrderId { get; set; }
public SAPOrderStatusEnum Status { get; set; }
public DateTime SentAt { get; set; }
public int RetryCount { get; set; }
public string LastError { get; set; }
public string ReturnMessage { get; set; }
}

View File

@ -120,3 +120,30 @@ public sealed class FreeGoodsRuleDto
public string UnitOfMeasure { get; set; } = string.Empty;
public string FocUnitOfMeasure { get; set; } = string.Empty;
}
public class GetOrderForSAPResponse
{
public GetOrderForSAPResponse()
{
to_Item = new List<GetOrderItemForSAPResponse>();
}
public string SalesOrderType { get; set; }
public string SalesOrganization { get; set; }
public string DistributionChannel { get; set; }
public string OrganizationDivision { get; set; }
public string SoldToParty { get; set; }
public string PurchaseOrderByCustomer { get; set; }
public string CustomerPurchaseOrderDate { get; set; }
public string PurchaseOrderByShipToParty { get; set; }
public string CustomerPaymentTerms { get; set; }
public string? CustomerGroup { get; set; }
public string? CustomerPriceGroup { get; set; }
public IList<GetOrderItemForSAPResponse> to_Item { get; set; }
}
public class GetOrderItemForSAPResponse
{
public string Material { get; set; }
public string RequestedQuantity { get; set; }
}

View File

@ -0,0 +1,13 @@

using Hangfire;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Contracts.BackgroupJobs;
public interface IBackgroundJobService
{
[Queue("sap_order_send")]
[DisableConcurrentExecution(timeoutInSeconds: 40 * 60)]
Task ExecuteSendingOrderAsync(CancellationToken cancellationToken);
}

View File

@ -1,5 +1,6 @@

using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@ -14,5 +15,7 @@ public interface IIntegrationHttpService
Task<IList<IntegrationMaterialStockHttpResponse>> GetMaterialStockAsync(string plantCode,
CancellationToken cancellationToken = default);
Task<string> SendSAPOrdersAsync(GetOrderForSAPResponse request,
CancellationToken cancellationToken = default);
}

View File

@ -1,8 +1,9 @@

using OnlineSalesAutoCrop.CoreAPI.Models.Requests;
using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Contracts.MobileApp;
@ -11,4 +12,8 @@ public interface IOrderService
{
Task<SaveOrderResponse> SaveOrderAsync(SaveOrderRequest request, int userId);
Task<PagedResult<GetSalesOrderResponse>> GetSalesOrdersSummary(GetOrderSummaryRequest request, int userId);
Task<IList<int>> GetAvailableOrderIdsForSAPAsync(SAPOrderStatusEnum orderStaus, CancellationToken cancellationToken = default);
Task<GetOrderForSAPResponse?> GetOrderDetailsForSapByIdAsync(int orderId, CancellationToken cancellationToken = default);
Task<bool> UpdateSendingSapDetailsAsync(UpdateSapDetailsRequest request, CancellationToken cancellationToken = default);
}

View File

@ -96,6 +96,7 @@
<ItemGroup>
<PackageReference Include="Ease.NetCore" Version="2.1.4" />
<PackageReference Include="Hangfire.Core" Version="1.8.24" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.8" />
<PackageReference Include="System.Data.SqlClient" Version="4.9.1" />

View File

@ -0,0 +1,55 @@

using Microsoft.Extensions.Logging;
using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Requests.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.BackgroupJobs;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.MobileApp;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.BackgroupJobs;
public class BackgroundJobService : IBackgroundJobService
{
private readonly IIntegrationHttpService _httpService;
private readonly IOrderService _orderService;
private readonly ILogger<BackgroundJobService> _logger;
public BackgroundJobService(IIntegrationHttpService httpService, IOrderService orderService, ILogger<BackgroundJobService> logger)
{
_httpService = httpService;
_orderService = orderService;
_logger = logger;
}
public async Task ExecuteSendingOrderAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("SendingOrder Job successfully run at: " + DateTime.Now.ToString());
try
{
var pendingOrderIds = await _orderService.GetAvailableOrderIdsForSAPAsync(SAPOrderStatusEnum.Pending);
foreach (var item in pendingOrderIds)
{
var orderDetails = await _orderService.GetOrderDetailsForSapByIdAsync(item);
string httpResponse = await _httpService.SendSAPOrdersAsync(orderDetails);
await _orderService.UpdateSendingSapDetailsAsync(new UpdateSapDetailsRequest
{
OrderId = item,
SentAt = DateTime.Now,
RetryCount =1,
ReturnMessage = httpResponse
});
}
}
catch(Exception ex)
{
_logger.LogError("Exception Occur in ExecuteSendingOrder "+ ex.Message);
throw;
}
_logger.LogInformation("SendingOrder Job successfully finished at: " + DateTime.Now.ToString());
}
}

View File

@ -1,6 +1,7 @@

using Microsoft.Extensions.Logging;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using System;
using System.Collections.Generic;
@ -71,4 +72,30 @@ public class IntegrationHttpService : IIntegrationHttpService
throw;
}
}
public async Task<string> SendSAPOrdersAsync(GetOrderForSAPResponse request, CancellationToken cancellationToken = default)
{
try
{
var requestUri = "http/sales_order";
var response = await _httpClient.PostAsJsonAsync(requestUri,request,cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Save Orders call failed: {StatusCode} — {Body}", response.StatusCode, body);
response.EnsureSuccessStatusCode();
return body;
}
var payload = await response.Content.ReadFromJsonAsync<SapODataResponse<IntegrationMaterialStockHttpResponse>>(cancellationToken: cancellationToken);
return string.Empty ;
}
catch (Exception)
{
throw;
}
}
}

View File

@ -1,6 +1,5 @@
using Ease.NetCore.DataAccess;
using Ease.NetCore.DataAccess.SQL;
using Microsoft.AspNetCore.Http;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Options;
using OnlineSalesAutoCrop.CoreAPI.Models;
@ -14,7 +13,7 @@ using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.MobileApp;
@ -808,4 +807,156 @@ public class OrderService : IOrderService
return response;
}
public async Task<IList<int>> GetAvailableOrderIdsForSAPAsync(SAPOrderStatusEnum orderStaus, CancellationToken cancellationToken = default)
{
List<int> response = new List<int>();
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@OrderStatus", pType: SqlDbType.Int, pValue: (int?)orderStaus ),
];
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetAvailableOrderIdsByStatus", parameterValues: p))
{
while (dr.Read())
{
response.Add(dr.GetInt32(0));
}
dr.Close();
}
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception ex)
{
throw;
}
return response;
}
public async Task<GetOrderForSAPResponse?> GetOrderDetailsForSapByIdAsync(int orderId, CancellationToken cancellationToken = default)
{
GetOrderForSAPResponse response = null;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode);
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@OrderId", pType: SqlDbType.Int, pValue: orderId ),
];
using (IDataReader dr = await tc.ExecuteReaderSpAsync("dbo.GetOrderDetailsById", parameterValues: p))
{
while (dr.Read())
{
response = new GetOrderForSAPResponse()
{
SalesOrderType = dr.GetString(0),
SalesOrganization = dr.GetString(1),
DistributionChannel = dr.GetString(2),
OrganizationDivision = dr.GetString(3),
SoldToParty = dr.GetString(4),
PurchaseOrderByCustomer = dr.GetString(5),
CustomerPurchaseOrderDate = ConvertOrderDateIntoMilisec( dr.GetDateTime(6)),
PurchaseOrderByShipToParty =dr.IsDBNull(7) ? null : dr.GetString(7),
CustomerPaymentTerms = dr.IsDBNull(8) ? null : dr.GetString(8),
CustomerGroup = dr.GetString(9),
CustomerPriceGroup = dr.GetString(10)
};
}
if (response is not null && dr.NextResult())
{
while (dr.Read())
{
response.to_Item.Add(new GetOrderItemForSAPResponse
{
Material = dr.GetString(0),
RequestedQuantity =Convert.ToString( dr.IsDBNull(1)? 0 : dr.GetDecimal(1))
});
}
}
dr.Close();
}
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception ex)
{
throw;
}
return response;
}
private string ConvertOrderDateIntoMilisec(DateTime orderDate)
{
long milliseconds = new DateTimeOffset(orderDate).ToUnixTimeMilliseconds();
string sapDate = $"/Date({milliseconds})/";
return sapDate;
}
public async Task<bool> UpdateSendingSapDetailsAsync(UpdateSapDetailsRequest request, CancellationToken cancellationToken = default)
{
bool response = false;
try
{
using TransactionContext tc = await TransactionContext.BeginAsync(_settings.DefaultConnection.ConnectionNode, true);
try
{
SqlParameter[] p =
[
SqlHelperExtension.CreateInParam(pName: "@OrderId", pType: SqlDbType.Int, pValue: request.OrderId ),
SqlHelperExtension.CreateInParam(pName: "@Status", pType: SqlDbType.Int, pValue: request.Status ),
SqlHelperExtension.CreateInParam(pName: "@SentAt", pType: SqlDbType.DateTime, pValue: DateTime.Now ),
SqlHelperExtension.CreateInParam(pName: "@RetryCount", pType: SqlDbType.Int, pValue: request.RetryCount ),
SqlHelperExtension.CreateInParam(pName: "@LastError", pType: SqlDbType.Int, pValue: request.LastError ),
SqlHelperExtension.CreateInParam(pName: "@ReturnMessage", pType: SqlDbType.Int, pValue: request.ReturnMessage ),
];
await tc.ExecuteNonQuerySpAsync("dbo.UpdateSendingSapDetails", parameterValues: p);
tc.End();
}
catch (Exception ie)
{
tc?.HandleError();
throw DBCustomError.GenerateCustomError(ie);
}
}
catch (Exception ex)
{
throw;
}
return response;
}
}

View File

@ -1,10 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
using Hangfire;
using Microsoft.Extensions.DependencyInjection;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Auth;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.BackgroupJobs;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Setups;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Auth;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.BackgroupJobs;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Setups;
@ -33,6 +36,7 @@ namespace OnlineSalesAutoCrop.CoreAPI.Configuration.DI
services.AddTransient<IRefreshTokenService, RefreshTokenService>();
services.AddScoped<IIntegrationService, IntegrationService>();
services.AddScoped<IMobileMasterDataService, MobileMasterDataService>();
services.AddScoped<IBackgroundJobService, BackgroundJobService>();
services.AddScoped<IOrderService, OrderService>();
}
}

View File

@ -167,7 +167,8 @@
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.0.0" />
<PackageReference Include="Ease.NetCore" Version="2.1.4" />
<PackageReference Include="Google.Cloud.Speech.V1" Version="3.9.0" />
<PackageReference Include="Hangfire" Version="1.8.23" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.24" />
<PackageReference Include="Hangfire.SqlServer" Version="1.8.24" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.8" />

View File

@ -8,6 +8,7 @@ using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -22,6 +23,7 @@ using OnlineSalesAutoCrop.CoreAPI.Configurations;
using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.BackgroupJobs;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Integrations;
using OnlineSalesAutoCrop.CoreAPI.SignalRHub;
@ -30,14 +32,14 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.RateLimiting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.HttpOverrides;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
namespace OnlineSalesAutoCrop.CoreAPI
{
@ -312,6 +314,10 @@ namespace OnlineSalesAutoCrop.CoreAPI
services.AddHangfire(config =>
{
config.UseFilter(new TimeRestrictionServerFilter(startHour: 1, endHour: 7));
config.SetDataCompatibilityLevel(CompatibilityLevel.Version_180);
config.UseSimpleAssemblyNameTypeSerializer();
config.UseRecommendedSerializerSettings();
config.UseSqlServerStorage(_appSettings.HangfireDb, new SqlServerStorageOptions
{
DisableGlobalLocks = true,
@ -321,9 +327,13 @@ namespace OnlineSalesAutoCrop.CoreAPI
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5)
});
});
services.AddHangfireServer();
services.AddHangfireServer(options =>
{
options.Queues = new[] { "sap_order_send", "default" }; // merge with any queues you already have
}); ;
}
#endregion
#region SWAGGER
@ -529,6 +539,11 @@ namespace OnlineSalesAutoCrop.CoreAPI
app.UseHangfireDashboard();
RecurringJobOptions options = new() { TimeZone = TimeZoneInfo.Local, MisfireHandling = MisfireHandlingMode.Relaxed };
RecurringJob.AddOrUpdate<IBackgroundJobService>(
"send-sap-orders",
service => service.ExecuteSendingOrderAsync(CancellationToken.None),
"*/1 * * * *");
}
#endregion

View File

@ -51,7 +51,7 @@
"EmailUseDefaultCredentials": false,
"EPMaxLevel": 2,
"FileProcessFolder": "D:\\Local\\EaseBilling\\Api\\OnlineSalesAutoCrop.CoreAPI\\FileProcessFolder",
"HangfireDb": "9oXGnVl0H8yqF5I2MjHeXGNp5367ljjQjnsatcKsW/KFPlA8iwEVdaY7ZMBnU+AtKZIaKjwWoHZvDLmNn0K8Ke96pS3Sj4tD+mJejgSfJLhaB+UERm6M+xqVC30kk25TsVBQezB7AsmS8PXLfgWAboe4YmyQ3pXMrDx7WCfiuJA=",
"HangfireDb": "Data Source=210.4.65.222,3341;Initial Catalog=AutoCropDB;User ID=dmsuser;Password=dMsu@er;Encrypt=false",
"JwtAudience": "www.celdevbd.com",
"JwtCryptoKey": "Computer ease limited, dhaka, bangladesh BDComputer ease limited",
"JwtIssuer": "https://www.celdevbd.com",