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}