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) } }) } }