implement intgreation customer ledger api when get customer details by a specific code

This commit is contained in:
dibakor 2026-07-19 13:58:28 +06:00
parent 56bbcdfa04
commit 3ae6fa0882
8 changed files with 255 additions and 16 deletions

View File

@ -267,10 +267,12 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Global
[Required] [Required]
public ApiSettings API { get; set; } public ApiSettings API { get; set; }
/// <summary> public AutoCropSAPApi AutoCropSAPApi { get; set; }
/// Swagger Setting
/// </summary> /// <summary>
[Required] /// Swagger Setting
/// </summary>
[Required]
public Swagger Swagger { get; set; } public Swagger Swagger { get; set; }
/// <summary> /// <summary>
@ -346,10 +348,20 @@ namespace OnlineSalesAutoCrop.CoreAPI.Models.Global
public string Url { get; set; } public string Url { get; set; }
} }
/// <summary> /// <summary>
/// /// AutoCrop Integration API Details
/// </summary> /// </summary>
public class ApiLicense public class AutoCropSAPApi
{
public string BaseUrl { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
/// <summary>
///
/// </summary>
public class ApiLicense
{ {
public string Name { get; set; } public string Name { get; set; }
public string Url { get; set; } public string Url { get; set; }

View File

@ -0,0 +1,91 @@

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
namespace OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
public class IntegrationCustLedgerHttpResponse
{
public string Company { get; set; }
public string Customer_no { get; set; }
public decimal Current_Balance { get; set; }
public decimal Overdue_Amount { get; set; }
}
public class IntegrationMaterialStockHttpResponse
{
public string Plant { get; set; }
public string Material { get; set; }
public decimal Available_Stock { get; set; }
public string UOM { get; set; }
}
// Reusable envelope for any SAP OData V2 collection response: { "d": { "results": [...] } }
public sealed class SapODataResponse<T>
{
[JsonPropertyName("d")]
public SapODataResult<T> D { get; set; } = default!;
}
public sealed class SapODataResult<T>
{
[JsonPropertyName("results")]
public List<T> Results { get; set; } = new();
}
public sealed class CustomerLedgerDto
{
[JsonPropertyName("company")]
public string Company { get; set; } = string.Empty;
[JsonPropertyName("customer_no")]
public string CustomerNo { get; set; } = string.Empty; // stays string — SAP customer numbers can carry leading zeros
[JsonPropertyName("current_balance")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public decimal CurrentBalance { get; set; }
[JsonPropertyName("overdue_amount")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public decimal OverdueAmount { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; } = string.Empty;
[JsonPropertyName("overdue_date")]
[JsonConverter(typeof(SapODataDateConverter))]
public DateTime OverdueDate { get; set; }
}
public sealed class SapODataDateConverter : JsonConverter<DateTime>
{
// Matches "/Date(1783641600000)/" and the rarer offset form "/Date(1783641600000+0600)/"
private static readonly Regex Pattern = new(@"/Date\((-?\d+)(?:[+-]\d{4})?\)/", RegexOptions.Compiled);
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var raw = reader.GetString();
if (string.IsNullOrEmpty(raw))
return default;
var match = Pattern.Match(raw);
if (!match.Success)
throw new JsonException($"Unexpected SAP OData date format: '{raw}'");
var ms = long.Parse(match.Groups[1].Value);
return DateTimeOffset.FromUnixTimeMilliseconds(ms).UtcDateTime;
}
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
var ms = new DateTimeOffset(value, TimeSpan.Zero).ToUnixTimeMilliseconds();
writer.WriteStringValue($"/Date({ms})/");
}
}

View File

@ -0,0 +1,17 @@

using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
public interface IIntegrationHttpService
{
Task<CustomerLedgerDto?> GetCustomerLedgerAsync(string customerNumber, string companyCode,
CancellationToken cancellationToken = default);
Task<IntegrationMaterialStockHttpResponse?> GetMaterialStockAsync(string plantCode, string materialCode,
CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,71 @@

using Microsoft.Extensions.Logging;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.Integrations;
public class IntegrationHttpService : IIntegrationHttpService
{
private readonly HttpClient _httpClient;
private readonly ILogger<IntegrationHttpService> _logger;
public IntegrationHttpService(HttpClient httpClient, ILogger<IntegrationHttpService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
public async Task<CustomerLedgerDto> GetCustomerLedgerAsync(string customerNumber, string companyCode, CancellationToken cancellationToken = default)
{
try
{
var requestUri = $"http/get_customer_ledger?customer={Uri.EscapeDataString(customerNumber)}&company={Uri.EscapeDataString(companyCode)}";
var response = await _httpClient.GetAsync(requestUri, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Customer ledger call failed: {StatusCode} — {Body}", response.StatusCode, body);
response.EnsureSuccessStatusCode();
}
var payload= await response.Content.ReadFromJsonAsync< SapODataResponse<CustomerLedgerDto>>(cancellationToken: cancellationToken);
return payload?.D.Results.FirstOrDefault();
}
catch(Exception)
{
throw;
}
}
public async Task<IntegrationMaterialStockHttpResponse> GetMaterialStockAsync(string plantCode, string materialCode, CancellationToken cancellationToken = default)
{
try
{
var requestUri = $"http/get_available_stocks?plant={Uri.EscapeDataString(plantCode)}&material={Uri.EscapeDataString(materialCode)}";
var response = await _httpClient.GetAsync(requestUri, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(cancellationToken);
_logger.LogError("Material Stock call failed: {StatusCode} — {Body}", response.StatusCode, body);
response.EnsureSuccessStatusCode();
}
return await response.Content.ReadFromJsonAsync<IntegrationMaterialStockHttpResponse>(cancellationToken: cancellationToken);
}
catch (Exception)
{
throw;
}
}
}

View File

@ -8,11 +8,13 @@ using OnlineSalesAutoCrop.CoreAPI.Models.Requests.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses; using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations; using OnlineSalesAutoCrop.CoreAPI.Models.Responses.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp; using OnlineSalesAutoCrop.CoreAPI.Models.Responses.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.MobileApp; using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.MobileApp;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems; using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.MobileApp; namespace OnlineSalesAutoCrop.CoreAPI.Services.Services.MobileApp;
@ -21,11 +23,13 @@ public class MobileMasterDataService : IMobileMasterDataService
{ {
private readonly AppSettings _settings; private readonly AppSettings _settings;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly IIntegrationHttpService _httpService;
public MobileMasterDataService(IOptions<AppSettings> options, IUserService userService) public MobileMasterDataService(IOptions<AppSettings> options, IUserService userService, IIntegrationHttpService httpService)
{ {
_settings = options.Value; _settings = options.Value;
_userService = userService; _userService = userService;
_httpService = httpService;
} }
public async Task<MarketHeirarchyByEmpResponse> GetMarketHeirarchiesByEmpAsync(GetMarketHeirarchyByEmpRequest request) public async Task<MarketHeirarchyByEmpResponse> GetMarketHeirarchiesByEmpAsync(GetMarketHeirarchyByEmpRequest request)
@ -236,6 +240,7 @@ public class MobileMasterDataService : IMobileMasterDataService
public async Task<PagedResult<GetCustomerResponse>> GetCustomersAsync(GetCustomersRequest request) public async Task<PagedResult<GetCustomerResponse>> GetCustomersAsync(GetCustomersRequest request)
{ {
string companyCode = string.Empty;
PagedResult<GetCustomerResponse> response = new(); PagedResult<GetCustomerResponse> response = new();
try try
{ {
@ -284,6 +289,7 @@ public class MobileMasterDataService : IMobileMasterDataService
CreditBalance = dr.GetDecimal(23), CreditBalance = dr.GetDecimal(23),
CreditOverdue = dr.GetDecimal(24), CreditOverdue = dr.GetDecimal(24),
}); });
companyCode = dr.GetString(25);
} }
dr.Close(); dr.Close();
@ -292,6 +298,18 @@ public class MobileMasterDataService : IMobileMasterDataService
response.PageSize = request.PageSize; response.PageSize = request.PageSize;
} }
tc.End(); tc.End();
if (!string.IsNullOrWhiteSpace(request.CustomerCode))
{
var customerLedger = await _httpService.GetCustomerLedgerAsync(request.CustomerCode, companyCode);
response.Items = response.Items.Select(x =>
{
x.CreditBalance = customerLedger.CurrentBalance;
x.CreditOverdue = customerLedger.OverdueAmount;
return x;
}).ToList();
}
} }
catch (Exception ie) catch (Exception ie)
{ {

View File

@ -171,6 +171,8 @@
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.8" /> <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.SqlServer" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.8" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.8.0" />
<PackageReference Include="OpenAI" Version="2.10.0" /> <PackageReference Include="OpenAI" Version="2.10.0" />
<PackageReference Include="RabbitMQ.Client" Version="7.2.1" /> <PackageReference Include="RabbitMQ.Client" Version="7.2.1" />
<PackageReference Include="ReportViewerCore.NETCore" Version="15.1.33" /> <PackageReference Include="ReportViewerCore.NETCore" Version="15.1.33" />

View File

@ -8,7 +8,6 @@ using Microsoft.AspNetCore.CookiePolicy;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -23,18 +22,22 @@ using OnlineSalesAutoCrop.CoreAPI.Configurations;
using OnlineSalesAutoCrop.CoreAPI.Models; using OnlineSalesAutoCrop.CoreAPI.Models;
using OnlineSalesAutoCrop.CoreAPI.Models.Global; using OnlineSalesAutoCrop.CoreAPI.Models.Global;
using OnlineSalesAutoCrop.CoreAPI.Models.Responses; using OnlineSalesAutoCrop.CoreAPI.Models.Responses;
using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Systems; using OnlineSalesAutoCrop.CoreAPI.Services.Contracts.Integrations;
using OnlineSalesAutoCrop.CoreAPI.Services.Services.Integrations;
using OnlineSalesAutoCrop.CoreAPI.SignalRHub; using OnlineSalesAutoCrop.CoreAPI.SignalRHub;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.HttpOverrides;
using System.Net.Http.Headers;
using System.Net.Http;
using System.Net;
namespace OnlineSalesAutoCrop.CoreAPI namespace OnlineSalesAutoCrop.CoreAPI
{ {
@ -341,9 +344,29 @@ namespace OnlineSalesAutoCrop.CoreAPI
services.ConfigureBusinessServices(); services.ConfigureBusinessServices();
#endregion #endregion
}
}
#region AutoCrop Integration Service
services.AddHttpClient<IIntegrationHttpService, IntegrationHttpService>((sp, client) =>
{
var opt = _appSettings.AutoCropSAPApi;
client.BaseAddress = new Uri(opt.BaseUrl);
var basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{opt.UserName}:{opt.Password}"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
})
.AddStandardResilienceHandler(); // retry + timeout + circuit breaker, bundled
#endregion
}
}
catch catch
{ {
//Nothimg to do here as we are handling all exceptions in Configure method using global exception handler //Nothimg to do here as we are handling all exceptions in Configure method using global exception handler

View File

@ -85,7 +85,12 @@
"WaMsgSvcSid": "MG8401d33a9a3b2aea95619bda3e5757b5", "WaMsgSvcSid": "MG8401d33a9a3b2aea95619bda3e5757b5",
"WaSenderId": "+8801326755660", "WaSenderId": "+8801326755660",
"RefreshTokenDuration": "15", "RefreshTokenDuration": "15",
"AccessTokenDuration": "60" "AccessTokenDuration": "60",
"AutoCropSAPApi": {
"BaseUrl": "https://accl-test-pad-l06vsvh7.it-cpi004-rt.cfapps.ap11.hana.ondemand.com/",
"UserName": "sb-1635ae0e-9941-4998-962f-e23f592d29b5!b45657|it-rt-accl-test-pad-l06vsvh7!b68",
"Password": "01b3f9b9-8dee-4aed-9cb9-bbefeffc1db8$eB6J_oUJZDGuJnpzAzLUxI9lAha_hljim2cfH1niJa8="
}
}, },
"MenuSettings": { "MenuSettings": {