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.ISODate;
022import org.jpos.iso.ISOException;
023import org.jpos.iso.ISOMsg;
024import org.jpos.util.Loggeable;
025
026import java.io.PrintStream;
027import java.io.Serializable;
028import java.util.Date;
029import java.util.StringTokenizer;
030
031/**
032 * @author apr@cs.com.uy
033 * @since jPOS 1.1
034 *
035 * This class is called 'CardHolder', but a better name could have been 'Card'
036 * At some point we'll deprecate this one and create a new 'Card' class.
037 */
038public class CardHolder implements Cloneable, Serializable, Loggeable {
039    private static final long serialVersionUID = 7449770625551878435L;
040    private static final String TRACK1_SEPARATOR = "^";
041    private static final char TRACK2_SEPARATOR = '=';
042    private static final int  BINLEN           =  6;
043    private static final int  MINPANLEN        = 10;
044    /**
045     * Primary Account Number
046     * @serial
047     */
048    protected String pan;
049    /**
050     * Expiration date (YYMM)
051     * @serial
052     */
053    protected String exp;
054    /**
055     * Track2 trailler
056     * @serial
057     */
058    protected String trailer;
059    /**
060     * Optional security code (CVC, CVV, Locale ID, wse)
061     * @serial
062     */
063    protected String securityCode;
064    /**
065     * Track1 Data
066     * @serial
067     */
068    protected String track1;
069
070    /**
071     * creates an empty CardHolder
072     */
073    public CardHolder() {
074        super();
075    }
076
077    /**
078     * creates a new CardHolder based on track2
079     * @param track2 cards track2
080     * @exception InvalidCardException
081     */
082    public CardHolder (String track2)
083        throws InvalidCardException
084    {
085        super();
086        parseTrack2 (track2);
087    }
088
089    /**
090     * creates a new CardHolder based on pan and exp
091     * @exception InvalidCardException
092     */
093    public CardHolder (String pan, String exp)
094        throws InvalidCardException
095    {
096        super();
097        setPAN (pan);
098        setEXP (exp);
099    }
100
101    /**
102     * Construct a CardHolder based on content received on
103     * field 35 (track2) or field 2 (PAN) + field 14 (EXP)
104     * @param m an ISOMsg
105     * @throws InvalidCardException
106     */
107    public CardHolder (ISOMsg m)
108        throws InvalidCardException
109    {
110        super();
111        if (m.hasField(35))
112            parseTrack2((String) m.getValue(35));
113        else if (m.hasField(2)) {
114            setPAN((String) m.getValue(2));
115            if (m.hasField(14))
116                setEXP((String) m.getValue(14));
117        } else {
118            throw new InvalidCardException("required fields not present");
119        }
120        if (m.hasField(45)) {
121            setTrack1((String) m.getValue(45));
122        }
123        if (m.hasField(55)) {
124            setSecurityCode(m.getString(55));
125        }
126    }
127
128    /**
129     * extract pan/exp/trailler from track2
130     * @param s a valid track2
131     * @exception InvalidCardException
132     */
133    public void parseTrack2 (String s)
134        throws InvalidCardException
135    {
136        if (s == null)
137            throw new InvalidCardException ("null track2 data");
138        int separatorIndex = s.replace ('D','=').indexOf(TRACK2_SEPARATOR);
139        if (separatorIndex > 0 && s.length() > separatorIndex+4) {
140            pan = s.substring(0, separatorIndex);
141            exp = s.substring(separatorIndex+1, separatorIndex+1+4);
142            trailer = s.substring(separatorIndex+1+4);
143        } else
144            throw new InvalidCardException ("Invalid track2 format");
145    }
146
147    /**
148     * @param track1 card's track1
149     */
150    public void setTrack1(String track1) {
151        this.track1 = track1;
152    }
153
154    /**
155     * @return the track1
156     */
157    public String getTrack1() {
158        return track1;
159    }
160
161    /**
162     * @return true if we have a track1
163     */
164    public boolean hasTrack1() {
165        return track1!=null;
166    }
167
168    /**
169     * @return the Name written on the card (from track1)
170     */
171    public String getNameOnCard() {
172        String name = null;
173        if (track1!=null) {
174            StringTokenizer st =
175                    new StringTokenizer(track1, TRACK1_SEPARATOR);
176            if (st.countTokens()<2)
177                return null;
178            st.nextToken(); // Skips the first token
179            name = st.nextToken(); // This is the name
180        }
181        return name;
182    }
183
184    /**
185     * @return reconstructed track2 or null
186     */
187    public String getTrack2() {
188        if (hasTrack2())
189            return pan + TRACK2_SEPARATOR + exp + trailer;
190        else
191            return null;
192    }
193    /**
194     * @return true if we have a (may be valid) track2
195     */
196    public boolean hasTrack2() {
197        return pan != null && exp != null && trailer != null;
198    }
199
200    /**
201     * assigns securityCode to this CardHolder object
202     * @param securityCode Card's security code
203     */
204    public void setSecurityCode(String securityCode) {
205        this.securityCode = securityCode;
206    }
207    /**
208     * @return securityCode (or null)
209     */
210    public String getSecurityCode() {
211        return securityCode;
212    }
213    /**
214     * @return true if we have a security code
215     */
216    public boolean hasSecurityCode() {
217        return securityCode != null;
218    }
219    /**
220     * @deprecated use getTrailer()
221     * @return trailer (may be null)
222     */
223    @SuppressWarnings("unused")
224    @Deprecated
225    public String getTrailler() {
226        return trailer;
227    }
228    /**
229     * Set Card's trailer
230     * @deprecated use setTrailer
231     * @param trailer Card's trailer
232     */
233    @SuppressWarnings("unused")
234    @Deprecated
235    public void setTrailler (String trailer) {
236        this.trailer = trailer;
237    }
238
239    public String getTrailer() {
240        return trailer;
241    }
242
243    public void setTrailer(String trailer) {
244        this.trailer = trailer;
245    }
246
247    /**
248     * Sets Primary Account Number
249     * @param pan Primary Account NUmber
250     * @exception InvalidCardException
251     */
252    public void setPAN (String pan)
253        throws InvalidCardException
254    {
255        if (pan.length() < MINPANLEN)
256            throw new InvalidCardException ("PAN length smaller than min required");
257        this.pan = pan;
258    }
259
260    /**
261     * @return Primary Account Number
262     */
263    public String getPAN () {
264        return pan;
265    }
266
267
268    /**
269     * Get the first <code>len</code> digits from the PAN.
270     * Can be used for the newer 8-digit BINs, or some arbitrary length.
271     * @return <code>len</code>-digit bank issuer number
272     */
273    public String getBIN (int len) {
274        return pan.substring(0, len);
275    }
276
277    /**
278     * Get the traditional 6-digit BIN (Bank Issuer Number) from the PAN
279     * @return 6-digit bank issuer number
280     */
281    public String getBIN () {
282        return getBIN(BINLEN);
283    }
284
285    /**
286     * Set Expiration Date
287     * @param exp card expiration date
288     * @exception InvalidCardException
289     */
290    public void setEXP (String exp)
291        throws InvalidCardException
292    {
293        if (exp.length() != 4)
294            throw new InvalidCardException ("Invalid Exp length, must be 4");
295        this.exp = exp;
296    }
297
298    /**
299     * Get Expiration Date
300     * @return card expiration date
301     */
302    public String getEXP () {
303        return exp;
304    }
305
306    /**
307     * Y2K compliant expiration check
308     * @return true if card is expired (or expiration is invalid)
309     */
310    public boolean isExpired () {
311        return isExpired(new Date());
312    }
313
314    /**
315     * Y2K compliant expiration check
316     * @param currentDate current system's date
317     * @return true if card is expired (or expiration is invalid)
318     */
319    public boolean isExpired(Date currentDate) {
320        if (exp == null || exp.length() != 4)
321            return true;
322        String now = ISODate.formatDate(currentDate, "yyyyMM");
323        try {
324            int mm = Integer.parseInt(exp.substring(2));
325            int aa = Integer.parseInt(exp.substring(0,2));
326            if (aa < 100 && mm > 0 && mm <= 12) {
327                String expDate = (aa < 70 ? "20" : "19") + exp;
328                if (expDate.compareTo(now) >= 0)
329                    return false;
330            }
331        } catch (NumberFormatException ignored) {
332            // NOPMD
333        }
334        return true;
335    }
336    public boolean isValidCRC () {
337        return isValidCRC(this.pan);
338    }
339    public static boolean isValidCRC (String p) {
340        int i, crc;
341
342        int odd = p.length() % 2;
343
344        for (i=crc=0; i<p.length(); i++) {
345            char c = p.charAt(i);
346            if (!Character.isDigit (c))
347                return false;
348            c = (char) (c - '0');
349            if (i % 2 == odd)
350                crc+= c*2 >= 10 ? c*2 -9 : c*2;
351            else
352                crc+=c;
353        }
354        return crc % 10 == 0;
355    }
356
357    /**
358     * dumps CardHolder basic information<br>
359     * by default we do not dump neither track1/2 nor securityCode
360     * for security reasons.
361     * @param p a PrintStream usually suplied by Logger
362     * @param indent ditto
363     * @see org.jpos.util.Loggeable
364     */
365    public void dump (PrintStream p, String indent) {
366        p.print (indent + "<CardHolder");
367        if (hasTrack1())
368            p.print (" trk1=\"true\"");
369
370        if (hasTrack2())
371            p.print (" trk2=\"true\"");
372
373        if (hasSecurityCode())
374            p.print (" sec=\"true\"");
375
376        if (isExpired())
377            p.print (" expired=\"true\"");
378
379        p.println (">");
380        p.println (indent + "  " + "<pan>" +pan +"</pan>");
381        p.println (indent + "  " + "<exp>" +exp +"</exp>");
382        p.println (indent + "</CardHolder>");
383    }
384
385    /**
386     * @return ServiceCode (if available) or a String with three blanks
387     */
388    public String getServiceCode () {
389        return trailer != null && trailer.length() >= 3 ?
390            trailer.substring (0, 3) :
391            "   ";
392    }
393    public boolean seemsManualEntry() {
394        return trailer == null || trailer.trim().length() == 0;
395    }
396
397    @Override
398    public int hashCode() {
399        final int prime = 31;
400        int result = 1;
401        result = prime * result + (exp == null ? 0 : exp.hashCode());
402        result = prime * result + (pan == null ? 0 : pan.hashCode());
403        return result;
404    }
405
406    @Override
407    public boolean equals(Object obj) {
408        if (this == obj)
409            return true;
410        if (obj == null)
411            return false;
412        if (getClass() != obj.getClass())
413            return false;
414        CardHolder other = (CardHolder) obj;
415        if (exp == null) {
416            if (other.exp != null)
417                return false;
418        } else if (!exp.equals(other.exp))
419            return false;
420        if (pan == null) {
421            if (other.pan != null)
422                return false;
423        } else if (!pan.equals(other.pan))
424            return false;
425        return true;
426    }
427}