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 *
029 * This class is based on the old 'CardHolder' class and adds support for multiple
030 * PAN and Expiration dates taken from manual entry, track1, track2. It also corrects the name.
031 *
032 * @author apr@jpos.org
033 * @since jPOS 2.0.5
034 *
035 */
036@SuppressWarnings("unused")
037public class Track2 {
038    private String pan;
039    private String exp;
040    private String cvv;
041    private String serviceCode;
042    private String discretionaryData;
043    private String track;
044
045    private Track2 () { }
046
047    /**
048     * Copies the track2 fields from the supplied {@link Builder}.
049     *
050     * @param builder builder carrying the parsed or assembled track2 fields
051     */
052    public Track2 (Builder builder) {
053        track       = builder.track;
054        pan         = builder.pan;
055        exp         = builder.exp;
056        cvv         = builder.cvv;
057        serviceCode = builder.serviceCode;
058        discretionaryData  = builder.discretionaryData;
059    }
060
061    /**
062     * Returns the primary account number.
063     *
064     * @return primary account number
065     */
066    public String getPan() {
067        return pan;
068    }
069
070    /**
071     * Returns the expiration date.
072     *
073     * @return expiration date in {@code YYMM} form, or {@code null} if absent
074     */
075    public String getExp() {
076        return exp;
077    }
078
079    /**
080     * Returns the CVV/CVC value, when present.
081     *
082     * @return CVV/CVC value, or {@code null} if not present in the track
083     */
084    public String getCvv() {
085        return cvv;
086    }
087
088    /**
089     * Returns the service code.
090     *
091     * @return three-digit service code, or {@code null} if absent
092     */
093    public String getServiceCode() {
094        return serviceCode;
095    }
096
097    /**
098     * Returns the discretionary data trailing the service code.
099     *
100     * @return remaining discretionary data trailing the service code, or {@code null}
101     */
102    public String getDiscretionaryData() {
103        return discretionaryData;
104    }
105
106    /**
107     * Returns the raw track2 string this object was built from.
108     *
109     * @return raw track2 string this object was built from, or {@code null} when assembled programmatically
110     */
111    public String getTrack() {
112        return track;
113    }
114
115    @Override
116    public String toString() {
117        return pan != null ? ISOUtil.protect(pan) : "nil";
118    }
119
120    @Override
121    public boolean equals(Object o) {
122        if (this == o) return true;
123        if (o == null || getClass() != o.getClass()) return false;
124        Track2 track21 = (Track2) o;
125        return Objects.equals(pan, track21.pan) &&
126          Objects.equals(exp, track21.exp) &&
127          Objects.equals(cvv, track21.cvv) &&
128          Objects.equals(serviceCode, track21.serviceCode) &&
129          Objects.equals(discretionaryData, track21.discretionaryData) &&
130          Objects.equals(track, track21.track);
131    }
132
133    @Override
134    public int hashCode() {
135        return Objects.hash(pan, exp, cvv, serviceCode, discretionaryData, track);
136    }
137
138    /**
139     * Creates a new builder for assembling a {@code Track2}.
140     *
141     * @return a new {@link Builder} for assembling a {@code Track2}
142     */
143    public static Builder builder() {
144        return new Builder();
145    }
146
147    /**
148     * Fluent builder that parses a raw track2 string or assembles a {@code Track2}
149     * from individual fields and validates the result against a configurable pattern.
150     */
151    public static class Builder {
152        private static String TRACK2_EXPR = "^([0-9]{1,19})[=D]([0-9]{4})?([0-9]{3})?([0-9]{4})?([0-9]{1,})?$";
153        private static Pattern TRACK2_PATTERN = Pattern.compile(TRACK2_EXPR);
154        private String pan;
155        private String exp;
156        private String cvv;
157        private String serviceCode;
158        private String discretionaryData;
159        private String track;
160        private Pattern pattern = TRACK2_PATTERN;
161
162        private Builder () { }
163
164        /**
165         * Sets the primary account number.
166         *
167         * @param pan primary account number
168         * @return this builder
169         */
170        public Builder pan (String pan) {
171            this.pan = pan; return this;
172        }
173
174        /**
175         * Sets the expiration date.
176         *
177         * @param exp expiration date in {@code YYMM} form
178         * @return this builder
179         */
180        public Builder exp (String exp) {
181            this.exp = exp; return this;
182        }
183
184        /**
185         * Sets the CVV/CVC value.
186         *
187         * @param cvv CVV/CVC value
188         * @return this builder
189         */
190        public Builder cvv (String cvv) {
191            this.cvv = cvv; return this;
192        }
193
194        /**
195         * Sets the service code.
196         *
197         * @param serviceCode three-digit service code
198         * @return this builder
199         */
200        public Builder serviceCode (String serviceCode) {
201            this.serviceCode = serviceCode; return this;
202        }
203
204        /**
205         * Sets the discretionary data trailing the service code.
206         *
207         * @param discretionaryData discretionary data trailing the service code
208         * @return this builder
209         */
210        public Builder discretionaryData (String discretionaryData) {
211            this.discretionaryData = discretionaryData;
212            return this;
213        }
214
215        /**
216         * Optional method, can be used to override default pattern
217         * @param pattern overrides default pattern
218         * @return this builder
219         */
220        public Builder pattern (Pattern pattern) {
221            this.pattern = pattern;
222            return this;
223        }
224
225        /**
226         * Parses a raw track2 string and populates the builder fields.
227         *
228         * @param s raw track2 data
229         * @return this builder
230         * @throws InvalidCardException if {@code s} is null, exceeds 37 characters,
231         *                              or does not match the configured pattern
232         */
233        public Builder track (String s)
234          throws InvalidCardException
235        {
236            if (s == null)
237                throw new InvalidCardException ("null track2 data");
238            if (s.length() > 37)
239                throw new InvalidCardException("track2 too long");
240
241            track = s;
242            Matcher matcher = pattern.matcher(s);
243            int cnt = matcher.groupCount();
244            if (matcher.find() && cnt >= 1) {
245                pan = matcher.group(1);
246                if (cnt > 1)
247                    exp = matcher.group(2);
248                if (cnt > 2)
249                    serviceCode = matcher.group(3);
250                if (cnt > 3)
251                    cvv = matcher.group(4);
252                if (cnt > 4)
253                    discretionaryData = matcher.group(5);
254            } else {
255                throw new InvalidCardException ("invalid track2");
256            }
257            return this;
258        }
259
260        /**
261         * Constructs the Track2 data based on the card data provided.
262         * The generated Track2 data is validated using the pattern.
263         * If the Track2 data doesn't match the pattern, the track attribute keeps the original value.
264         * @return this builder.
265         */
266        public Builder buildTrackData() {
267            StringBuilder track2 = new StringBuilder(this.pan);
268            track2.append("=");
269            track2.append(this.exp);
270            track2.append(this.serviceCode);
271            track2.append(this.cvv);
272            track2.append(this.discretionaryData);
273
274            Matcher matcher = this.pattern.matcher(track2);
275            int cnt = matcher.groupCount();
276            if (matcher.find() && cnt >= 1)
277                this.track = track2.toString();
278
279            return this;
280        }
281
282        /**
283         * Builds the immutable {@link Track2}. If no raw track string was set,
284         * one is assembled from the individual fields via {@link #buildTrackData()}.
285         *
286         * @return the built {@link Track2}
287         */
288        public Track2 build() {
289            if (this.track == null)
290                buildTrackData();
291            return new Track2(this);
292        }
293    }
294}