using System.Text; using System.Text.Json; using Application.Abstractions.Data; using Application.Abstractions.Payment; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace Infrastructure.Payment; internal sealed class DanalPayService( HttpClient httpClient, IAppDbContext db, ILogger logger ) : IDanalPayService { private const string ConfirmUrl = "https://one-api.danalpay.com/payments/confirm"; private const string CancelUrl = "https://one-api.danalpay.com/payments/cancel"; public async Task GetClientConfigAsync(CancellationToken ct) { var (cpid, clientKey, _) = await GetKeysAsync(ct); return new DanalClientConfig(clientKey, cpid); } public async Task ConfirmAsync(string method, string transactionID, string orderID, int amount, CancellationToken ct) { try { var (cpid, _, secretKey) = await GetKeysAsync(ct); var authHeader = BuildBasicAuth(secretKey); var request = new HttpRequestMessage(HttpMethod.Post, ConfirmUrl); request.Headers.Add("Authorization", authHeader); var body = new { method, transactionId = transactionID, orderId = orderID, amount = amount.ToString(), merchantId = cpid }; request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); var response = await httpClient.SendAsync(request, ct); var json = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("[DanalPay] Confirm response: {StatusCode} {Body}", response.StatusCode, json); var doc = JsonSerializer.Deserialize(json); var code = doc.TryGetProperty("code", out var codeProp) ? codeProp.GetString() : null; var message = doc.TryGetProperty("message", out var msgProp) ? msgProp.GetString() : null; string? Get(string key) => doc.TryGetProperty(key, out var p) ? p.GetString() : null; int? GetInt(string key) => int.TryParse(Get(key), out var v) ? v : null; byte? GetByte(string key) => byte.TryParse(Get(key), out var v) ? v : null; return new DanalConfirmResult( code == "SUCCESS", code, message, Get("transactionId") ?? transactionID, Get("orderName"), GetInt("totalAmount"), GetInt("discountAmount"), Get("userName"), Get("transDate"), Get("transTime"), Get("cardCode"), Get("cardName"), Get("cardNo"), GetByte("installmentMonths"), Get("approveNo"), Get("approvalDateTime"), Get("authKey"), Get("accountNumber"), Get("bankCode"), Get("userId"), Get("userEmail"), Get("bankName"), Get("expireDate"), Get("expireTime"), Get("virtualAccountNumber"), Get("useCashReceipt") ); } catch (Exception ex) { logger.LogError(ex, "[DanalPay] Confirm error for orderID={OrderID}", orderID); return new DanalConfirmResult(false, "ERROR", ex.Message, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null); } } public async Task CancelAsync(string method, string transactionID, int amount, string cancelType, CancellationToken ct) { try { var (cpid, _, secretKey) = await GetKeysAsync(ct); var authHeader = BuildBasicAuth(secretKey); var request = new HttpRequestMessage(HttpMethod.Post, CancelUrl); request.Headers.Add("Authorization", authHeader); var body = new { method, transactionId = transactionID, amount = amount.ToString(), merchantId = cpid, cancelType }; request.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"); var response = await httpClient.SendAsync(request, ct); var json = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("[DanalPay] Cancel response: {StatusCode} {Body}", response.StatusCode, json); var doc = JsonSerializer.Deserialize(json); var code = doc.TryGetProperty("code", out var codeProp) ? codeProp.GetString() : null; var message = doc.TryGetProperty("message", out var msgProp) ? msgProp.GetString() : null; string? Get(string key) => doc.TryGetProperty(key, out var p) ? p.GetString() : null; int? GetInt(string key) => int.TryParse(Get(key), out var v) ? v : null; return new DanalCancelResult( code == "SUCCESS", code, message, Get("originalTransactionId"), GetInt("cancelledAmount"), Get("transDate"), Get("transTime"), Get("balance"), Get("remainedAmount"), Get("approvalDateTime") ); } catch (Exception ex) { logger.LogError(ex, "[DanalPay] Cancel error for transactionID={TransactionID}", transactionID); return new DanalCancelResult(false, "ERROR", ex.Message, null, null, null, null, null, null, null); } } // ── Private ────────────────────────────────────────────────────── private async Task<(string Cpid, string ClientKey, string SecretKey)> GetKeysAsync(CancellationToken ct) { var config = await db.Config.FirstOrDefaultAsync(ct); if (config?.External is null) { throw new InvalidOperationException("다날 결제 설정이 없습니다. 관리자 페이지에서 설정해주세요."); } var ext = config.External; var isLive = string.Equals(ext.DanalPayMode, "live", StringComparison.OrdinalIgnoreCase); var cpid = isLive ? ext.DanalLiveCpid : ext.DanalTestCpid; var clientKey = isLive ? ext.DanalLiveClientKeyEnc : ext.DanalTestClientKeyEnc; var secretKey = isLive ? ext.DanalLiveSecretKeyEnc : ext.DanalTestSecretKeyEnc; if (string.IsNullOrEmpty(cpid) || string.IsNullOrEmpty(clientKey) || string.IsNullOrEmpty(secretKey)) { throw new InvalidOperationException($"다날 {(isLive ? "라이브" : "테스트")} 설정이 불완전합니다."); } return (cpid, clientKey, secretKey); } private static string BuildBasicAuth(string secretKey) { // SecretKey + ":" → Base64 var raw = $"{secretKey}:"; var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)); return $"Basic {base64}"; } }