From d266463c9fc5b9597feabfbc084e465f79287161 Mon Sep 17 00:00:00 2001
From: Thanakarn Klangkasame <77600906+Simulationable@users.noreply.github.com>
Date: Fri, 3 Oct 2025 10:33:55 +0700
Subject: [PATCH] Fix Redundance Reference
---
.../AMREZ.EOP.Abstractions.csproj | 3 +
.../IVerifyFromImageUseCase.cs | 2 +-
.../SlipVerification/QrDecode/IQrDecoder.cs | 2 +-
.../SCB/Clients/IScbSlipClient.cs | 4 +-
.../AMREZ.EOP.Application.csproj | 6 ++
.../BankDetect/ParserBankDetect.cs | 4 +-
.../QrDecode/TransRefParser.cs | 85 ++++++++++++++++-
.../QrDecode/ZxingQrDecoder.cs | 20 +++-
.../VerifyFromImageUseCase.cs | 91 ++++++++++++++++++-
.../DTOs/Integrations/SCB/Common/SCBStatus.cs | 5 +-
.../Integrations/SCB/OAuth/OAuthRequest.cs | 11 ++-
.../Integrations/SCB/OAuth/OAuthResponse.cs | 13 ++-
.../SlipVerificationEnvelope.cs | 39 +++++++-
.../VerifyFromImageRequest.cs | 11 ++-
.../VerifyFromImageResponse.cs | 10 +-
.../Shared/Payments/SCBOptions.cs | 9 +-
.../AMREZ.EOP.Infrastructures.csproj | 2 +
.../ServiceCollectionExtensions.cs | 25 ++++-
.../Integrations/SCB/Clients/ScbSlipClient.cs | 81 ++++++++++++++++-
qodana.yaml | 46 ++++++++++
20 files changed, 440 insertions(+), 29 deletions(-)
create mode 100644 qodana.yaml
diff --git a/AMREZ.EOP.Abstractions/AMREZ.EOP.Abstractions.csproj b/AMREZ.EOP.Abstractions/AMREZ.EOP.Abstractions.csproj
index f1b38eb..0d66818 100644
--- a/AMREZ.EOP.Abstractions/AMREZ.EOP.Abstractions.csproj
+++ b/AMREZ.EOP.Abstractions/AMREZ.EOP.Abstractions.csproj
@@ -16,4 +16,7 @@
+
+
+
diff --git a/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/IVerifyFromImageUseCase.cs b/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/IVerifyFromImageUseCase.cs
index 1bc21ff..4528f51 100644
--- a/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/IVerifyFromImageUseCase.cs
+++ b/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/IVerifyFromImageUseCase.cs
@@ -1,6 +1,6 @@
using AMREZ.EOP.Contracts.DTOs.Payments.SlipVerification;
-namespace AMREZ.EOP.Abstractions.Applications.UseCases.Payments;
+namespace AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification;
public interface IVerifyFromImageUseCase
{
diff --git a/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/QrDecode/IQrDecoder.cs b/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/QrDecode/IQrDecoder.cs
index 71342ef..4dcdfe9 100644
--- a/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/QrDecode/IQrDecoder.cs
+++ b/AMREZ.EOP.Abstractions/Applications/UseCases/Payments/SlipVerification/QrDecode/IQrDecoder.cs
@@ -2,5 +2,5 @@ namespace AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification
public interface IQrDecoder
{
-
+ string? TryDecodeText(byte[] imageBytes);
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Abstractions/Infrastructures/Integrations/SCB/Clients/IScbSlipClient.cs b/AMREZ.EOP.Abstractions/Infrastructures/Integrations/SCB/Clients/IScbSlipClient.cs
index 45a1693..e972152 100644
--- a/AMREZ.EOP.Abstractions/Infrastructures/Integrations/SCB/Clients/IScbSlipClient.cs
+++ b/AMREZ.EOP.Abstractions/Infrastructures/Integrations/SCB/Clients/IScbSlipClient.cs
@@ -1,6 +1,8 @@
+using AMREZ.EOP.Contracts.DTOs.Integrations.SCB.SlipVerification;
+
namespace AMREZ.EOP.Abstractions.Infrastructures.Integrations.SCB.Clients;
public interface IScbSlipClient
{
-
+ Task VerifyAsync(string transRef, string sendingBank, CancellationToken ct);
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Application/AMREZ.EOP.Application.csproj b/AMREZ.EOP.Application/AMREZ.EOP.Application.csproj
index b1fd7ad..eef6ff9 100644
--- a/AMREZ.EOP.Application/AMREZ.EOP.Application.csproj
+++ b/AMREZ.EOP.Application/AMREZ.EOP.Application.csproj
@@ -12,6 +12,12 @@
+
+
+
+
+
+
diff --git a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/BankDetect/ParserBankDetect.cs b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/BankDetect/ParserBankDetect.cs
index c26ba18..fe8a542 100644
--- a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/BankDetect/ParserBankDetect.cs
+++ b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/BankDetect/ParserBankDetect.cs
@@ -1,12 +1,10 @@
-using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification.BankDetect;
using AMREZ.EOP.Application.UseCases.Payments.SlipVerification.QrDecode;
using SkiaSharp;
namespace AMREZ.EOP.Application.UseCases.Payments.SlipVerification.BankDetect;
-public static class BankDetect
+public static class ParserBankDetect
{
- // SCB=014, KBank=004
public static string? FromQrText(string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return null;
diff --git a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/TransRefParser.cs b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/TransRefParser.cs
index a1342f4..82e6394 100644
--- a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/TransRefParser.cs
+++ b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/TransRefParser.cs
@@ -1,6 +1,87 @@
+using System.Text.RegularExpressions;
+
namespace AMREZ.EOP.Application.UseCases.Payments.SlipVerification.QrDecode;
-public class TransRefParser
+public static partial class TransRefParser
{
-
+ [GeneratedRegex(@"(?i)(?:[?&](?:transref|transactionid|txnid|transid)=)(?[-A-Za-z0-9]{10,64})",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase)]
+ private static partial Regex QueryId();
+
+ [GeneratedRegex(@"(?[0-9]{6,14}[A-Za-z0-9]{6,40})(?![A-Za-z0-9])",
+ RegexOptions.Compiled)]
+ private static partial Regex FallbackShape();
+
+ public static string? TryExtract(string? raw)
+ {
+ if (string.IsNullOrWhiteSpace(raw)) return null;
+
+ // 1) ?transRef=... ใน URL
+ var q = QueryId().Match(raw);
+ if (q.Success) return q.Groups["id"].Value;
+
+ // 2) เดิน TLV แบบ recursive
+ foreach (var val in EnumerateTlvValues(raw))
+ {
+ // ต้องเป็น a-z0-9 ล้วน ยาวพอสมควร และมีตัวอักษรด้วย (กันเคสเป็นตัวเลขล้วน)
+ if (val.Length is >= 18 and <= 40
+ && val.All(IsAlphaNum)
+ && val.Any(char.IsLetter))
+ return val;
+ }
+
+ // 3) เผื่อกรณีหลุดรูปแบบ TLV
+ var f = FallbackShape().Match(raw);
+ return f.Success ? f.Groups["id"].Value : null;
+ }
+
+ private static IEnumerable EnumerateTlvValues(string s)
+ {
+ int i = 0;
+ while (i + 4 <= s.Length)
+ {
+ // รูปแบบ TLV: [ID(2)][LEN(2)][VALUE(LEN)]
+ if (!char.IsDigit(s[i]) || !char.IsDigit(s[i + 1]) ||
+ !char.IsDigit(s[i + 2]) || !char.IsDigit(s[i + 3]))
+ yield break;
+
+ var id = s.Substring(i, 2);
+ if (!int.TryParse(s.AsSpan(i + 2, 2), out var len)) yield break;
+ var end = i + 4 + len;
+ if (len < 0 || end > s.Length) yield break;
+
+ var val = s.Substring(i + 4, len);
+ yield return val;
+
+ // ถ้า value ดูเป็น TLV ซ้อนอยู่ ให้ไล่ต่อ
+ if (LooksLikeTlv(val))
+ foreach (var inner in EnumerateTlvValues(val))
+ yield return inner;
+
+ // CRC tag "63" ไม่เกี่ยว แต่เราไม่ถึงเงื่อนไข length อยู่แล้ว
+ i = end;
+ }
+ }
+
+ private static bool LooksLikeTlv(string s)
+ {
+ int i = 0;
+ while (i + 4 <= s.Length)
+ {
+ if (!char.IsDigit(s[i]) || !char.IsDigit(s[i + 1]) ||
+ !char.IsDigit(s[i + 2]) || !char.IsDigit(s[i + 3]))
+ return false;
+
+ if (!int.TryParse(s.AsSpan(i + 2, 2), out var len)) return false;
+ i += 4 + len;
+ if (i == s.Length) return true;
+ if (i > s.Length) return false;
+ }
+ return false;
+ }
+
+ private static bool IsAlphaNum(char c) =>
+ (c >= '0' && c <= '9') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= 'a' && c <= 'z');
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/ZxingQrDecoder.cs b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/ZxingQrDecoder.cs
index ce0bd3c..95236ca 100644
--- a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/ZxingQrDecoder.cs
+++ b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/QrDecode/ZxingQrDecoder.cs
@@ -1,6 +1,22 @@
+using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification.QrDecode;
+using SkiaSharp;
+using ZXing.SkiaSharp;
+
namespace AMREZ.EOP.Application.UseCases.Payments.SlipVerification.QrDecode;
-public class ZxingQrDecoder
+public sealed class ZxingQrDecoder : IQrDecoder
{
-
+ public string? TryDecodeText(byte[] imageBytes)
+ {
+ using var bmp = SKBitmap.Decode(imageBytes);
+ if (bmp is null) return null;
+
+ var reader = new BarcodeReader
+ {
+ AutoRotate = true,
+ Options = { TryHarder = true, PossibleFormats = new[] { ZXing.BarcodeFormat.QR_CODE } }
+ };
+ var res = reader.Decode(bmp);
+ return res?.Text;
+ }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/VerifyFromImageUseCase.cs b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/VerifyFromImageUseCase.cs
index 510508e..79b3001 100644
--- a/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/VerifyFromImageUseCase.cs
+++ b/AMREZ.EOP.Application/UseCases/Payments/SlipVerification/VerifyFromImageUseCase.cs
@@ -1,6 +1,93 @@
+using System.Globalization;
+using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification;
+using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification.QrDecode;
+using AMREZ.EOP.Abstractions.Infrastructures.Integrations.SCB.Clients;
+using AMREZ.EOP.Application.UseCases.Payments.SlipVerification.BankDetect;
+using AMREZ.EOP.Application.UseCases.Payments.SlipVerification.QrDecode;
+using AMREZ.EOP.Contracts.DTOs.Payments.SlipVerification;
+using AMREZ.EOP.Domain.Shared.Payments;
+
namespace AMREZ.EOP.Application.UseCases.Payments.SlipVerification;
-public class VerifyFromImageUseCase
+public sealed class VerifyFromImageUseCase : IVerifyFromImageUseCase
{
-
+ private readonly IQrDecoder _qr;
+ private readonly IScbSlipClient _scb;
+ private readonly SCBOptions _opts;
+
+ public VerifyFromImageUseCase(IQrDecoder qr, IScbSlipClient scb, SCBOptions opts)
+ {
+ _qr = qr;
+ _scb = scb;
+ _opts = opts;
+ }
+
+ public async Task ExecuteAsync(VerifyFromImageRequest request, CancellationToken ct)
+ {
+ await using var ms = new MemoryStream();
+ await request.Image.CopyToAsync(ms, ct);
+
+ var qrText = _qr.TryDecodeText(ms.ToArray());
+ var transId = TransRefParser.TryExtract(qrText);
+ var bank = ParserBankDetect.FromQrText(qrText) ?? _opts.DefaultSendingBank;
+
+ if (string.IsNullOrWhiteSpace(transId)) return null; // ไม่มี id ก็หยุด
+
+ // ← SCB API ต้องการ “id” (transRef/txnId) + sendingBank เท่านั้น
+ var env = await _scb.VerifyAsync(transId, bank, ct);
+ if (env?.Status?.Code != 1000 || env.Data is null) return null;
+
+ // 3) map → controller response
+ var d = env.Data;
+ var amount = decimal.TryParse(d.PaidLocalAmount ?? d.Amount ?? "0", NumberStyles.Any,
+ CultureInfo.InvariantCulture, out var v)
+ ? v
+ : 0m;
+ var when = CombineBangkok(d.TransDate, d.TransTime);
+
+ var msisdn = d.Sender?.Proxy?.Type == "MSISDN" ? d.Sender.Proxy.Value : null;
+ var biller = d.Receiver?.Proxy?.Type == "BILLERID" ? d.Receiver.Proxy.Value : null;
+
+ return new VerifyFromImageResponse
+ {
+ TransRef = d.TransRef ?? transId,
+ SendingBank = bank,
+ AmountTHB = amount,
+ CurrencyNumeric = d.PaidLocalCurrency ?? "764",
+ SenderMsisdn = msisdn,
+ BillerId = biller,
+ TransactedAt = when
+ };
+ }
+
+ private static DateTimeOffset CombineBangkok(string? date, string? time)
+ {
+ if (string.IsNullOrWhiteSpace(date) || string.IsNullOrWhiteSpace(time))
+ return DateTimeOffset.UtcNow; // fallback
+
+ var dt = DateTime.ParseExact($"{date} {time}", "yyyyMMdd HH:mm:ss", CultureInfo.InvariantCulture);
+ var local = DateTime.SpecifyKind(dt, DateTimeKind.Unspecified);
+
+ TimeZoneInfo? tz = null;
+ try
+ {
+ tz = TimeZoneInfo.FindSystemTimeZoneById("Asia/Bangkok");
+ }
+ catch
+ {
+ }
+
+ if (tz is null)
+ try
+ {
+ tz = TimeZoneInfo.FindSystemTimeZoneById("SE Asia Standard Time");
+ }
+ catch
+ {
+ }
+
+ return tz is null
+ ? new DateTimeOffset(local, TimeSpan.FromHours(7))
+ : new DateTimeOffset(local, tz.GetUtcOffset(local));
+ }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/Common/SCBStatus.cs b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/Common/SCBStatus.cs
index 4ecfd34..254f9c7 100644
--- a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/Common/SCBStatus.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/Common/SCBStatus.cs
@@ -1,6 +1,7 @@
namespace AMREZ.EOP.Contracts.DTOs.Integrations.SCB.Common;
-public class SCBStatus
+public sealed class SCBStatus
{
-
+ public int? Code { get; set; }
+ public string? Description { get; set; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthRequest.cs b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthRequest.cs
index bf6cc5a..fc1fa21 100644
--- a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthRequest.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthRequest.cs
@@ -1,6 +1,13 @@
namespace AMREZ.EOP.Contracts.DTOs.Integrations.SCB.OAuth;
-public class OAuthRequest
+public sealed class OAuthRequest
{
-
+ public OAuthRequest(string applicationKey, string applicationSecret)
+ {
+ ApplicationKey = applicationKey;
+ ApplicationSecret = applicationSecret;
+ }
+
+ public string ApplicationKey { get; }
+ public string ApplicationSecret { get; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthResponse.cs b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthResponse.cs
index 9e6b450..b212272 100644
--- a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthResponse.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/OAuth/OAuthResponse.cs
@@ -1,6 +1,15 @@
+using AMREZ.EOP.Contracts.DTOs.Integrations.SCB.Common;
+
namespace AMREZ.EOP.Contracts.DTOs.Integrations.SCB.OAuth;
-public class OAuthResponse
+public sealed class OAuthResponse
{
-
+ public SCBStatus? Status { get; set; }
+ public OAuthData? Data { get; set; }
+}
+
+public sealed class OAuthData
+{
+ public string? AccessToken { get; set; }
+ public int? ExpiresIn { get; set; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/SlipVerification/SlipVerificationEnvelope.cs b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/SlipVerification/SlipVerificationEnvelope.cs
index 7d11026..9257b84 100644
--- a/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/SlipVerification/SlipVerificationEnvelope.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Integrations/SCB/SlipVerification/SlipVerificationEnvelope.cs
@@ -1,6 +1,41 @@
+using AMREZ.EOP.Contracts.DTOs.Integrations.SCB.Common;
+
namespace AMREZ.EOP.Contracts.DTOs.Integrations.SCB.SlipVerification;
-public class SlipVerificationEnvelope
+public sealed class SlipVerificationEnvelope
{
-
+ public SCBStatus? Status { get; set; }
+ public SlipVerificationData? Data { get; set; }
+}
+
+public sealed class SlipVerificationData
+{
+ public string? TransRef { get; set; }
+ public string? SendingBank { get; set; }
+ public string? ReceivingBank { get; set; }
+ public string? TransDate { get; set; } // yyyyMMdd
+ public string? TransTime { get; set; } // HH:mm:ss
+ public SCBParty? Sender { get; set; }
+ public SCBParty? Receiver { get; set; }
+ public string? Amount { get; set; }
+ public string? PaidLocalAmount { get; set; }
+ public string? PaidLocalCurrency { get; set; } // "764"
+ public string? CountryCode { get; set; } // "TH"
+ public string? Ref1 { get; set; }
+ public string? Ref2 { get; set; }
+ public string? Ref3 { get; set; }
+}
+
+public sealed class SCBParty
+{
+ public string? DisplayName { get; set; }
+ public string? Name { get; set; }
+ public SCBId? Proxy { get; set; }
+ public SCBId? Account { get; set; }
+}
+
+public sealed class SCBId
+{
+ public string? Type { get; set; }
+ public string? Value { get; set; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageRequest.cs b/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageRequest.cs
index c96d5f8..5c0528c 100644
--- a/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageRequest.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageRequest.cs
@@ -1,6 +1,13 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
namespace AMREZ.EOP.Contracts.DTOs.Payments.SlipVerification;
-public class VerifyFromImageRequest
+public sealed class VerifyFromImageRequest
{
-
+ [FromForm(Name = "Image")]
+ public IFormFile? Image { get; set; } = default!;
+
+ [FromForm(Name = "OverrideSendingBank")]
+ public string? OverrideSendingBank { get; set; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageResponse.cs b/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageResponse.cs
index 139ebf9..8ce5d25 100644
--- a/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageResponse.cs
+++ b/AMREZ.EOP.Contracts/DTOs/Payments/SlipVerification/VerifyFromImageResponse.cs
@@ -1,6 +1,12 @@
namespace AMREZ.EOP.Contracts.DTOs.Payments.SlipVerification;
-public class VerifyFromImageResponse
+public sealed class VerifyFromImageResponse
{
-
+ public required string TransRef { get; init; }
+ public required string SendingBank { get; init; }
+ public decimal AmountTHB { get; init; }
+ public string CurrencyNumeric { get; init; } = "764";
+ public string? SenderMsisdn { get; init; }
+ public string? BillerId { get; init; }
+ public DateTimeOffset TransactedAt { get; init; }
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Domain/Shared/Payments/SCBOptions.cs b/AMREZ.EOP.Domain/Shared/Payments/SCBOptions.cs
index 3c97a4e..3037e95 100644
--- a/AMREZ.EOP.Domain/Shared/Payments/SCBOptions.cs
+++ b/AMREZ.EOP.Domain/Shared/Payments/SCBOptions.cs
@@ -1,6 +1,11 @@
namespace AMREZ.EOP.Domain.Shared.Payments;
-public class SCBOptions
+public sealed class SCBOptions
{
-
+ public string BaseUrl { get; init; } = "https://api.partners.scb/partners";
+ public string SCBResourceOwnerId { get; init; } = "l7eb41a014586448c6be6cfaecce1f5b5e";
+ public string SCBApplicationKey { get; init; } = "l7eb41a014586448c6be6cfaecce1f5b5e";
+ public string SCBApplicationSecret { get; init; } = "5f20b35809034d16b7930af95ab6a7de";
+ public string DefaultLanguage { get; init; } = "EN";
+ public string DefaultSendingBank { get; init; } = "014";
}
\ No newline at end of file
diff --git a/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj b/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj
index 00b400c..38b0f06 100644
--- a/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj
+++ b/AMREZ.EOP.Infrastructures/AMREZ.EOP.Infrastructures.csproj
@@ -12,9 +12,11 @@
+
+
diff --git a/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs b/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs
index 623779e..86d9068 100644
--- a/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs
+++ b/AMREZ.EOP.Infrastructures/DependencyInjections/ServiceCollectionExtensions.cs
@@ -1,16 +1,24 @@
using AMREZ.EOP.Abstractions.Applications.Tenancy;
using AMREZ.EOP.Abstractions.Applications.UseCases.Authentications;
using AMREZ.EOP.Abstractions.Applications.UseCases.HumanResources;
+using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification;
+using AMREZ.EOP.Abstractions.Applications.UseCases.Payments.SlipVerification.QrDecode;
using AMREZ.EOP.Abstractions.Applications.UseCases.Tenancy;
using AMREZ.EOP.Abstractions.Infrastructures.Common;
+using AMREZ.EOP.Abstractions.Infrastructures.Integrations.SCB.Clients;
using AMREZ.EOP.Abstractions.Infrastructures.Repositories;
using AMREZ.EOP.Abstractions.Security;
using AMREZ.EOP.Abstractions.Storage;
using AMREZ.EOP.Application.UseCases.Authentications;
using AMREZ.EOP.Application.UseCases.HumanResources;
+using AMREZ.EOP.Application.UseCases.Payments.SlipVerification;
+using AMREZ.EOP.Application.UseCases.Payments.SlipVerification.BankDetect;
+using AMREZ.EOP.Application.UseCases.Payments.SlipVerification.QrDecode;
using AMREZ.EOP.Application.UseCases.Tenancy;
+using AMREZ.EOP.Domain.Shared.Payments;
using AMREZ.EOP.Domain.Shared.Tenancy;
using AMREZ.EOP.Infrastructures.Data;
+using AMREZ.EOP.Infrastructures.Integrations.SCB.Clients;
using AMREZ.EOP.Infrastructures.Options;
using AMREZ.EOP.Infrastructures.Repositories;
using AMREZ.EOP.Infrastructures.Security;
@@ -30,7 +38,17 @@ public static class ServiceCollectionExtensions
{
services.AddHttpContextAccessor();
services.AddScoped();
-
+
+ services.AddSingleton(new SCBOptions());
+
+ services.AddHttpClient((sp, http) =>
+ {
+ var o = sp.GetRequiredService();
+ var baseUrl = o.BaseUrl.EndsWith('/') ? o.BaseUrl[..^1] : o.BaseUrl;
+ http.BaseAddress = new Uri(baseUrl + "/v1/");
+ http.Timeout = TimeSpan.FromSeconds(20);
+ });
+
// Options
services.AddOptions()
.BindConfiguration("Connections")
@@ -116,6 +134,11 @@ public static class ServiceCollectionExtensions
services.AddScoped();
services.AddScoped();
+
+ // UseCase & helpers (no OCR)
+ services.AddScoped();
+ services.AddScoped();
+
// Redis (optional)
services.AddSingleton(sp =>
{
diff --git a/AMREZ.EOP.Infrastructures/Integrations/SCB/Clients/ScbSlipClient.cs b/AMREZ.EOP.Infrastructures/Integrations/SCB/Clients/ScbSlipClient.cs
index 0af769a..69dc94a 100644
--- a/AMREZ.EOP.Infrastructures/Integrations/SCB/Clients/ScbSlipClient.cs
+++ b/AMREZ.EOP.Infrastructures/Integrations/SCB/Clients/ScbSlipClient.cs
@@ -1,6 +1,83 @@
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using AMREZ.EOP.Abstractions.Infrastructures.Integrations.SCB.Clients;
+using AMREZ.EOP.Contracts.DTOs.Integrations.SCB.OAuth;
+using AMREZ.EOP.Contracts.DTOs.Integrations.SCB.SlipVerification;
+using AMREZ.EOP.Domain.Shared.Payments;
+
namespace AMREZ.EOP.Infrastructures.Integrations.SCB.Clients;
-public class ScbSlipClient
+public sealed class ScbSlipClient : IScbSlipClient
{
-
+ private readonly HttpClient _http;
+ private readonly SCBOptions _opts;
+ private readonly JsonSerializerOptions _json = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
+ };
+
+ private string? _token;
+ private DateTimeOffset _exp;
+ private readonly SemaphoreSlim _gate = new(1,1);
+
+ public ScbSlipClient(HttpClient http, SCBOptions opts)
+ {
+ _http = http;
+ _opts = opts;
+ }
+
+ public async Task VerifyAsync(string transRef, string sendingBank, CancellationToken ct)
+ {
+ var bearer = await GetTokenAsync(ct);
+ var path = $"payment/billpayment/transactions/{Uri.EscapeDataString(transRef)}?sendingBank={Uri.EscapeDataString(sendingBank)}";
+
+ using var req = new HttpRequestMessage(HttpMethod.Get, path);
+ AddHeaders(req.Headers, bearer);
+
+ using var res = await _http.SendAsync(req, ct);
+ var body = await res.Content.ReadAsStringAsync(ct);
+ if (!res.IsSuccessStatusCode) return null;
+
+ return JsonSerializer.Deserialize(body, _json);
+ }
+
+ private async Task GetTokenAsync(CancellationToken ct)
+ {
+ if (!string.IsNullOrEmpty(_token) && _exp > DateTimeOffset.UtcNow.AddSeconds(30)) return _token!;
+ await _gate.WaitAsync(ct);
+ try
+ {
+ if (!string.IsNullOrEmpty(_token) && _exp > DateTimeOffset.UtcNow.AddSeconds(30)) return _token!;
+
+ var json = JsonSerializer.Serialize(new OAuthRequest(_opts.SCBApplicationKey, _opts.SCBApplicationSecret), _json);
+ using var req = new HttpRequestMessage(HttpMethod.Post, "oauth/token")
+ { Content = new StringContent(json, Encoding.UTF8, "application/json") };
+ AddHeaders(req.Headers, null);
+
+ using var res = await _http.SendAsync(req, ct);
+ var body = await res.Content.ReadAsStringAsync(ct);
+ res.EnsureSuccessStatusCode();
+
+ var parsed = JsonSerializer.Deserialize(body, _json)
+ ?? throw new UnauthorizedAccessException("OAuth parse failed");
+ if (parsed.Status?.Code != 1000) throw new UnauthorizedAccessException(parsed.Status?.Description ?? "OAuth not OK");
+
+ _token = parsed.Data?.AccessToken ?? throw new UnauthorizedAccessException("No token");
+ var ttl = parsed.Data?.ExpiresIn ?? 300;
+ _exp = DateTimeOffset.UtcNow.AddSeconds(ttl);
+ return _token!;
+ }
+ finally { _gate.Release(); }
+ }
+
+ private void AddHeaders(HttpRequestHeaders h, string? bearer)
+ {
+ h.TryAddWithoutValidation("accept-language", _opts.DefaultLanguage);
+ h.TryAddWithoutValidation("requestUId", Guid.NewGuid().ToString());
+ h.TryAddWithoutValidation("resourceOwnerId", _opts.SCBResourceOwnerId);
+ if (!string.IsNullOrEmpty(bearer))
+ h.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
+ }
}
\ No newline at end of file
diff --git a/qodana.yaml b/qodana.yaml
new file mode 100644
index 0000000..541fb41
--- /dev/null
+++ b/qodana.yaml
@@ -0,0 +1,46 @@
+#-------------------------------------------------------------------------------#
+# Qodana analysis is configured by qodana.yaml file #
+# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
+#-------------------------------------------------------------------------------#
+
+#################################################################################
+# WARNING: Do not store sensitive information in this file, #
+# as its contents will be included in the Qodana report. #
+#################################################################################
+version: "1.0"
+
+#Specify IDE code to run analysis without container (Applied in CI/CD pipeline)
+ide: QDNET
+
+#Specify inspection profile for code analysis
+profile:
+ name: qodana.starter
+
+#Enable inspections
+#include:
+# - name:
+
+#Disable inspections
+#exclude:
+# - name:
+# paths:
+# -
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+# - id: #(plugin id can be found at https://plugins.jetbrains.com)
+
+# Quality gate. Will fail the CI/CD pipeline if any condition is not met
+# severityThresholds - configures maximum thresholds for different problem severities
+# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code
+# Code Coverage is available in Ultimate and Ultimate Plus plans
+#failureConditions:
+# severityThresholds:
+# any: 15
+# critical: 5
+# testCoverageThresholds:
+# fresh: 70
+# total: 50