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