281 lines
11 KiB
Go
281 lines
11 KiB
Go
package printer
|
|
|
|
import (
|
|
"bytes"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type receiptTestChar struct {
|
|
value byte
|
|
center, bold bool
|
|
}
|
|
|
|
type receiptTestOutput struct {
|
|
lines []string
|
|
chars []receiptTestChar
|
|
feeds, cuts int
|
|
}
|
|
|
|
// readReceiptText understands only the commands emitted by this receipt builder.
|
|
func readReceiptText(t *testing.T, data []byte) receiptTestOutput {
|
|
t.Helper()
|
|
var out receiptTestOutput
|
|
var line strings.Builder
|
|
var center, bold bool
|
|
for i := 0; i < len(data); {
|
|
if data[i] == ESC || data[i] == GS {
|
|
if i+2 >= len(data) {
|
|
t.Fatalf("BuildCardholderReceipt control bytes = %v, want complete command", data[i:])
|
|
}
|
|
prefix, command, value := data[i], data[i+1], data[i+2]
|
|
switch {
|
|
case prefix == ESC && command == 'a':
|
|
if value != 0 && value != CENTER {
|
|
t.Errorf("receipt alignment = %d, want left or center", value)
|
|
}
|
|
center = value == CENTER
|
|
case prefix == ESC && command == 'E':
|
|
if value != BOLD_OFF && value != BOLD_ON {
|
|
t.Errorf("receipt bold = %d, want on or off", value)
|
|
}
|
|
bold = value == BOLD_ON
|
|
case prefix == GS && command == '!':
|
|
if value != NORMAL_FONT {
|
|
t.Errorf("receipt size = %d, want normal size", value)
|
|
}
|
|
case prefix == ESC && command == 'd':
|
|
out.feeds++
|
|
if value != 7 || center || bold || i != len(data)-6 {
|
|
t.Errorf("receipt feed = %d, center=%t bold=%t offset=%d, want final seven-line feed in normal state", value, center, bold, i)
|
|
}
|
|
case prefix == GS && command == 'V':
|
|
out.cuts++
|
|
if value != 1 || i != len(data)-3 {
|
|
t.Errorf("receipt cut = %d at %d, want final GS V 1", value, i)
|
|
}
|
|
default:
|
|
t.Fatalf("BuildCardholderReceipt command = %v, want supported text command", data[i:i+3])
|
|
}
|
|
i += 3
|
|
continue
|
|
}
|
|
if data[i] == '\n' {
|
|
out.lines = append(out.lines, line.String())
|
|
line.Reset()
|
|
} else {
|
|
line.WriteByte(data[i])
|
|
out.chars = append(out.chars, receiptTestChar{value: data[i], center: center, bold: bold})
|
|
}
|
|
i++
|
|
}
|
|
if line.Len() != 0 || center || bold || out.feeds != 1 || out.cuts != 1 {
|
|
t.Errorf("receipt termination: trailing=%q center=%t bold=%t feeds=%d cuts=%d, want normal state and one feed/cut", line.String(), center, bold, out.feeds, out.cuts)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func buildReceiptText(t *testing.T, entries []ReceiptEntryXML) receiptTestOutput {
|
|
t.Helper()
|
|
before := append([]ReceiptEntryXML(nil), entries...)
|
|
data, err := BuildCardholderReceipt(entries)
|
|
if err != nil {
|
|
t.Fatalf("BuildCardholderReceipt(%v) error = %v, want nil", entries, err)
|
|
}
|
|
if !reflect.DeepEqual(entries, before) {
|
|
t.Errorf("BuildCardholderReceipt input = %v, want unchanged %v", entries, before)
|
|
}
|
|
if !bytes.HasSuffix(data, []byte{ESC, 'd', 7, GS, 'V', 1}) {
|
|
t.Errorf("BuildCardholderReceipt suffix = %v, want unchanged feed/cut", data)
|
|
}
|
|
return readReceiptText(t, data)
|
|
}
|
|
|
|
func sampleReceipt(recipient string) []ReceiptEntryXML {
|
|
return []ReceiptEntryXML{
|
|
{ReceiptEntryId: "Recipient", Value: recipient},
|
|
{ReceiptEntryId: "MerchantName", Value: "Standard Test Merchant"},
|
|
{ReceiptEntryId: "Aid", Value: "A0000000031010", Label: "AID"},
|
|
{ReceiptEntryId: "CardScheme", Value: "VISA DEBIT"},
|
|
{ReceiptEntryId: "PanMasked", Value: "************1133", Label: "Card"},
|
|
{ReceiptEntryId: "PanSequence", Value: "01", Label: "PAN Seq No"},
|
|
{ReceiptEntryId: "TransactionSource", Value: "ICC"},
|
|
{ReceiptEntryId: "TransactionType", Value: "SALE"},
|
|
{ReceiptEntryId: "TotalAmount", Value: "GBP120.00", Label: "TOTAL"},
|
|
{ReceiptEntryId: "CardholderVerification", Value: "PIN VERIFIED"},
|
|
{ReceiptEntryId: "TransactionResult", Value: "APPROVED"},
|
|
{ReceiptEntryId: "AuthCode", Value: "D0A35D", Label: "Auth Code"},
|
|
{ReceiptEntryId: "AuthReference", Value: "ChipDNA-Sale-20260908144731", Label: "Ref"},
|
|
{ReceiptEntryId: "MerchantIdMasked", Value: "******7897", Label: "MID"},
|
|
{ReceiptEntryId: "TerminalIdMasked", Value: "****9508", Label: "TID"},
|
|
{ReceiptEntryId: "TransactionSeqNumber", Value: "00001059", Label: "Transaction Seq. No"},
|
|
{ReceiptEntryId: "AuthDateTime", Value: "08/09/2026 14:48:06"},
|
|
{ReceiptEntryId: "RetentionMessage", Value: "Please retain for your records"},
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptCompleteLayout(t *testing.T) {
|
|
for _, recipient := range []string{"CARDHOLDER COPY", "MERCHANT COPY"} {
|
|
t.Run(recipient, func(t *testing.T) {
|
|
entries := sampleReceipt(recipient)
|
|
out := buildReceiptText(t, entries)
|
|
// Ignore padding used for left/right placement, not semantic row order.
|
|
var rows []string
|
|
for _, line := range out.lines {
|
|
rows = append(rows, strings.Join(strings.Fields(line), " "))
|
|
}
|
|
rule := strings.Repeat("-", 44)
|
|
want := []string{recipient, "Standard Test Merchant", rule, "VISA DEBIT", "Card: ************1133",
|
|
"AID: A0000000031010 PAN Seq No: 01", "ICC SALE", rule, "TOTAL: GBP120.00", "PIN VERIFIED APPROVED", rule,
|
|
"Auth Code: D0A35D", "Ref: ChipDNA-Sale-20260908144731", "Transaction Seq. No: 00001059",
|
|
"MID: ******7897 TID: ****9508", "08/09/2026 14:48:06", rule, "Please retain for your records"}
|
|
if !reflect.DeepEqual(rows, want) {
|
|
t.Errorf("BuildCardholderReceipt(%q) rows = %q, want %q", recipient, rows, want)
|
|
}
|
|
for _, entry := range entries {
|
|
if !strings.Contains(strings.Join(out.lines, "\n"), entry.Value) {
|
|
t.Errorf("BuildCardholderReceipt(%q) lost %s=%q", recipient, entry.ReceiptEntryId, entry.Value)
|
|
}
|
|
}
|
|
for _, line := range out.lines {
|
|
if line == "" || len(line) > 44 {
|
|
t.Errorf("BuildCardholderReceipt(%q) row = %q, want compact nonempty row within 44 columns", recipient, line)
|
|
}
|
|
}
|
|
if len(out.lines) > 19 {
|
|
t.Errorf("BuildCardholderReceipt(%q) rows = %d, want representative receipt at most 19", recipient, len(out.lines))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptFormattingState(t *testing.T) {
|
|
out := buildReceiptText(t, sampleReceipt("CARDHOLDER COPY"))
|
|
var plain strings.Builder
|
|
for _, char := range out.chars {
|
|
plain.WriteByte(char.value)
|
|
}
|
|
for _, tc := range []struct {
|
|
text string
|
|
center, bold bool
|
|
}{
|
|
{"CARDHOLDER COPY", true, true}, {"Standard Test Merchant", true, false},
|
|
{"VISA DEBIT", false, false}, {"TOTAL:", false, true}, {"GBP120.00", false, true},
|
|
{"PIN VERIFIED", false, false}, {"APPROVED", false, true}, {"Auth Code:", false, false},
|
|
{"Please retain for your records", true, false},
|
|
} {
|
|
start := strings.Index(plain.String(), tc.text)
|
|
if start < 0 {
|
|
t.Fatalf("receipt text lacks %q", tc.text)
|
|
}
|
|
for _, char := range out.chars[start : start+len(tc.text)] {
|
|
if char.center != tc.center || char.bold != tc.bold {
|
|
t.Errorf("receipt style for %q = center:%t bold:%t, want center:%t bold:%t", tc.text, char.center, char.bold, tc.center, tc.bold)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptMissingFields(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
entries []ReceiptEntryXML
|
|
want []string
|
|
}{
|
|
{name: "empty"},
|
|
{name: "empty values", entries: []ReceiptEntryXML{{ReceiptEntryId: "Aid", Label: "AID"}, {ReceiptEntryId: "Unknown", Label: "Unused"}}},
|
|
{name: "one partner", entries: []ReceiptEntryXML{{ReceiptEntryId: "PanSequence", Value: "02"}}, want: []string{"PAN Seq No: 02"}},
|
|
{name: "result only", entries: []ReceiptEntryXML{{ReceiptEntryId: "TransactionResult", Value: "DECLINED"}}, want: []string{"DECLINED"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
out := buildReceiptText(t, tc.entries)
|
|
if !reflect.DeepEqual(out.lines, tc.want) {
|
|
t.Errorf("BuildCardholderReceipt(%v) rows = %q, want %q", tc.entries, out.lines, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptPreservesRemainingEntries(t *testing.T) {
|
|
entries := []ReceiptEntryXML{
|
|
{ReceiptEntryId: "AuthCode", Value: ""},
|
|
{ReceiptEntryId: "FooBar", Value: "first-extra", Priority: "9"},
|
|
{ReceiptEntryId: "AuthCode", Value: "primary", Label: "Provider authorization"},
|
|
{ReceiptEntryId: "AuthCode", Value: "duplicate", Label: "Second authorization"},
|
|
{ReceiptEntryId: "FooBar", Value: "second-extra", Label: "Foo", Priority: "1"},
|
|
{ReceiptEntryId: "RetentionMessage", Value: "Retain this"},
|
|
{ReceiptEntryId: "RetentionMessage", Value: "Extra retention"},
|
|
{ReceiptEntryId: "HTTPCode", Value: "provider-code"},
|
|
{Value: "unlabelled-value"},
|
|
}
|
|
out := buildReceiptText(t, entries)
|
|
want := []string{"Provider authorization: primary", "Foo Bar: first-extra", "Second authorization: duplicate",
|
|
"Foo: second-extra", "Extra retention", "HTTP Code: provider-code", "unlabelled-value", strings.Repeat("-", 44), "Retain this"}
|
|
if !reflect.DeepEqual(out.lines, want) {
|
|
t.Errorf("BuildCardholderReceipt remaining rows = %q, want %q", out.lines, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptPairBoundaries(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, left, right string
|
|
paired bool
|
|
}{
|
|
{"short", "ICC", "SALE", true},
|
|
{"exact width", strings.Repeat("L", 20), strings.Repeat("R", 22), true},
|
|
{"overflow", strings.Repeat("L", 21), strings.Repeat("R", 22), false},
|
|
{"unicode", "TAP \u00c9", "SALE", false},
|
|
{"multiline", "ICC\nFALLBACK", "SALE", false},
|
|
{"left missing", "", "SALE", false},
|
|
{"right missing", "ICC", "", false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
out := buildReceiptText(t, []ReceiptEntryXML{{ReceiptEntryId: "TransactionSource", Value: tc.left}, {ReceiptEntryId: "TransactionType", Value: tc.right}})
|
|
var want []string
|
|
if tc.paired {
|
|
want = []string{tc.left + strings.Repeat(" ", 44-len(tc.left)-len(tc.right)) + tc.right}
|
|
} else {
|
|
if tc.left != "" {
|
|
want = append(want, strings.Split(tc.left, "\n")...)
|
|
}
|
|
if tc.right != "" {
|
|
want = append(want, tc.right)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(out.lines, want) {
|
|
t.Errorf("BuildCardholderReceipt pair(%q,%q) rows = %q, want %q", tc.left, tc.right, out.lines, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptLongValuesAndLabels(t *testing.T) {
|
|
ref := strings.Repeat("long-reference-", 8)
|
|
verify := "CONSUMER DEVICE VERIFICATION WITH ADDITIONAL PROVIDER TEXT"
|
|
out := buildReceiptText(t, []ReceiptEntryXML{
|
|
{ReceiptEntryId: "CardholderVerification", Value: verify},
|
|
{ReceiptEntryId: "TransactionResult", Value: "DECLINED"},
|
|
{ReceiptEntryId: "AuthReference", Label: "Provider reference:", Value: ref},
|
|
{ReceiptEntryId: "MerchantIdMasked", Label: strings.Repeat("Merchant", 6), Value: "******7897"},
|
|
{ReceiptEntryId: "TerminalIdMasked", Value: "****9508"},
|
|
})
|
|
want := []string{verify, "DECLINED", strings.Repeat("-", 44), "Provider reference: " + ref,
|
|
strings.Repeat("Merchant", 6) + ": ******7897", "TID: ****9508"}
|
|
if !reflect.DeepEqual(out.lines, want) {
|
|
t.Errorf("BuildCardholderReceipt long fields rows = %q, want %q", out.lines, want)
|
|
}
|
|
}
|
|
|
|
func TestBuildCardholderReceiptProviderAmounts(t *testing.T) {
|
|
for _, amount := range []string{"GBP120.00", "EUR 42,50", "USD1.23", "amount supplied verbatim", strings.Repeat("9", 50)} {
|
|
t.Run(amount, func(t *testing.T) {
|
|
out := buildReceiptText(t, []ReceiptEntryXML{{ReceiptEntryId: "TotalAmount", Label: "Provider total", Value: amount}})
|
|
if len(out.lines) != 1 || !strings.HasPrefix(out.lines[0], "Provider total:") || !strings.HasSuffix(out.lines[0], amount) {
|
|
t.Errorf("BuildCardholderReceipt amount(%q) rows = %q, want provider label and unchanged amount", amount, out.lines)
|
|
}
|
|
})
|
|
}
|
|
}
|