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, track1. It also corrects the name.
031 *
032 * @author apr@jpos.org
033 * @since jPOS 2.0.5
034 *
035 */
036
037@SuppressWarnings("unused")
038public class Track1 {
039    private String pan;
040    private String nameOnCard;
041    private String exp;
042    private String serviceCode;
043    private String cvv;
044    private String discretionaryData;
045    private String track;
046
047    private Track1 () { }
048
049    /**
050     * Copies the track1 fields from the supplied {@link Builder}.
051     *
052     * @param builder builder carrying the parsed or assembled track1 fields
053     */
054    public Track1 (Builder builder) {
055        pan                = builder.pan;
056        nameOnCard         = builder.nameOnCard;
057        exp                = builder.exp;
058        cvv                = builder.cvv;
059        discretionaryData  = builder.discretionaryData;
060        serviceCode        = builder.serviceCode;
061        track              = builder.track;
062    }
063
064    /**
065     * Returns the primary account number.
066     *
067     * @return primary account number
068     */
069    public String getPan() {
070        return pan;
071    }
072
073    /**
074     * Returns the cardholder name encoded on the track.
075     *
076     * @return cardholder name as encoded on the track
077     */
078    public String getNameOnCard() {
079        return nameOnCard;
080    }
081
082    /**
083     * Returns the expiration date.
084     *
085     * @return expiration date in {@code YYMM} form, or {@code null} if absent
086     */
087    public String getExp() {
088        return exp;
089    }
090
091    /**
092     * Returns the CVV/CVC value, when present.
093     *
094     * @return CVV/CVC value, or {@code null} if not present in the track
095     */
096    public String getCvv() {
097        return cvv;
098    }
099
100    /**
101     * Returns the service code.
102     *
103     * @return three-digit service code, or {@code null} if absent
104     */
105    public String getServiceCode() {
106        return serviceCode;
107    }
108
109    /**
110     * Returns the discretionary data trailing the service code.
111     *
112     * @return remaining discretionary data, or {@code null} if absent
113     */
114    public String getDiscretionaryData() {
115        return discretionaryData;
116    }
117
118    /**
119     * Returns the raw track1 string this object was built from.
120     *
121     * @return raw track1 string this object was built from, or {@code null} when assembled programmatically
122     */
123    public String getTrack() {
124
125        return track;
126    }
127
128    /**
129     * Returns {@code true} when the service code marks this as an IC (EMV) card.
130     *
131     * @return {@code true} if the Track 1 service code indicates an IC card
132     */
133    public boolean isEMV() {
134        return isICCard();
135    }
136
137    /**
138     * Returns {@code true} when the service code marks this as an IC card
139     * (first digit {@code 2} for international or {@code 6} for national).
140     *
141     * @return {@code true} if the Track 1 service code indicates an IC card
142     */
143    public boolean isICCard() {
144        return serviceCode != null && serviceCode.length() == 3 &&
145          (serviceCode.charAt(0) == '2' || serviceCode.charAt(0) == '6');
146    }
147
148    /**
149     * Returns {@code true} when the service code marks this as an
150     * internationally-usable IC card (first digit {@code 2}).
151     *
152     * @return {@code true} if the Track 1 service code indicates an international IC card
153     */
154    public boolean isInternationalICCard() {
155        return serviceCode != null && serviceCode.length() == 3 && serviceCode.charAt(0) == '2';
156    }
157
158    @Override
159    public String toString() {
160        return pan != null ? ISOUtil.protect(pan) : "nil";
161    }
162
163    @Override
164    public boolean equals(Object o) {
165        if (this == o) return true;
166        if (o == null || getClass() != o.getClass()) return false;
167        Track1 track11 = (Track1) o;
168        return Objects.equals(pan, track11.pan) &&
169          Objects.equals(nameOnCard, track11.nameOnCard) &&
170          Objects.equals(exp, track11.exp) &&
171          Objects.equals(serviceCode, track11.serviceCode) &&
172          Objects.equals(cvv, track11.cvv) &&
173          Objects.equals(discretionaryData, track11.discretionaryData) &&
174          Objects.equals(track, track11.track);
175    }
176
177    @Override
178    public int hashCode() {
179        return Objects.hash(pan, nameOnCard, exp, serviceCode, cvv, discretionaryData, track);
180    }
181
182    /**
183     * Creates a new builder for assembling a {@code Track1}.
184     *
185     * @return a new {@link Builder} for assembling a {@code Track1}
186     */
187    public static Builder builder() {
188        return new Builder();
189    }
190
191    /**
192     * Fluent builder that parses a raw track1 string or assembles a {@code Track1}
193     * from individual fields and validates the result against a configurable pattern.
194     */
195    public static class Builder {
196        private static String TRACK1_EXPR = "^[%]?[A-Z]+([0-9]{1,19})\\^([^\\^]{2,26})\\^([0-9]{4})([0-9]{3})([0-9]{4})?([0-9]{1,10})?";
197        private static Pattern TRACK1_PATTERN = Pattern.compile(TRACK1_EXPR);
198        private String pan;
199        private String nameOnCard;
200        private String exp;
201        private String cvv;
202        private String serviceCode;
203        private String discretionaryData;
204        private String track;
205        private Pattern pattern = TRACK1_PATTERN;
206        private Builder () { }
207
208        /**
209         * Sets the primary account number.
210         *
211         * @param pan primary account number
212         * @return this builder
213         */
214        public Builder pan (String pan) {
215            this.pan = pan; return this;
216        }
217
218        /**
219         * Sets the cardholder name.
220         *
221         * @param nameOnCard cardholder name as encoded on the track
222         * @return this builder
223         */
224        public Builder nameOnCard (String nameOnCard) {
225            this.nameOnCard = nameOnCard;
226            return this;
227        }
228
229        /**
230         * Sets the expiration date.
231         *
232         * @param exp expiration date in {@code YYMM} form
233         * @return this builder
234         */
235        public Builder exp (String exp) {
236            this.exp = exp; return this;
237        }
238
239        /**
240         * Sets the CVV/CVC value.
241         *
242         * @param cvv CVV/CVC value
243         * @return this builder
244         */
245        public Builder cvv (String cvv) {
246            this.cvv = cvv; return this;
247        }
248
249        /**
250         * Sets the service code.
251         *
252         * @param serviceCode three-digit service code
253         * @return this builder
254         */
255        public Builder serviceCode (String serviceCode) {
256            this.serviceCode = serviceCode; return this;
257        }
258
259        /**
260         * Sets the discretionary data trailing the service code.
261         *
262         * @param discretionaryData discretionary data trailing the service code
263         * @return this builder
264         */
265        public Builder discretionaryData (String discretionaryData) {
266            this.discretionaryData = discretionaryData;
267            return this;
268        }
269
270        /**
271         * Optional method, can be used to override default pattern
272         * @param pattern overrides default pattern
273         * @return this builder
274         */
275        public Builder pattern (Pattern pattern) {
276            this.pattern = pattern;
277            return this;
278        }
279
280        /**
281         * Parses a raw track1 string and populates the builder fields.
282         *
283         * @param s raw track1 data
284         * @return this builder
285         * @throws InvalidCardException if {@code s} is null or does not match
286         *                              the configured pattern
287         */
288        public Builder track (String s)
289          throws InvalidCardException
290        {
291            if (s == null)
292                throw new InvalidCardException ("null track1 data");
293
294            track = s;
295            Matcher matcher = pattern.matcher(s);
296            int cnt = matcher.groupCount();
297            if (matcher.find() && cnt >= 2) {
298                pan = matcher.group(1);
299                nameOnCard = matcher.group(2);
300                if (cnt > 2)
301                    exp = matcher.group(3);
302                if (cnt > 3)
303                    serviceCode = matcher.group(4);
304                if (cnt > 4)
305                    cvv = matcher.group(5);
306                if (cnt > 5)
307                    discretionaryData = matcher.group(6);
308            } else {
309                throw new InvalidCardException ("invalid track1");
310            }
311            return this;
312        }
313
314        /**
315         * Constructs the Track1 data based on the card data provided.
316         * The generated Track1 data is validated using the pattern.
317         * If the Track1 data doesn't match the pattern, the track attribute keeps the original value.
318         * @return this builder.
319         */
320        public Builder buildTrackData() {
321            StringBuilder track1 = new StringBuilder("%");
322            track1.append("B");
323            track1.append(this.pan);
324            track1.append("^");
325            track1.append(this.nameOnCard);
326            track1.append("^");
327            track1.append(this.exp);
328            track1.append(this.serviceCode);
329            track1.append(this.cvv);
330            track1.append(this.discretionaryData);
331
332            Matcher matcher = this.pattern.matcher(track1);
333            int cnt = matcher.groupCount();
334            if (matcher.find() && cnt >= 2)
335                this.track = track1.toString();
336
337            return this;
338        }
339
340        /**
341         * Builds the immutable {@link Track1}. If no raw track string was set,
342         * one is assembled from the individual fields via {@link #buildTrackData()}.
343         *
344         * @return the built {@link Track1}
345         */
346        public Track1 build() {
347            if (this.track == null)
348                buildTrackData();
349            return new Track1(this);
350        }
351    }
352}