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.ISOMsg;
023import org.jpos.iso.ISOUtil;
024
025import java.math.BigInteger;
026import java.util.Date;
027import java.util.Objects;
028
029/**
030 * @author apr@jpos.org
031 * @since jPOS 2.0.5
032 *
033 * This class is based on the old 'CardHolder' class and adds support for multiple
034 * PAN and Expiration dates taken from manual entry, track1, track2. It also corrects the name.
035 */
036public class Card {
037    private String pan;
038    private String exp;
039    private String cvv2;
040    private String serviceCode;
041    private Track1 track1;
042    private Track2 track2;
043    public static final int BINLEN = 6;
044
045    private Card() { }
046
047    public Card(Builder builder) {
048        pan         = builder.pan;
049        exp         = builder.exp;
050        cvv2        = builder.cvv2;
051        serviceCode = builder.serviceCode;
052        track1      = builder.track1;
053        track2      = builder.track2;
054    }
055
056    public String getPan() {
057        return pan;
058    }
059
060    public BigInteger getPanAsNumber() {
061        return new BigInteger(pan);
062    }
063
064    public String getExp() {
065        return exp;
066    }
067
068    public String getCvv2() {
069        return cvv2;
070    }
071
072    public String getServiceCode() {
073        return serviceCode;
074    }
075
076    public boolean hasTrack1() {
077        return track1 != null;
078    }
079
080    public boolean hasTrack2() {
081        return track2 != null;
082    }
083
084    public boolean hasBothTracks() {
085        return hasTrack1() && hasTrack2();
086    }
087
088    /**
089     * Returns the traditional 6-digit BIN from the PAN
090     * @return the first 6 digits of the PAN
091     */
092    public String getBin () {
093        return getBin(BINLEN);
094    }
095
096    /**
097     * Returns the first <code>len</code> digits from the PAN.
098     * Can be used for the newer 8-digit BINs, or some arbitrary length.
099     * @return the first <code>len</code> digits of the PAN
100     */
101    public String getBin (int len) {
102        return pan.substring(0, len);
103    }
104
105    @Override
106    public String toString() {
107        return pan != null ? ISOUtil.protect(pan) : "nil";
108    }
109
110    @Override
111    public boolean equals(Object o) {
112        if (this == o) return true;
113        if (o == null || getClass() != o.getClass()) return false;
114        Card card = (Card) o;
115        return Objects.equals(pan, card.pan) &&
116          Objects.equals(exp, card.exp) &&
117          Objects.equals(cvv2, card.cvv2) &&
118          Objects.equals(serviceCode, card.serviceCode) &&
119          Objects.equals(track1, card.track1) &&
120          Objects.equals(track2, card.track2);
121    }
122
123    @Override
124    public int hashCode() {
125        return Objects.hash(pan, exp, cvv2, serviceCode, track1, track2);
126    }
127
128    public Track1 getTrack1() {
129        return track1;
130    }
131
132    public Track2 getTrack2() {
133        return track2;
134    }
135
136    public boolean isExpired (Date currentDate) {
137        if (exp == null || exp.length() != 4)
138            return true;
139        String now = ISODate.formatDate(currentDate, "yyyyMM");
140        try {
141            int mm = Integer.parseInt(exp.substring(2));
142            int aa = Integer.parseInt(exp.substring(0,2));
143            if (aa < 100 && mm > 0 && mm <= 12) {
144                String expDate = (aa < 70 ? "20" : "19") + exp;
145                if (expDate.compareTo(now) >= 0)
146                    return false;
147            }
148        } catch (NumberFormatException ignored) {
149            // NOPMD
150        }
151        return true;
152    }
153
154    public static Builder builder() {
155        return new Builder();
156    }
157
158    public static class Builder {
159        public static CardValidator DEFAULT_CARD_VALIDATOR  = new DefaultCardValidator();
160        private String pan;
161        private String exp;
162        private String cvv;
163        private String cvv2;
164        private String serviceCode;
165        private Track1 track1;
166        private Track2 track2;
167        private Track1.Builder track1Builder = Track1.builder();
168        private Track2.Builder track2Builder = Track2.builder();
169        private CardValidator validator = DEFAULT_CARD_VALIDATOR;
170
171        private Builder () { }
172        public Builder pan (String pan) { this.pan = pan; return this; }
173        public Builder exp (String exp) { this.exp = exp; return this; }
174        public Builder cvv (String cvv) { this.cvv = cvv; return this; }
175        public Builder cvv2 (String cvv2) { this.cvv2 = cvv2; return this; }
176        public Builder serviceCode (String serviceCode) { this.serviceCode = serviceCode; return this; }
177        public Builder validator (CardValidator validator) {
178            this.validator = validator;
179            return this;
180        }
181        public Builder withTrack1Builder (Track1.Builder track1Builder) {
182            this.track1Builder = track1Builder;
183            return this;
184        }
185        public Builder withTrack2Builder (Track2.Builder track2Builder) {
186            this.track2Builder = track2Builder;
187            return this;
188        }
189        public Builder track1 (Track1 track1) {
190            this.track1 = track1;
191            return this;
192        }
193        public Builder track2 (Track2 track2) {
194            this.track2 = track2;
195            return this;
196        }
197        public Builder isomsg (ISOMsg m) throws InvalidCardException {
198            if (m.hasField(2))
199                pan(m.getString(2));
200            if (m.hasField(14))
201                exp(m.getString(14));
202            if (m.hasField(35))
203                track2(track2Builder.track(m.getString(35)).build());
204            if (m.hasField(45))
205                track1(track1Builder.track(m.getString(45)).build());
206            if (pan == null && track2 != null)
207                pan (track2.getPan());
208            if (pan == null && track1 != null)
209                pan (track1.getPan());
210            if (exp == null && track2 != null)
211                exp (track2.getExp());
212            if (exp == null && track1 != null)
213                exp (track1.getExp());
214            if (track2 != null) {
215                if (pan == null)
216                    pan (track2.getPan());
217                if (exp == null)
218                    exp (track2.getExp());
219                if (serviceCode == null)
220                    serviceCode(track2.getServiceCode());
221            }
222            if (track1 != null) {
223                if (pan == null)
224                    pan (track1.getPan());
225                if (exp == null)
226                    exp (track1.getExp());
227                if (serviceCode == null)
228                    serviceCode(track1.getServiceCode());
229            }
230            return this;
231        }
232
233        public Card build() throws InvalidCardException {
234            Card c = new Card(this);
235            if (validator != null)
236                validator.validate(c);
237            return c;
238        }
239
240    }
241}