feat: improve CreditCall receipt layout
This commit is contained in:
parent
afb8bb0e9e
commit
c8cbd1e7a3
@ -245,95 +245,174 @@ func printCardholderReceipt(cardholderReceipt string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// receiptColumns follows the existing 44-column normal-size text convention.
|
||||||
|
const receiptColumns = 44
|
||||||
|
|
||||||
func BuildCardholderReceipt(entries []ReceiptEntryXML) ([]byte, error) {
|
func BuildCardholderReceipt(entries []ReceiptEntryXML) ([]byte, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
rendered := make([]bool, len(entries))
|
||||||
|
take := func(id string) string {
|
||||||
|
for i, entry := range entries {
|
||||||
|
if !rendered[i] && entry.ReceiptEntryId == id && entry.Value != "" {
|
||||||
|
rendered[i] = true
|
||||||
|
return receiptEntryText(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
write := func(b []byte) { buf.Write(b) }
|
// Set and restore only text state; leave printer setup and transport alone.
|
||||||
// writeStr := func(s string) { buf.WriteString(s) }
|
normal := func() { buf.Write([]byte{ESC, 'a', 0, ESC, 'E', BOLD_OFF, GS, '!', NORMAL_FONT}) }
|
||||||
writeln := func(s string) {
|
normal()
|
||||||
buf.WriteString(s)
|
hasRows, newSection := false, false
|
||||||
|
beginLine := func(center bool) {
|
||||||
|
if hasRows && newSection {
|
||||||
|
buf.WriteString(strings.Repeat("-", receiptColumns) + "\n")
|
||||||
|
}
|
||||||
|
hasRows, newSection = true, false
|
||||||
|
if center {
|
||||||
|
buf.Write([]byte{ESC, 'a', CENTER})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text := func(value string, bold bool) {
|
||||||
|
if bold {
|
||||||
|
buf.Write([]byte{ESC, 'E', BOLD_ON})
|
||||||
|
}
|
||||||
|
buf.WriteString(value)
|
||||||
|
if bold {
|
||||||
|
buf.Write([]byte{ESC, 'E', BOLD_OFF})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
line := func(value string, bold, center bool) {
|
||||||
|
if value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
beginLine(center)
|
||||||
|
text(value, bold)
|
||||||
buf.WriteByte('\n')
|
buf.WriteByte('\n')
|
||||||
|
normal()
|
||||||
|
}
|
||||||
|
pair := func(left, right string, boldRight bool) {
|
||||||
|
if gap, fits := receiptPairGap(left, right); fits {
|
||||||
|
beginLine(false)
|
||||||
|
text(left+gap, false)
|
||||||
|
text(right, boldRight)
|
||||||
|
buf.WriteByte('\n')
|
||||||
|
normal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line(left, false, false)
|
||||||
|
line(right, boldRight, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a lookup map by ReceiptEntryId
|
line(take("Recipient"), true, true)
|
||||||
m := make(map[string]ReceiptEntryXML, len(entries))
|
line(take("MerchantName"), false, true)
|
||||||
for _, e := range entries {
|
newSection = true
|
||||||
m[e.ReceiptEntryId] = e
|
line(take("CardScheme"), false, false)
|
||||||
|
line(take("PanMasked"), false, false)
|
||||||
|
pair(take("Aid"), take("PanSequence"), false)
|
||||||
|
pair(take("TransactionSource"), take("TransactionType"), false)
|
||||||
|
newSection = true
|
||||||
|
// Align the provider amount without parsing or modifying its value.
|
||||||
|
for i, entry := range entries {
|
||||||
|
if entry.ReceiptEntryId == "TotalAmount" && entry.Value != "" {
|
||||||
|
rendered[i] = true
|
||||||
|
label := entry.Label
|
||||||
|
if label == "" {
|
||||||
|
label = "TOTAL"
|
||||||
}
|
}
|
||||||
|
label = strings.TrimSuffix(label, ":") + ":"
|
||||||
// Utility to center text
|
if gap, fits := receiptPairGap(label, entry.Value); fits {
|
||||||
// center := func(s string) {
|
line(label+gap+entry.Value, true, false)
|
||||||
// write([]byte{ESC, 'a', CENTER})
|
} else {
|
||||||
// writeln(s)
|
line(receiptEntryText(entry), true, false)
|
||||||
// write([]byte{ESC, 'a', 0})
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Utility for bold lines
|
|
||||||
// boldOn := func() { write([]byte{GS, '!', WIDER_FONT}) }
|
|
||||||
// boldOff := func() { write([]byte{GS, '!', NORMAL_FONT}) }
|
|
||||||
|
|
||||||
// boldOn()
|
|
||||||
|
|
||||||
// 1) Header: CARDHOLDER COPY
|
|
||||||
writeln(m["Recipient"].Value)
|
|
||||||
// writeStr("\n\n")
|
|
||||||
// 2) Merchant name
|
|
||||||
writeln(m["MerchantName"].Value)
|
|
||||||
// writeStr("\n\n")
|
|
||||||
// 3) AID
|
|
||||||
writeln("AID: " + m["Aid"].Value)
|
|
||||||
|
|
||||||
// 4) Card scheme & pan
|
|
||||||
writeln(fmt.Sprintf("%s Card: %s", m["CardScheme"].Value, m["PanMasked"].Value))
|
|
||||||
|
|
||||||
// 5) PAN Seq No
|
|
||||||
writeln("PAN Seq No: " + m["PanSequence"].Value)
|
|
||||||
|
|
||||||
// 6) Transaction source
|
|
||||||
writeln(m["TransactionSource"].Value)
|
|
||||||
|
|
||||||
// 7) Transaction type (Sale/Account Verification)
|
|
||||||
writeln(m["TransactionType"].Value)
|
|
||||||
|
|
||||||
// 8) Total
|
|
||||||
// assuming value like "GBP1.50" — insert space after currency
|
|
||||||
total := m["TotalAmount"].Value
|
|
||||||
if !strings.HasPrefix(total, "GBP") && len(total) > 3 {
|
|
||||||
total = total[:3] + " " + total[3:]
|
|
||||||
}
|
}
|
||||||
writeln("TOTAL: " + total)
|
break
|
||||||
|
}
|
||||||
// 9) Cardholder verification
|
}
|
||||||
writeln(m["CardholderVerification"].Value)
|
pair(take("CardholderVerification"), take("TransactionResult"), true)
|
||||||
|
newSection = true
|
||||||
// 10) Approved/Declined
|
line(take("AuthCode"), false, false)
|
||||||
writeln(m["TransactionResult"].Value)
|
line(take("AuthReference"), false, false)
|
||||||
|
line(take("TransactionSeqNumber"), false, false)
|
||||||
// 11) Auth code
|
pair(take("MerchantIdMasked"), take("TerminalIdMasked"), false)
|
||||||
writeln("Auth Code: " + m["AuthCode"].Value)
|
line(take("AuthDateTime"), false, false)
|
||||||
|
footer := take("RetentionMessage")
|
||||||
// 12) Reference
|
// Preserve every remaining occurrence, including unknown IDs, in provider order.
|
||||||
writeln("Ref: " + m["AuthReference"].Value)
|
for i, entry := range entries {
|
||||||
|
if !rendered[i] && entry.Value != "" {
|
||||||
// 13) Merchant & terminal IDs
|
line(receiptEntryText(entry), false, false)
|
||||||
writeln("MID: " + m["MerchantIdMasked"].Value)
|
}
|
||||||
writeln("TID: " + m["TerminalIdMasked"].Value)
|
}
|
||||||
|
newSection = true
|
||||||
// 14) Date/time
|
line(footer, false, true)
|
||||||
writeln(m["AuthDateTime"].Value)
|
normal()
|
||||||
|
buf.Write([]byte{ESC, 'd', 7})
|
||||||
// 15) Retention message
|
buf.Write([]byte{GS, 'V', 1})
|
||||||
writeln(m["RetentionMessage"].Value)
|
|
||||||
|
|
||||||
// boldOff()
|
|
||||||
|
|
||||||
// finally feed & cut
|
|
||||||
write([]byte{ESC, 'd', 7}) // Feed 5 lines
|
|
||||||
write([]byte{GS, 'V', 1})
|
|
||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func receiptEntryText(entry ReceiptEntryXML) string {
|
||||||
|
label := entry.Label
|
||||||
|
if label == "" {
|
||||||
|
switch entry.ReceiptEntryId {
|
||||||
|
case "Recipient", "MerchantName", "CardScheme", "TransactionSource", "TransactionType",
|
||||||
|
"CardholderVerification", "TransactionResult", "AuthDateTime", "RetentionMessage":
|
||||||
|
return entry.Value
|
||||||
|
case "PanMasked":
|
||||||
|
label = "Card"
|
||||||
|
case "Aid":
|
||||||
|
label = "AID"
|
||||||
|
case "PanSequence":
|
||||||
|
label = "PAN Seq No"
|
||||||
|
case "TotalAmount":
|
||||||
|
label = "TOTAL"
|
||||||
|
case "AuthCode":
|
||||||
|
label = "Auth Code"
|
||||||
|
case "AuthReference":
|
||||||
|
label = "Ref"
|
||||||
|
case "TransactionSeqNumber":
|
||||||
|
label = "Transaction Seq. No"
|
||||||
|
case "MerchantIdMasked":
|
||||||
|
label = "MID"
|
||||||
|
case "TerminalIdMasked":
|
||||||
|
label = "TID"
|
||||||
|
default:
|
||||||
|
var readable strings.Builder
|
||||||
|
runes := []rune(entry.ReceiptEntryId)
|
||||||
|
for i, r := range runes {
|
||||||
|
if i > 0 && r >= 'A' && r <= 'Z' &&
|
||||||
|
((runes[i-1] >= 'a' && runes[i-1] <= 'z') ||
|
||||||
|
(i+1 < len(runes) && runes[i+1] >= 'a' && runes[i+1] <= 'z')) {
|
||||||
|
readable.WriteByte(' ')
|
||||||
|
}
|
||||||
|
if r == '_' || r == '-' {
|
||||||
|
r = ' '
|
||||||
|
}
|
||||||
|
readable.WriteRune(r)
|
||||||
|
}
|
||||||
|
label = strings.TrimSpace(readable.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if label == "" {
|
||||||
|
return entry.Value
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(label, ":") + ": " + entry.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiptPairGap(left, right string) (string, bool) {
|
||||||
|
if left == "" || right == "" || len(left)+2+len(right) > receiptColumns {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
// Only printable ASCII has a predictable single-column width here.
|
||||||
|
for _, r := range left + right {
|
||||||
|
if r < ' ' || r > '~' {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Repeat(" ", receiptColumns-len(left)-len(right)), true
|
||||||
|
}
|
||||||
|
|
||||||
func printLogo(path string) ([]byte, error) {
|
func printLogo(path string) ([]byte, error) {
|
||||||
const maxLogoWidth = 384
|
const maxLogoWidth = 384
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
|
|||||||
280
internal/printer/receipt_test.go
Normal file
280
internal/printer/receipt_test.go
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user