Fix multi-bill display, charge amount parsing, and charge log responses

- chargeoptions is the real field gating 2000/5000/10000 yen display
- chargeid (not chargenum) carries the actual denomination on charge
- getrecentcharge/getlastlog: fixed num mismatch, added missing result
  section, and added placeholder items for empty result sets
This commit is contained in:
Kayori
2026-09-07 02:14:14 +02:00
parent 68b77bb9c3
commit e5bdb3eb95
8 changed files with 137 additions and 24 deletions
@@ -1,4 +1,5 @@
using Abstractions.Entities;
using System.Text.RegularExpressions;
using Abstractions.Entities;
using Abstractions.Handlers;
using Abstractions.SerializationTypes;
using Abstractions.Services;
@@ -10,7 +11,7 @@ using ISessionService = MedusaPluginChargeMachine.Services.ISessionService;
namespace MedusaPluginChargeMachine.Handlers.LA9;
public class ChargeEaChargeHandler(
public partial class ChargeEaChargeHandler(
ICardService cardService,
ISessionService sessionService,
UserManager<User> userManager,
@@ -22,9 +23,20 @@ public class ChargeEaChargeHandler(
Method("charge");
}
// chargeid carries the actual denomination as e.g. "N10000P"
// (Note/10000/Paseli) - chargenum is just the quantity of that
// denomination being charged (normally "1" per bill inserted).
[GeneratedRegex(@"^N(\d+)P$")]
private static partial Regex ChargeIdPattern();
public override async Task<ChargeResponse> HandleAsync(ChargeRequest req, GameModel model)
{
var amount = int.Parse(req.ChargeNumber) * 1000;
var match = ChargeIdPattern().Match(req.ChargeId);
if (!match.Success)
throw new InvalidOperationException($"Unrecognized chargeid format: '{req.ChargeId}'");
var denomination = int.Parse(match.Groups[1].Value);
var amount = denomination * int.Parse(req.ChargeNumber);
var foundSession = sessionService.GetSession(req.SessionId);
@@ -18,13 +18,6 @@ namespace MedusaPluginChargeMachine.Handlers.LA9;
{
var articles = await db.ArticleContents.ToListAsync();
// Real server ("automap" capture against services.tablanbass.net) sends
// an EMPTY articledoc for every DOCMT_* type - the disclosure/agreement
// text itself is apparently rendered client-side from a bundled local
// asset, not delivered over the wire. Sending real text for DOCMT_*
// here produced a solid black content area on a real cabinet (working
// touch, live server) instead of the disclosure text, so match the
// real server's (empty) behavior instead of guessing at content.
var articleList = articles
.Select(a => new EaChargeConfigChargeArticleItem
{
@@ -38,6 +31,7 @@ namespace MedusaPluginChargeMachine.Handlers.LA9;
{
Status = 0,
Expire = 1200,
ChargeOptions = new XrpcString("multibills"),
ChargeArticle = new EaChargeConfigChargeArticle
{
ArticleRevision = new XrpcString("1"),
@@ -31,28 +31,80 @@ public class GetLastLogEaChargeHandler(ISessionService sessionService, ChargeMac
if (cardId is not null)
query = query.Where(h => h.CardId == cardId);
var todayTotal = await query
var todayRows = await query
.Where(h => h.OrderDate.Date == today)
.SumAsync(h => (int?)h.ChargeAmount) ?? 0;
.ToListAsync();
var todayTotal = todayRows.Sum(h => h.ChargeAmount);
var headerItems = todayRows
.GroupBy(Denomination)
.Select(g => new GetLastLogChargeItem
{
ChargeId = new XrpcString($"N{g.Key}P"),
ChargeNum = new XrpcInt(g.Sum(h => h.ChargeNum)),
ChargeAmount = new XrpcInt(g.Sum(h => h.ChargeAmount))
})
.ToList();
var recentItems = await query
.OrderByDescending(h => h.OrderDate)
.Take(10)
.ToListAsync();
var resultItems = recentItems
.Select(h => new GetLastLogResultItem
{
OrderDate = new XrpcString(h.OrderDate.ToString("yyyyMMddHHmmss")),
ChargeId = new XrpcString($"N{Denomination(h)}P"),
ChargeNum = new XrpcInt(h.ChargeNum),
CardType = new XrpcInt(1),
CardNo = new XrpcString(MaskCardId(h.CardId))
})
.ToList();
if (headerItems.Count == 0)
{
headerItems.Add(new GetLastLogChargeItem
{
ChargeId = new XrpcString("N1000P"),
ChargeNum = new XrpcInt(0),
ChargeAmount = new XrpcInt(0)
});
}
if (resultItems.Count == 0)
{
resultItems.Add(new GetLastLogResultItem
{
OrderDate = new XrpcString(""),
ChargeId = new XrpcString("N1000P"),
ChargeNum = new XrpcInt(0),
CardType = new XrpcInt(1),
CardNo = new XrpcString("")
});
}
return new GetLastLogEaChargeResponse
{
Status = 0,
Header = new GetLastLogEaChargeHeader
{
Chargedaily = new XrpcInt(todayTotal),
Items = recentItems.Select((h, i) => new GetLastLogChargeItem
{
ChargeId = new XrpcString(h.Id.ToString()),
ChargeNum = new XrpcInt(h.ChargeNum),
ChargeAmount = new XrpcInt(h.ChargeAmount)
}).ToList()
Items = headerItems
},
Result = new GetLastLogEaChargeResult
{
Items = resultItems
}
};
}
private static int Denomination(Entities.ChargeHistory h) =>
h.ChargeAmount / Math.Max(h.ChargeNum, 1);
private static string MaskCardId(string cardId) =>
cardId.Length <= 4
? new string('*', cardId.Length)
: new string('*', cardId.Length - 4) + cardId[^4..];
}
@@ -21,7 +21,10 @@ public class GetRecentChargeEaChargeHandler(ISessionService sessionService, Char
var cardId = ResolveCardId(req);
var page = int.TryParse(req.Page, out var p) ? Math.Max(1, p) : 1;
var perPage = int.TryParse(req.PerPage, out var pp) ? Math.Max(1, pp) : 10;
var perPage = int.TryParse(req.Num, out var n) ? Math.Max(1, n)
: int.TryParse(req.PerPage, out var pp) ? Math.Max(1, pp)
: 10;
var query = db.ChargeHistories.AsQueryable();
@@ -31,8 +34,6 @@ public class GetRecentChargeEaChargeHandler(ISessionService sessionService, Char
if (req.TargetDate is not null && DateTime.TryParse(req.TargetDate, out var targetDate))
query = query.Where(h => h.OrderDate.Date == targetDate.Date);
var total = await query.CountAsync();
var rows = await query
.OrderByDescending(h => h.OrderDate)
.Skip((page - 1) * perPage)
@@ -49,10 +50,23 @@ public class GetRecentChargeEaChargeHandler(ISessionService sessionService, Char
FontColor = new XrpcString("#FFFFFF")
}).ToList();
if (items.Count == 0)
{
items.Add(new GetRecentChargeItem
{
RcvTime = new XrpcString(""),
ChrgPrice = new XrpcInt(0),
CardNo = new XrpcString(""),
AccCode = new XrpcString(""),
RcvStatus = new XrpcInt(0),
FontColor = new XrpcString("#FFFFFF")
});
}
return new GetRecentChargeResponse
{
Status = 0,
Num = new XrpcInt(total),
Num = new XrpcInt(items.Count),
Items = items
};
}
@@ -8,6 +8,14 @@ public class ChargeRequest
[XmlElement("chargenum")]
public required string ChargeNumber { get; set; }
// The actual denomination, not chargenum - real traffic (spice2x
// "automap" capture) shows chargenum is always the quantity ("1" for a
// single bill), while chargeid carries the value as e.g. "N10000P"
// (Note/10000/Paseli). chargenum * 1000 alone reports every bill as
// 1000 yen regardless of what was actually inserted.
[XmlElement("chargeid")]
public required string ChargeId { get; set; }
[XmlElement("sessid")]
public required string SessionId { get; set; }
}
@@ -5,6 +5,9 @@ namespace MedusaPluginChargeMachine.Models.Request;
[XmlRoot("eacharge")]
public class GetRecentChargeRequest
{
[XmlElement("num")]
public string? Num { get; set; }
[XmlElement("sessid")]
public string? SessionId { get; set; }
@@ -12,7 +12,10 @@ public class EaChargeConfigResponse
[XmlAttribute("expire")]
public required int Expire { get; set; }
[XmlElement("chargeoptions")]
public required XrpcString ChargeOptions { get; set; }
[XmlElement("chargearticle")]
public required EaChargeConfigChargeArticle ChargeArticle { get; set; }
@@ -12,6 +12,9 @@ public class GetLastLogEaChargeResponse
[XmlElement("header")]
public required GetLastLogEaChargeHeader Header { get; set; }
[XmlElement("result")]
public required GetLastLogEaChargeResult Result { get; set; }
}
public record GetLastLogEaChargeHeader
@@ -33,4 +36,28 @@ public record GetLastLogChargeItem
[XmlElement("chargeamount")]
public XrpcInt ChargeAmount { get; set; }
}
}
public record GetLastLogEaChargeResult
{
[XmlElement("item")]
public List<GetLastLogResultItem> Items { get; set; } = [];
}
public record GetLastLogResultItem
{
[XmlElement("orderdate")]
public XrpcString OrderDate { get; set; }
[XmlElement("chargeid")]
public XrpcString ChargeId { get; set; }
[XmlElement("chargenum")]
public XrpcInt ChargeNum { get; set; }
[XmlElement("cardtype")]
public XrpcInt CardType { get; set; }
[XmlElement("cardno")]
public XrpcString CardNo { get; set; }
}