using System.Net.Http.Json; using System.Text.Json; using Meezi.API.Models.Printing; using Meezi.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace Meezi.API.Services; public record PosDeviceResult(bool Success, bool Skipped, string? ErrorCode, string? Detail = null) { public static PosDeviceResult Ok() => new(true, false, null); public static PosDeviceResult SkippedNotConfigured() => new(true, true, null); public static PosDeviceResult Fail(string code, string? detail = null) => new(false, false, code, detail); } public interface IPosDeviceService { Task SendPaymentRequestAsync( string cafeId, string branchId, PosPaymentRequest request, CancellationToken ct = default); } public class PosDeviceService : IPosDeviceService { private const int DefaultPort = 8088; private static readonly TimeSpan RequestTimeout = TimeSpan.FromSeconds(90); private readonly AppDbContext _db; private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; public PosDeviceService( AppDbContext db, IHttpClientFactory httpClientFactory, ILogger logger) { _db = db; _httpClientFactory = httpClientFactory; _logger = logger; } public async Task SendPaymentRequestAsync( string cafeId, string branchId, PosPaymentRequest request, CancellationToken ct = default) { if (request.Amount <= 0) return PosDeviceResult.Fail("INVALID_AMOUNT"); var branch = await _db.Branches .AsNoTracking() .FirstOrDefaultAsync(b => b.Id == branchId && b.CafeId == cafeId, ct); if (branch is null) return PosDeviceResult.Fail("BRANCH_NOT_FOUND"); if (string.IsNullOrWhiteSpace(branch.PosDeviceIp)) return PosDeviceResult.SkippedNotConfigured(); var port = branch.PosDevicePort is > 0 and <= 65535 ? branch.PosDevicePort.Value : DefaultPort; var order = await _db.Orders .AsNoTracking() .FirstOrDefaultAsync(o => o.Id == request.OrderId && o.CafeId == cafeId, ct); if (order is null) return PosDeviceResult.Fail("ORDER_NOT_FOUND"); var payload = new { amount = (long)Math.Round(request.Amount, 0, MidpointRounding.AwayFromZero), orderId = request.OrderId, branchId, }; var url = $"http://{branch.PosDeviceIp!.Trim()}:{port}/pay"; try { var client = _httpClientFactory.CreateClient(nameof(PosDeviceService)); client.Timeout = RequestTimeout; using var response = await client.PostAsJsonAsync(url, payload, ct); if (!response.IsSuccessStatusCode) { var body = await response.Content.ReadAsStringAsync(ct); _logger.LogWarning( "POS device returned {Status} for {Url}: {Body}", (int)response.StatusCode, url, body.Length > 200 ? body[..200] : body); return PosDeviceResult.Fail( "POS_DEVICE_REJECTED", $"HTTP {(int)response.StatusCode}"); } return PosDeviceResult.Ok(); } catch (TaskCanceledException) when (!ct.IsCancellationRequested) { return PosDeviceResult.Fail("POS_DEVICE_TIMEOUT"); } catch (HttpRequestException ex) { _logger.LogWarning(ex, "POS device connection failed for {Url}", url); return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message); } catch (JsonException ex) { _logger.LogWarning(ex, "POS device response invalid for {Url}", url); return PosDeviceResult.Fail("POS_DEVICE_CONNECTION_FAILED", ex.Message); } } }