001/*
002 * jPOS Project [http://jpos.org]
003 * Copyright (C) 2000-2026 jPOS Software SRL
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU Affero General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU Affero General Public License for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jpos.core;
020
021import org.jpos.iso.ISOUtil;
022
023import java.util.Objects;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027/**
028 * @author apr@jpos.org
029 * @since jPOS 2.0.5
030 *
031 * This class is based on the old 'CardHolder' class and adds support for multiple
032 * PAN and Expiration dates taken from manual entry, track1, track2. It also corrects the name.
033 */
034@SuppressWarnings("unused")
035public class Track2 {
036    private String pan;
037    private String exp;
038    private String cvv;
039    private String serviceCode;
040    private String discretionaryData;
041    private String track;
042
043    private Track2 () { }
044
045    public Track2 (Builder builder) {
046        track       = builder.track;
047        pan         = builder.pan;
048        exp         = builder.exp;
049        cvv         = builder.cvv;
050        serviceCode = builder.serviceCode;
051        discretionaryData  = builder.discretionaryData;
052    }
053
054    public String getPan() {
055        return pan;
056    }
057
058    public String getExp() {
059        return exp;
060    }
061
062    public String getCvv() {
063        return cvv;
064    }
065
066    public String getServiceCode() {
067        return serviceCode;
068    }
069
070    public String getDiscretionaryData() {
071        return discretionaryData;
072    }
073
074    public String getTrack() {
075        return track;
076    }
077
078    @Override
079    public String toString() {
080        return pan != null ? ISOUtil.protect(pan) : "nil";
081    }
082
083    @Override
084    public boolean equals(Object o) {
085        if (this == o) return true;
086        if (o == null || getClass() != o.getClass()) return false;
087        Track2 track21 = (Track2) o;
088        return Objects.equals(pan, track21.pan) &&
089          Objects.equals(exp, track21.exp) &&
090          Objects.equals(cvv, track21.cvv) &&
091          Objects.equals(serviceCode, track21.serviceCode) &&
092          Objects.equals(discretionaryData, track21.discretionaryData) &&
093          Objects.equals(track, track21.track);
094    }
095
096    @Override
097    public int hashCode() {
098        return Objects.hash(pan, exp, cvv, serviceCode, discretionaryData, track);
099    }
100
101    public static Builder builder() {
102        return new Builder();
103    }
104
105    public static class Builder {
106        private static String TRACK2_EXPR = "^([0-9]{1,19})[=D]([0-9]{4})?([0-9]{3})?([0-9]{4})?([0-9]{1,})?$";
107        private static Pattern TRACK2_PATTERN = Pattern.compile(TRACK2_EXPR);
108        private String pan;
109        private String exp;
110        private String cvv;
111        private String serviceCode;
112        private String discretionaryData;
113        private String track;
114        private Pattern pattern = TRACK2_PATTERN;
115
116        private Builder () { }
117
118        public Builder pan (String pan) {
119            this.pan = pan; return this;
120        }
121
122        public Builder exp (String exp) {
123            this.exp = exp; return this;
124        }
125
126        public Builder cvv (String cvv) {
127            this.cvv = cvv; return this;
128        }
129
130        public Builder serviceCode (String serviceCode) {
131            this.serviceCode = serviceCode; return this;
132        }
133
134        public Builder discretionaryData (String discretionaryData) {
135            this.discretionaryData = discretionaryData;
136            return this;
137        }
138
139        /**
140         * Optional method, can be used to override default pattern
141         * @param pattern overrides default pattern
142         * @return this builder
143         */
144        public Builder pattern (Pattern pattern) {
145            this.pattern = pattern;
146            return this;
147        }
148
149        public Builder track (String s)
150          throws InvalidCardException
151        {
152            if (s == null)
153                throw new InvalidCardException ("null track2 data");
154            if (s.length() > 37)
155                throw new InvalidCardException("track2 too long");
156
157            track = s;
158            Matcher matcher = pattern.matcher(s);
159            int cnt = matcher.groupCount();
160            if (matcher.find() && cnt >= 1) {
161                pan = matcher.group(1);
162                if (cnt > 1)
163                    exp = matcher.group(2);
164                if (cnt > 2)
165                    serviceCode = matcher.group(3);
166                if (cnt > 3)
167                    cvv = matcher.group(4);
168                if (cnt > 4)
169                    discretionaryData = matcher.group(5);
170            } else {
171                throw new InvalidCardException ("invalid track2");
172            }
173            return this;
174        }
175
176        /**
177         * Constructs the Track2 data based on the card data provided.
178         * The generated Track2 data is validated using the pattern.
179         * If the Track2 data doesn't match the pattern, the track attribute keeps the original value.
180         * @return this builder.
181         */
182        public Builder buildTrackData() {
183            StringBuilder track2 = new StringBuilder(this.pan);
184            track2.append("=");
185            track2.append(this.exp);
186            track2.append(this.serviceCode);
187            track2.append(this.cvv);
188            track2.append(this.discretionaryData);
189
190            Matcher matcher = this.pattern.matcher(track2);
191            int cnt = matcher.groupCount();
192            if (matcher.find() && cnt >= 1)
193                this.track = track2.toString();
194
195            return this;
196        }
197
198        public Track2 build() {
199            if (this.track == null)
200                buildTrackData();
201            return new Track2(this);
202        }
203    }
204}