diff --git a/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/AADEFactory.cs b/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/AADEFactory.cs index e7ef1100f..4aa40e21d 100644 --- a/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/AADEFactory.cs +++ b/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/AADEFactory.cs @@ -1227,20 +1227,22 @@ private static long GetInvoiceMark(ReceiptResponse receiptResponse) private static List GetPayments(ReceiptRequest receiptRequest) { - // what is payitemcase 99? - return receiptRequest.cbPayItems.Where(x => !(x.ftPayItemCase.IsCase(PayItemCase.Grant) && x.ftPayItemCase.IsFlag(PayItemCaseFlags.Tip)) && !(x.ftPayItemCase.IsCase(PayItemCase.DebitCardPayment) && x.ftPayItemCase.IsFlag(PayItemCaseFlags.Tip))).Select(x => + return receiptRequest.GetGroupedPayItems().Select(group => { + var x = group.payItem; var payment = new PaymentMethodDetailType { type = AADEMappings.GetPaymentType(x), amount = receiptRequest.ftReceiptCase.IsFlag(ReceiptCaseFlags.Refund) ? -x.Amount : x.Amount, paymentMethodInfo = x.Description, }; - var tipPayment = receiptRequest.cbPayItems.FirstOrDefault(x => x.ftPayItemCase.IsFlag(PayItemCaseFlags.Tip)); - if (tipPayment != null) + if (group.tip != null) { - payment.tipAmount = tipPayment.Amount; + payment.tipAmount = Math.Abs(group.tip.Amount); // TipAmount should always be positive payment.tipAmountSpecified = true; + // in case of using a tip we need to correct the amount being sent to AADE, as AADE expects the total amount including the tip, while in our model the tip is a separate PayItem. So we need to add the tip amount to the main payment amount. In case of refunds we assume that the tip is also refunded and we apply the same negative sign as for the main payment. + var combinedAmount = x.Amount + group.tip.Amount; + payment.amount = receiptRequest.ftReceiptCase.IsFlag(ReceiptCaseFlags.Refund) ? -combinedAmount : combinedAmount; } if (x.ftPayItemCaseData != null) diff --git a/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/Helpers/ReceiptRequestExtensions.cs b/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/Helpers/ReceiptRequestExtensions.cs index a3ce19875..77c5d401e 100644 --- a/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/Helpers/ReceiptRequestExtensions.cs +++ b/scu-gr/src/fiskaltrust.Middleware.SCU.GR.MyData/Helpers/ReceiptRequestExtensions.cs @@ -142,6 +142,39 @@ public static bool TryDeserializeftChargeItemCaseData(this ChargeItem chargeI return data; } + public static List<(PayItem payItem, PayItem? tip)> GetGroupedPayItems(this ReceiptRequest receiptRequest) + { + var data = new List<(PayItem payItem, PayItem? tip)>(); + foreach (var payItem in receiptRequest.cbPayItems) + { + if (payItem.ftPayItemCase.IsFlag(PayItemCaseFlags.Tip)) + { + var last = data.Count > 0 ? data[^1] : default; + if (last == default) + { + throw new ArgumentException($"Tip pay item at position {payItem.Position} ('{payItem.Description}') has no preceding payment."); + } + else if (last.tip != null) + { + throw new ArgumentException($"Tip pay item at position {payItem.Position} ('{payItem.Description}') cannot be attached because the preceding payment at position {last.payItem.Position} already has a tip assigned."); + } + else if (last.payItem.ftPayItemCase.Case() != payItem.ftPayItemCase.Case()) + { + throw new ArgumentException($"Tip pay item at position {payItem.Position} ('{payItem.Description}') has a payment case that does not match the preceding payment at position {last.payItem.Position}."); + } + else + { + data[^1] = (last.payItem, payItem); + } + } + else + { + data.Add((payItem, null)); + } + } + return data; + } + public static bool ContainsCustomerInfo(this ReceiptRequest receiptRequest) { if (receiptRequest.cbCustomer != null) diff --git a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/AADEMappingsIncomeClassificationCategoryTypeTests.cs b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/AADEMappingsIncomeClassificationCategoryTypeTests.cs index f4501781f..1bd16069d 100644 --- a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/AADEMappingsIncomeClassificationCategoryTypeTests.cs +++ b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/AADEMappingsIncomeClassificationCategoryTypeTests.cs @@ -81,22 +81,4 @@ public void GetIncomeClassificationCategoryType_WithDifferentServiceTypes_Return // Assert result.Should().Be(expectedCategory); } - - [Theory] - [InlineData(ChargeItemCaseTypeOfService.Tip)] - [InlineData(ChargeItemCaseTypeOfService.Voucher)] - [InlineData(ChargeItemCaseTypeOfService.Grant)] - [InlineData(ChargeItemCaseTypeOfService.Receivable)] - [InlineData(ChargeItemCaseTypeOfService.CashTransfer)] - public void GetIncomeClassificationCategoryType_WithNonSupportedType_ReturnsException(ChargeItemCaseTypeOfService serviceType) - { - // Arrange - var receiptRequest = CreateReceiptRequest(); - var chargeItem = CreateChargeItem(serviceType); - - - var action = () => AADEMappings.GetIncomeClassificationCategoryType(receiptRequest, chargeItem); - action.Should().Throw() - .WithMessage($"The ChargeItem type {chargeItem.ftChargeItemCase.TypeOfService()} is not supported for IncomeClassificationCategoryType."); - } } \ No newline at end of file diff --git a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetGroupedPayItemsTests.cs b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetGroupedPayItemsTests.cs new file mode 100644 index 000000000..4aec1e426 --- /dev/null +++ b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetGroupedPayItemsTests.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using fiskaltrust.ifPOS.v2; +using fiskaltrust.ifPOS.v2.Cases; +using fiskaltrust.Middleware.SCU.GR.MyData.Helpers; +using FluentAssertions; +using Xunit; + +namespace fiskaltrust.Middleware.SCU.GR.MyData.UnitTest; + +public class GetGroupedPayItemsTests +{ + private static PayItemCase GR(PayItemCase c) => ((PayItemCase) 0x4752_2000_0000_0000).WithCase(c); + + private static ReceiptRequest BuildRequest(List payItems) => + new ReceiptRequest + { + cbTerminalID = "1", + Currency = Currency.EUR, + cbReceiptMoment = System.DateTime.UtcNow, + cbReceiptReference = System.Guid.NewGuid().ToString(), + ftPosSystemId = System.Guid.NewGuid(), + cbChargeItems = new List(), + cbPayItems = payItems, + ftReceiptCase = ((ReceiptCase) 0x4752_2000_0000_0000).WithCase(ReceiptCase.Pay0x3005) + }; + + [Fact] + public void SinglePayment_NoTip_ShouldReturnOneGroupWithNullTip() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment) } + }); + + var result = request.GetGroupedPayItems(); + + result.Should().HaveCount(1); + result[0].payItem.Description.Should().Be("Card"); + result[0].tip.Should().BeNull(); + } + + [Fact] + public void SinglePayment_FollowedByMatchingTip_ShouldGroup() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = -1.5m, Description = "Tip", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var result = request.GetGroupedPayItems(); + + result.Should().HaveCount(1); + result[0].payItem.Description.Should().Be("Card"); + result[0].tip.Should().NotBeNull(); + result[0].tip!.Amount.Should().Be(-1.5m); + } + + [Fact] + public void SinglePayment_FollowedByTipWithDifferentCase_ShouldThrow() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Cash", ftPayItemCase = GR(PayItemCase.CashPayment) }, + new PayItem { Position = 2, Amount = -1m, Description = "Tip", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var act = () => request.GetGroupedPayItems(); + + act.Should().Throw().Which.Message.Should().Contain("does not match"); + } + + [Fact] + public void TwoPayments_EachFollowedByTip_ShouldGroupCorrectly() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card 1", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = -1.5m, Description = "Tip 1", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) }, + new PayItem { Position = 3, Amount = 20m, Description = "Card 2", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 4, Amount = -3m, Description = "Tip 2", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var result = request.GetGroupedPayItems(); + + result.Should().HaveCount(2); + result[0].payItem.Description.Should().Be("Card 1"); + result[0].tip!.Description.Should().Be("Tip 1"); + result[1].payItem.Description.Should().Be("Card 2"); + result[1].tip!.Description.Should().Be("Tip 2"); + } + + [Fact] + public void TwoPayments_OnlyFirstFollowedByTip_SecondShouldHaveNoTip() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card 1", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = -2m, Description = "Tip", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) }, + new PayItem { Position = 3, Amount = 15m, Description = "Card 2", ftPayItemCase = GR(PayItemCase.CreditCardPayment) } + }); + + var result = request.GetGroupedPayItems(); + + result.Should().HaveCount(2); + result[0].tip!.Description.Should().Be("Tip"); + result[1].tip.Should().BeNull("no tip follows the second card"); + } + + [Fact] + public void TipNotDirectlyAfterPayment_ShouldThrow() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = 5m, Description = "Cash", ftPayItemCase = GR(PayItemCase.CashPayment) }, + new PayItem { Position = 3, Amount = -1m, Description = "Tip for Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var act = () => request.GetGroupedPayItems(); + + act.Should().Throw().Which.Message.Should().Contain("does not match"); + } + + [Fact] + public void PaymentAlreadyHasTip_SecondTipShouldThrow() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = -1m, Description = "Tip 1", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) }, + new PayItem { Position = 3, Amount = -0.5m, Description = "Tip 2", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var act = () => request.GetGroupedPayItems(); + + act.Should().Throw().Which.Message.Should().Contain("already has a tip"); + } + + [Fact] + public void EmptyPayItems_ShouldReturnEmptyList() + { + var request = BuildRequest(new List()); + + var result = request.GetGroupedPayItems(); + + result.Should().BeEmpty(); + } + + [Fact] + public void OnlyTipItems_ShouldThrow() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = -1m, Description = "Tip", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var act = () => request.GetGroupedPayItems(); + + act.Should().Throw().Which.Message.Should().Contain("no preceding payment"); + } + + [Fact] + public void MixedCases_TipOnlyMatchesImmediatelyPrecedingSameCase() + { + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10m, Description = "Card", ftPayItemCase = GR(PayItemCase.CreditCardPayment) }, + new PayItem { Position = 2, Amount = -1m, Description = "Card Tip", ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) }, + new PayItem { Position = 3, Amount = 5m, Description = "Cash", ftPayItemCase = GR(PayItemCase.CashPayment) }, + new PayItem { Position = 4, Amount = -0.5m, Description = "Cash Tip", ftPayItemCase = GR(PayItemCase.CashPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + var result = request.GetGroupedPayItems(); + + result.Should().HaveCount(2); + result[0].payItem.Description.Should().Be("Card"); + result[0].tip!.Description.Should().Be("Card Tip"); + result[1].payItem.Description.Should().Be("Cash"); + result[1].tip!.Description.Should().Be("Cash Tip"); + } +} diff --git a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetPaymentsTests.cs b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetPaymentsTests.cs new file mode 100644 index 000000000..d7935bcb4 --- /dev/null +++ b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/GetPaymentsTests.cs @@ -0,0 +1,380 @@ +using System; +using System.Collections.Generic; +using fiskaltrust.ifPOS.v2; +using fiskaltrust.ifPOS.v2.Cases; +using fiskaltrust.Middleware.SCU.GR.Abstraction; +using fiskaltrust.Middleware.SCU.GR.MyData; +using fiskaltrust.Middleware.SCU.GR.MyData.Models; +using FluentAssertions; +using Xunit; + +namespace fiskaltrust.Middleware.SCU.GR.MyData.UnitTest; + +public class GetPaymentsTests +{ + private const long LocalPayItemFlag = 0x0000_0001_0000_0000; + + private static readonly ReceiptCase Pay0x3005Case = + ((ReceiptCase) 0x4752_2000_0000_0000).WithCase(ReceiptCase.Pay0x3005); + + private static PayItemCase GR(PayItemCase c) => + ((PayItemCase) 0x4752_2000_0000_0000).WithCase(c); + + private static PayItemCase WithLocalFlag(PayItemCase payItemCase) => + (PayItemCase) ((long) payItemCase | LocalPayItemFlag); + + private static AADEFactory CreateFactory() => + new AADEFactory(new storage.V0.MasterData.MasterDataConfiguration + { + Account = new storage.V0.MasterData.AccountMasterData { VatId = "EL123456789" } + }, "https://test.receipts.example.com"); + + private static ReceiptRequest BuildRequest(List payItems) => + new ReceiptRequest + { + cbTerminalID = "1", + Currency = Currency.EUR, + cbReceiptMoment = DateTime.UtcNow, + cbReceiptReference = Guid.NewGuid().ToString(), + ftPosSystemId = Guid.NewGuid(), + cbChargeItems = new List(), + cbPayItems = payItems, + ftReceiptCase = Pay0x3005Case + }; + + private PaymentMethodDetailType[] GetPaymentDetails(List payItems, ReceiptCase? receiptCase = null) + { + var factory = CreateFactory(); + var request = BuildRequest(payItems); + if (receiptCase.HasValue) + { + request.ftReceiptCase = receiptCase.Value; + } + (var doc, var error) = factory.MapToPaymentMethodsDoc(request, 123); + error.Should().BeNull(); + return doc!.paymentMethods[0].paymentMethodDetails; + } + + #region Payment Type Mapping + + [Fact] + public void CashPayment_ShouldMapToCashType() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 15.0m, Description = "Cash Payment", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)) } + }); + + details.Should().HaveCount(1); + var detail = details[0]; + detail.type.Should().Be(MyDataPaymentMethods.Cash); + detail.amount.Should().Be(15.0m); + detail.paymentMethodInfo.Should().Be("Cash Payment"); + detail.tipAmountSpecified.Should().BeFalse(); + detail.ProvidersSignature.Should().BeNull(); + } + + [Fact] + public void DebitCardPayment_ShouldMapToPosEPos() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 20.0m, Description = "Debit Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.DebitCardPayment)) } + }); + + details[0].type.Should().Be(MyDataPaymentMethods.PosEPos); + } + + [Fact] + public void CreditCardPayment_ShouldMapToPosEPos() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 20.0m, Description = "Credit Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)) } + }); + + details[0].type.Should().Be(MyDataPaymentMethods.PosEPos); + } + + [Fact] + public void DescriptionShouldBeSetAsPaymentMethodInfo() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 10.0m, Description = "My Custom Description", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)) } + }); + + details[0].paymentMethodInfo.Should().Be("My Custom Description"); + } + + #endregion + + #region Refund + + [Fact] + public void RefundFlag_ShouldNegateAmount() + { + var details = GetPaymentDetails( + new List + { + new PayItem { Position = 1, Amount = -10.0m, Description = "Refund", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)).WithFlag(PayItemCaseFlags.Refund) } + }, + Pay0x3005Case.WithFlag(ReceiptCaseFlags.Refund)); + + details[0].amount.Should().Be(10.0m, "refund negates the negative amount to positive"); + } + + [Fact] + public void RefundWithTip_ShouldNegateAmountAndSubtractTip() + { + var details = GetPaymentDetails( + new List + { + new PayItem { Position = 1, Amount = -10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)).WithFlag(PayItemCaseFlags.Refund) }, + new PayItem { Position = 2, Amount = 2.0m, Description = "Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip).WithFlag(PayItemCaseFlags.Refund) } + }, + Pay0x3005Case.WithFlag(ReceiptCaseFlags.Refund)); + + details.Should().HaveCount(1); + var detail = details[0]; + detail.amount.Should().Be(8.0m, "refund negates -10.0 to 10.0, then tip (2.0) is subtracted"); + detail.tipAmount.Should().Be(2.0m); + detail.tipAmountSpecified.Should().BeTrue(); + } + + [Fact] + public void RefundWithTip_PositiveRefundPayItem_ShouldAddTipForNegativeAmount() + { + var details = GetPaymentDetails( + new List + { + new PayItem { Position = 1, Amount = -10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)).WithFlag(PayItemCaseFlags.Refund) }, + new PayItem { Position = 2, Amount = 1.8m, Description = "Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip).WithFlag(PayItemCaseFlags.Refund) } + }, + Pay0x3005Case.WithFlag(ReceiptCaseFlags.Refund)); + + details.Should().HaveCount(1); + var detail = details[0]; + detail.amount.Should().Be(8.2m, "refund negates +10.0 to -10.0, then tip (1.8) should be added"); + detail.tipAmount.Should().Be(1.8m); + detail.tipAmountSpecified.Should().BeTrue(); + } + + #endregion + + #region Tip Handling + + [Fact] + public void TipNextInLine_ShouldSubtractFromAmountAndSetTipAmount() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)) }, + new PayItem { Position = 2, Amount = -1.8m, Description = "Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + details.Should().HaveCount(1); + var detail = details[0]; + detail.amount.Should().Be(8.2m, "10.0 - 1.8 tip"); + detail.tipAmount.Should().Be(1.8m); + detail.tipAmountSpecified.Should().BeTrue(); + } + + [Fact] + public void TipWithDifferentCase_ShouldReturnError() + { + var factory = CreateFactory(); + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10.0m, Description = "Cash", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)) }, + new PayItem { Position = 2, Amount = -1.0m, Description = "Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(request, 123); + + doc.Should().BeNull(); + error.Should().NotBeNull(); + error!.Exception.Should().BeOfType(); + error.Exception.Message.Should().Contain("does not match"); + } + + [Fact] + public void TipNotDirectlyAfterPayment_ShouldReturnError() + { + var factory = CreateFactory(); + var request = BuildRequest(new List + { + new PayItem { Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)) }, + new PayItem { Position = 2, Amount = 5.0m, Description = "Cash", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)) }, + new PayItem { Position = 3, Amount = -1.0m, Description = "Card Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(request, 123); + + doc.Should().BeNull(); + error.Should().NotBeNull(); + error!.Exception.Should().BeOfType(); + error.Exception.Message.Should().Contain("does not match"); + } + + [Fact] + public void MultiplePaymentTypes_TipOnlyAppliestoMatchingPrecedingPayment() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 5.0m, Description = "Cash", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CashPayment)) }, + new PayItem { Position = 2, Amount = 15.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)) }, + new PayItem { Position = 3, Amount = -1.0m, Description = "Card Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) } + }); + + details.Should().HaveCount(2); + details[0].type.Should().Be(MyDataPaymentMethods.Cash); + details[0].amount.Should().Be(5.0m); + details[0].tipAmountSpecified.Should().BeFalse(); + + details[1].type.Should().Be(MyDataPaymentMethods.PosEPos); + details[1].amount.Should().Be(14.0m, "15.0 - 1.0 tip"); + details[1].tipAmount.Should().Be(1.0m); + details[1].tipAmountSpecified.Should().BeTrue(); + } + + #endregion + + #region Provider Signature Data + + [Fact] + public void NoPayItemCaseData_ShouldNotSetProviderSignature() + { + var details = GetPaymentDetails(new List + { + new PayItem { Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.DebitCardPayment)) } + }); + + details[0].ProvidersSignature.Should().BeNull(); + details[0].transactionId.Should().BeNull(); + } + + [Fact] + public void CloudApiProviderData_ShouldExtractSignatureAndTransactionId() + { + var details = GetPaymentDetails(new List + { + new PayItem + { + Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.DebitCardPayment)), + ftPayItemCaseData = new + { + Provider = new + { + Protocol = "", + ProtocolVersion = "1.0", + Action = "", + ProtocolRequest = new + { + aadeProviderSignatureData = "", + aadeProviderSignature = "cloud-signature-123" + }, + ProtocolResponse = new + { + aadeTransactionId = "TXN-CLOUD-001" + } + } + } + } + }); + + var detail = details[0]; + detail.ProvidersSignature.Should().NotBeNull(); + detail.ProvidersSignature!.Signature.Should().Be("cloud-signature-123"); + detail.ProvidersSignature.SigningAuthor.Should().Be("126"); + detail.transactionId.Should().Be("TXN-CLOUD-001"); + } + + [Fact] + public void GenericAadeSignatureData_ShouldExtractSignatureAndTransactionId() + { + var details = GetPaymentDetails(new List + { + new PayItem + { + Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.DebitCardPayment)), + ftPayItemCaseData = new + { + aadeSignatureData = new + { + aadeProviderSignature = "generic-signature-456", + aadeTransactionId = "TXN-GENERIC-002" + } + } + } + }); + + var detail = details[0]; + detail.ProvidersSignature.Should().NotBeNull(); + detail.ProvidersSignature!.Signature.Should().Be("generic-signature-456"); + detail.ProvidersSignature.SigningAuthor.Should().Be("126"); + detail.transactionId.Should().Be("TXN-GENERIC-002"); + } + + #endregion + + #region Combined Tip + Provider Data + + [Fact] + public void TipWithProviderData_ShouldApplyBoth() + { + var details = GetPaymentDetails(new List + { + new PayItem + { + Position = 1, Amount = 10.0m, Description = "Card", + ftPayItemCase = WithLocalFlag(GR(PayItemCase.CreditCardPayment)), + ftPayItemCaseData = new + { + aadeSignatureData = new + { + aadeProviderSignature = "sig-with-tip", + aadeTransactionId = "TXN-TIP-001" + } + } + }, + new PayItem + { + Position = 2, Amount = -1.5m, Description = "Tip", + ftPayItemCase = GR(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + details.Should().HaveCount(1); + var detail = details[0]; + detail.amount.Should().Be(8.5m, "10.0 - 1.5 tip"); + detail.tipAmount.Should().Be(1.5m); + detail.tipAmountSpecified.Should().BeTrue(); + detail.ProvidersSignature!.Signature.Should().Be("sig-with-tip"); + detail.transactionId.Should().Be("TXN-TIP-001"); + } + + #endregion +} diff --git a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/MyDataXmlSignatureTests.cs b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/MyDataXmlSignatureTests.cs index 9def19367..8bb5dca74 100644 --- a/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/MyDataXmlSignatureTests.cs +++ b/scu-gr/test/fiskaltrust.Middleware.SCU.GR.MyData.UnitTest/MyDataXmlSignatureTests.cs @@ -49,8 +49,8 @@ public void AddMyDataXmlSignature_ShouldAddSignatureWithXmlPayload() request.ReceiptResponse.ftSignatures.Should().ContainSingle(s => s.Caption == "mydata-xml"); var sig = request.ReceiptResponse.ftSignatures.First(s => s.Caption == "mydata-xml"); - sig.Data.Should().StartWith(""); + sig.Data.Should().StartWith(""); sig.Data.Should().NotStartWith("123456789"); sig.ftSignatureFormat.Should().Be(SignatureFormat.Text); @@ -286,8 +286,8 @@ public void AddMyDataXmlSignature_StoredXmlShouldContainAllInvoiceFields() var sig = request.ReceiptResponse.ftSignatures.First(s => s.Caption == "mydata-xml"); var xml = sig.Data; - xml.Should().StartWith(""); + xml.Should().StartWith(""); xml.Should().NotStartWith(" + { + new PayItem + { + Position = 1, + Amount = 10.0m, + Description = "Card", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment)), + ftPayItemCaseData = new + { + Provider = new + { + Protocol = "", + ProtocolVersion = "1.0", + Action = "", + ProtocolRequest = new + { + aadeProviderSignatureData = "", + aadeProviderSignature = "817a9c8bc1b5fcfed5cc47b8ed85ba18" + }, + ProtocolResponse = new + { + aadeTransactionId = "TXN20240001" + } + } + } + }, + new PayItem + { + Position = 2, + Amount = -1.8m, + Description = "Tip", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 400001951868897); + + error.Should().BeNull(); + doc.Should().NotBeNull(); + doc!.paymentMethods[0].paymentMethodDetails.Should().HaveCount(1, + "the tip pay item should be filtered out, not treated as a separate payment method"); + var detail = doc.paymentMethods[0].paymentMethodDetails[0]; + detail.type.Should().Be(MyDataPaymentMethods.PosEPos); + detail.amount.Should().Be(8.2m, + "the payment amount (10.0) should be reduced by the tip (1.8)"); + detail.tipAmountSpecified.Should().BeTrue(); + detail.tipAmount.Should().Be(1.8m, + "tipAmount should be the absolute value of the tip pay item amount"); + detail.ProvidersSignature.Should().NotBeNull(); + detail.ProvidersSignature!.Signature.Should().Be("817a9c8bc1b5fcfed5cc47b8ed85ba18"); + detail.transactionId.Should().Be("TXN20240001"); + } + + [Fact] + public void MapToPaymentMethodsDoc_WithNegativeTipAmount_ShouldUseAbsoluteValue() + { + var factory = CreateFactory(); + var receiptRequest = BuildRequest(new List + { + new PayItem + { + Position = 1, + Amount = 25.0m, + Description = "Cash", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CashPayment)) + }, + new PayItem + { + Position = 2, + Amount = -3.5m, + Description = "Tip", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CashPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 123456789); + + error.Should().BeNull(); + doc!.paymentMethods[0].paymentMethodDetails.Should().HaveCount(1); + var detail = doc.paymentMethods[0].paymentMethodDetails[0]; + detail.amount.Should().Be(21.5m, + "the payment amount (25.0) should be reduced by the tip (3.5)"); + detail.tipAmount.Should().Be(3.5m, + "negative tip amounts should be converted to positive via Math.Abs"); + detail.tipAmountSpecified.Should().BeTrue(); + } + + [Fact] + public void MapToPaymentMethodsDoc_WithTipOnDifferentCase_ShouldReturnError() + { + var factory = CreateFactory(); + var receiptRequest = BuildRequest(new List + { + new PayItem + { + Position = 1, + Amount = 25.0m, + Description = "Cash", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CashPayment)) + }, + new PayItem + { + Position = 2, + Amount = -3.5m, + Description = "Tip", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 123456789); + + doc.Should().BeNull(); + error.Should().NotBeNull(); + error!.Exception.Should().BeOfType(); + error.Exception.Message.Should().Contain("does not match"); + } + + [Fact] + public void MapToPaymentMethodsDoc_WithTwoCardPaymentsAndTwoTips_ShouldMatchEachTipToItsPayment() + { + var factory = CreateFactory(); + var receiptRequest = BuildRequest(new List + { + new PayItem + { + Position = 1, + Amount = 10.0m, + Description = "Card 1", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment)) + }, + new PayItem + { + Position = 2, + Amount = -1.5m, + Description = "Tip 1", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + }, + new PayItem + { + Position = 3, + Amount = 20.0m, + Description = "Card 2", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment)) + }, + new PayItem + { + Position = 4, + Amount = -3.0m, + Description = "Tip 2", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 400001951868897); + + error.Should().BeNull(); + doc!.paymentMethods[0].paymentMethodDetails.Should().HaveCount(2); + + var detail1 = doc.paymentMethods[0].paymentMethodDetails[0]; + detail1.amount.Should().Be(8.5m, "10.0 - 1.5 tip"); + detail1.tipAmount.Should().Be(1.5m); + detail1.tipAmountSpecified.Should().BeTrue(); + + var detail2 = doc.paymentMethods[0].paymentMethodDetails[1]; + detail2.amount.Should().Be(17.0m, "20.0 - 3.0 tip"); + detail2.tipAmount.Should().Be(3.0m); + detail2.tipAmountSpecified.Should().BeTrue(); + } + + [Fact] + public void MapToPaymentMethodsDoc_WithTwoCardPaymentsAndOneTip_ShouldOnlyApplyTipToFirst() + { + var factory = CreateFactory(); + var receiptRequest = BuildRequest(new List + { + new PayItem + { + Position = 1, + Amount = 10.0m, + Description = "Card 1", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment)) + }, + new PayItem + { + Position = 2, + Amount = -2.0m, + Description = "Tip", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + }, + new PayItem + { + Position = 3, + Amount = 15.0m, + Description = "Card 2", + ftPayItemCase = WithLocalFlag(((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment)) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 400001951868897); + + error.Should().BeNull(); + doc!.paymentMethods[0].paymentMethodDetails.Should().HaveCount(2); + + var detail1 = doc.paymentMethods[0].paymentMethodDetails[0]; + detail1.amount.Should().Be(8.0m, "10.0 - 2.0 tip"); + detail1.tipAmount.Should().Be(2.0m); + detail1.tipAmountSpecified.Should().BeTrue(); + + var detail2 = doc.paymentMethods[0].paymentMethodDetails[1]; + detail2.amount.Should().Be(15.0m, "no tip remaining for second card"); + detail2.tipAmountSpecified.Should().BeFalse(); + } + + [Fact] + public void MapToPaymentMethodsDoc_WithOnlyTipPayItems_ShouldReturnError() + { + var factory = CreateFactory(); + var receiptRequest = BuildRequest(new List + { + new PayItem + { + Position = 1, + Amount = -1.0m, + Description = "Tip", + ftPayItemCase = ((PayItemCase) 0x4752_2000_0000_0000).WithCase(PayItemCase.CreditCardPayment).WithFlag(PayItemCaseFlags.Tip) + } + }); + + (var doc, var error) = factory.MapToPaymentMethodsDoc(receiptRequest, 400001951868897); + + doc.Should().BeNull(); + error.Should().NotBeNull(); + error!.Exception.Should().BeOfType(); + error.Exception.Message.Should().Contain("no preceding payment"); + } + #endregion #region GeneratePaymentMethodPayload