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 * Immutable card data carrier.
031 *
032 * This class is based on the old {@code CardHolder} class and adds support for multiple
033 * PAN and expiration date sources (manual entry, track 1, track 2). It also fixes naming.
034 *
035 * @author apr@jpos.org
036 * @since jPOS 2.0.5
037 */
038public class Card {
039    /** Primary account number. */
040    private String pan;
041    /** Expiration date (YYMM). */
042    private String exp;
043    /** CVV2 / CVC2 value. */
044    private String cvv2;
045    /** 3-digit ISO service code. */
046    private String serviceCode;
047    /** Track 1 data. */
048    private Track1 track1;
049    /** Track 2 data. */
050    private Track2 track2;
051    /** Length of the BIN (Bank Identification Number) in digits. */
052    public static final int BINLEN = 6;
053
054    private Card() { }
055
056    /**
057     * Creates a Card from the given Builder.
058     * @param builder the builder
059     */
060    public Card(Builder builder) {
061        pan         = builder.pan;
062        exp         = builder.exp;
063        cvv2        = builder.cvv2;
064        serviceCode = builder.serviceCode;
065        track1      = builder.track1;
066        track2      = builder.track2;
067    }
068
069    /**
070     * Returns the primary account number.
071     * @return the PAN
072     */
073    public String getPan() {
074        return pan;
075    }
076
077    /**
078     * Returns the primary account number as a {@link BigInteger}.
079     * @return the PAN as a BigInteger
080     */
081    public BigInteger getPanAsNumber() {
082        return new BigInteger(pan);
083    }
084
085    /**
086     * Returns the card expiry date.
087     * @return the expiry date in YYMM format
088     */
089    public String getExp() {
090        return exp;
091    }
092
093    /**
094     * Returns the CVV2 / CVC2 value.
095     * @return the CVV2 value
096     */
097    public String getCvv2() {
098        return cvv2;
099    }
100
101    /**
102     * Returns the ISO service code.
103     * @return the 3-digit service code
104     */
105    public String getServiceCode() {
106        return serviceCode;
107    }
108
109    /**
110     * Returns true if track 1 data is present.
111     * @return true if track 1 is available
112     */
113    public boolean hasTrack1() {
114        return track1 != null;
115    }
116
117    /**
118     * Returns true if track 2 data is present.
119     * @return true if track 2 is available
120     */
121    public boolean hasTrack2() {
122        return track2 != null;
123    }
124
125    /**
126     * Returns true if both track 1 and track 2 data are present.
127     * @return true if both tracks are available
128     */
129    public boolean hasBothTracks() {
130        return hasTrack1() && hasTrack2();
131    }
132
133    /**
134     * Returns the traditional 6-digit BIN from the PAN.
135     * @return the first {@value #BINLEN} digits of the PAN
136     */
137    public String getBin () {
138        return getBin(BINLEN);
139    }
140
141    /**
142     * Returns the first {@code len} digits from the PAN.
143     * Can be used for the newer 8-digit BINs, or some arbitrary length.
144     * @param len number of leading digits to return
145     * @return the first {@code len} digits of the PAN
146     */
147    public String getBin (int len) {
148        return pan.substring(0, len);
149    }
150
151    /** {@inheritDoc} */
152    @Override
153    public String toString() {
154        return pan != null ? ISOUtil.protect(pan) : "nil";
155    }
156
157    /** {@inheritDoc} */
158    @Override
159    public boolean equals(Object o) {
160        if (this == o) return true;
161        if (o == null || getClass() != o.getClass()) return false;
162        Card card = (Card) o;
163        return Objects.equals(pan, card.pan) &&
164          Objects.equals(exp, card.exp) &&
165          Objects.equals(cvv2, card.cvv2) &&
166          Objects.equals(serviceCode, card.serviceCode) &&
167          Objects.equals(track1, card.track1) &&
168          Objects.equals(track2, card.track2);
169    }
170
171    /** {@inheritDoc} */
172    @Override
173    public int hashCode() {
174        return Objects.hash(pan, exp, cvv2, serviceCode, track1, track2);
175    }
176
177    /**
178     * Returns the Track 1 data.
179     * @return the {@link Track1} object, or null if not present
180     */
181    public Track1 getTrack1() {
182        return track1;
183    }
184
185    /**
186     * Returns the Track 2 data.
187     * @return the {@link Track2} object, or null if not present
188     */
189    public Track2 getTrack2() {
190        return track2;
191    }
192
193    /**
194     * Returns true if the card is expired relative to the given date.
195     * @param currentDate the date to compare against
196     * @return true if the card is expired as of {@code currentDate}
197     */
198    public boolean isExpired (Date currentDate) {
199        if (exp == null || exp.length() != 4)
200            return true;
201        String now = ISODate.formatDate(currentDate, "yyyyMM");
202        try {
203            int mm = Integer.parseInt(exp.substring(2));
204            int aa = Integer.parseInt(exp.substring(0,2));
205            if (aa < 100 && mm > 0 && mm <= 12) {
206                String expDate = (aa < 70 ? "20" : "19") + exp;
207                if (expDate.compareTo(now) >= 0)
208                    return false;
209            }
210        } catch (NumberFormatException ignored) {
211            // NOPMD
212        }
213        return true;
214    }
215
216    /**
217     * Returns a new {@link Builder} for constructing a {@link Card}.
218     * @return a new Builder
219     */
220    public static Builder builder() {
221        return new Builder();
222    }
223
224    /** Builder for constructing {@link Card} instances. */
225    public static class Builder {
226        /** Default card validator instance. */
227        public static CardValidator DEFAULT_CARD_VALIDATOR  = new DefaultCardValidator();
228        private String pan;
229        private String exp;
230        private String cvv;
231        private String cvv2;
232        private String serviceCode;
233        private Track1 track1;
234        private Track2 track2;
235        private Track1.Builder track1Builder = Track1.builder();
236        private Track2.Builder track2Builder = Track2.builder();
237        private CardValidator validator = DEFAULT_CARD_VALIDATOR;
238
239        private Builder () { }
240
241        /**
242         * Sets the primary account number.
243         * @param pan the PAN
244         * @return this builder
245         */
246        public Builder pan (String pan) { this.pan = pan; return this; }
247
248        /**
249         * Sets the expiry date.
250         * @param exp the expiry in YYMM format
251         * @return this builder
252         */
253        public Builder exp (String exp) { this.exp = exp; return this; }
254
255        /**
256         * Sets the CVV1 value.
257         * @param cvv the CVV1 value
258         * @return this builder
259         */
260        public Builder cvv (String cvv) { this.cvv = cvv; return this; }
261
262        /**
263         * Sets the CVV2 value.
264         * @param cvv2 the CVV2 value
265         * @return this builder
266         */
267        public Builder cvv2 (String cvv2) { this.cvv2 = cvv2; return this; }
268
269        /**
270         * Sets the service code.
271         * @param serviceCode the 3-digit service code
272         * @return this builder
273         */
274        public Builder serviceCode (String serviceCode) { this.serviceCode = serviceCode; return this; }
275
276        /**
277         * Sets the card validator.
278         * @param validator the card validator to use
279         * @return this builder
280         */
281        public Builder validator (CardValidator validator) {
282            this.validator = validator;
283            return this;
284        }
285
286        /**
287         * Provides a Track 1 builder.
288         * @param track1Builder a {@link Track1.Builder} instance
289         * @return this builder
290         */
291        public Builder withTrack1Builder (Track1.Builder track1Builder) {
292            this.track1Builder = track1Builder;
293            return this;
294        }
295
296        /**
297         * Provides a Track 2 builder.
298         * @param track2Builder a {@link Track2.Builder} instance
299         * @return this builder
300         */
301        public Builder withTrack2Builder (Track2.Builder track2Builder) {
302            this.track2Builder = track2Builder;
303            return this;
304        }
305
306        /**
307         * Sets the Track 1 data.
308         * @param track1 the {@link Track1} object
309         * @return this builder
310         */
311        public Builder track1 (Track1 track1) {
312            this.track1 = track1;
313            return this;
314        }
315
316        /**
317         * Sets the Track 2 data.
318         * @param track2 the {@link Track2} object
319         * @return this builder
320         */
321        public Builder track2 (Track2 track2) {
322            this.track2 = track2;
323            return this;
324        }
325
326        /**
327         * Populates card data from an {@link ISOMsg}.
328         * Extracts PAN, expiry, track 1, and track 2 from the appropriate fields.
329         * @param m an ISOMsg to extract card data from
330         * @return this builder
331         * @throws InvalidCardException if card data is invalid
332         */
333        public Builder isomsg (ISOMsg m) throws InvalidCardException {
334            if (m.hasField(2))
335                pan(m.getString(2));
336            if (m.hasField(14))
337                exp(m.getString(14));
338            if (m.hasField(35))
339                track2(track2Builder.track(m.getString(35)).build());
340            if (m.hasField(45))
341                track1(track1Builder.track(m.getString(45)).build());
342            if (pan == null && track2 != null)
343                pan (track2.getPan());
344            if (pan == null && track1 != null)
345                pan (track1.getPan());
346            if (exp == null && track2 != null)
347                exp (track2.getExp());
348            if (exp == null && track1 != null)
349                exp (track1.getExp());
350            if (track2 != null) {
351                if (pan == null)
352                    pan (track2.getPan());
353                if (exp == null)
354                    exp (track2.getExp());
355                if (serviceCode == null)
356                    serviceCode(track2.getServiceCode());
357            }
358            if (track1 != null) {
359                if (pan == null)
360                    pan (track1.getPan());
361                if (exp == null)
362                    exp (track1.getExp());
363                if (serviceCode == null)
364                    serviceCode(track1.getServiceCode());
365            }
366            return this;
367        }
368
369        /**
370         * Builds and validates the {@link Card}.
371         * @return a new Card instance
372         * @throws InvalidCardException if the card data is invalid
373         */
374        public Card build() throws InvalidCardException {
375            Card c = new Card(this);
376            if (validator != null)
377                validator.validate(c);
378            return c;
379        }
380
381    }
382}