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.ISOUtil; 022 023import java.util.Objects; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027/** 028 * 029 * This class is based on the old 'CardHolder' class and adds support for multiple 030 * PAN and Expiration dates taken from manual entry, track1, track1. It also corrects the name. 031 * 032 * @author apr@jpos.org 033 * @since jPOS 2.0.5 034 * 035 */ 036 037@SuppressWarnings("unused") 038public class Track1 { 039 private String pan; 040 private String nameOnCard; 041 private String exp; 042 private String serviceCode; 043 private String cvv; 044 private String discretionaryData; 045 private String track; 046 047 private Track1 () { } 048 049 /** 050 * Copies the track1 fields from the supplied {@link Builder}. 051 * 052 * @param builder builder carrying the parsed or assembled track1 fields 053 */ 054 public Track1 (Builder builder) { 055 pan = builder.pan; 056 nameOnCard = builder.nameOnCard; 057 exp = builder.exp; 058 cvv = builder.cvv; 059 discretionaryData = builder.discretionaryData; 060 serviceCode = builder.serviceCode; 061 track = builder.track; 062 } 063 064 /** 065 * Returns the primary account number. 066 * 067 * @return primary account number 068 */ 069 public String getPan() { 070 return pan; 071 } 072 073 /** 074 * Returns the cardholder name encoded on the track. 075 * 076 * @return cardholder name as encoded on the track 077 */ 078 public String getNameOnCard() { 079 return nameOnCard; 080 } 081 082 /** 083 * Returns the expiration date. 084 * 085 * @return expiration date in {@code YYMM} form, or {@code null} if absent 086 */ 087 public String getExp() { 088 return exp; 089 } 090 091 /** 092 * Returns the CVV/CVC value, when present. 093 * 094 * @return CVV/CVC value, or {@code null} if not present in the track 095 */ 096 public String getCvv() { 097 return cvv; 098 } 099 100 /** 101 * Returns the service code. 102 * 103 * @return three-digit service code, or {@code null} if absent 104 */ 105 public String getServiceCode() { 106 return serviceCode; 107 } 108 109 /** 110 * Returns the discretionary data trailing the service code. 111 * 112 * @return remaining discretionary data, or {@code null} if absent 113 */ 114 public String getDiscretionaryData() { 115 return discretionaryData; 116 } 117 118 /** 119 * Returns the raw track1 string this object was built from. 120 * 121 * @return raw track1 string this object was built from, or {@code null} when assembled programmatically 122 */ 123 public String getTrack() { 124 125 return track; 126 } 127 128 /** 129 * Returns {@code true} when the service code marks this as an IC (EMV) card. 130 * 131 * @return {@code true} if the Track 1 service code indicates an IC card 132 */ 133 public boolean isEMV() { 134 return isICCard(); 135 } 136 137 /** 138 * Returns {@code true} when the service code marks this as an IC card 139 * (first digit {@code 2} for international or {@code 6} for national). 140 * 141 * @return {@code true} if the Track 1 service code indicates an IC card 142 */ 143 public boolean isICCard() { 144 return serviceCode != null && serviceCode.length() == 3 && 145 (serviceCode.charAt(0) == '2' || serviceCode.charAt(0) == '6'); 146 } 147 148 /** 149 * Returns {@code true} when the service code marks this as an 150 * internationally-usable IC card (first digit {@code 2}). 151 * 152 * @return {@code true} if the Track 1 service code indicates an international IC card 153 */ 154 public boolean isInternationalICCard() { 155 return serviceCode != null && serviceCode.length() == 3 && serviceCode.charAt(0) == '2'; 156 } 157 158 @Override 159 public String toString() { 160 return pan != null ? ISOUtil.protect(pan) : "nil"; 161 } 162 163 @Override 164 public boolean equals(Object o) { 165 if (this == o) return true; 166 if (o == null || getClass() != o.getClass()) return false; 167 Track1 track11 = (Track1) o; 168 return Objects.equals(pan, track11.pan) && 169 Objects.equals(nameOnCard, track11.nameOnCard) && 170 Objects.equals(exp, track11.exp) && 171 Objects.equals(serviceCode, track11.serviceCode) && 172 Objects.equals(cvv, track11.cvv) && 173 Objects.equals(discretionaryData, track11.discretionaryData) && 174 Objects.equals(track, track11.track); 175 } 176 177 @Override 178 public int hashCode() { 179 return Objects.hash(pan, nameOnCard, exp, serviceCode, cvv, discretionaryData, track); 180 } 181 182 /** 183 * Creates a new builder for assembling a {@code Track1}. 184 * 185 * @return a new {@link Builder} for assembling a {@code Track1} 186 */ 187 public static Builder builder() { 188 return new Builder(); 189 } 190 191 /** 192 * Fluent builder that parses a raw track1 string or assembles a {@code Track1} 193 * from individual fields and validates the result against a configurable pattern. 194 */ 195 public static class Builder { 196 private static String TRACK1_EXPR = "^[%]?[A-Z]+([0-9]{1,19})\\^([^\\^]{2,26})\\^([0-9]{4})([0-9]{3})([0-9]{4})?([0-9]{1,10})?"; 197 private static Pattern TRACK1_PATTERN = Pattern.compile(TRACK1_EXPR); 198 private String pan; 199 private String nameOnCard; 200 private String exp; 201 private String cvv; 202 private String serviceCode; 203 private String discretionaryData; 204 private String track; 205 private Pattern pattern = TRACK1_PATTERN; 206 private Builder () { } 207 208 /** 209 * Sets the primary account number. 210 * 211 * @param pan primary account number 212 * @return this builder 213 */ 214 public Builder pan (String pan) { 215 this.pan = pan; return this; 216 } 217 218 /** 219 * Sets the cardholder name. 220 * 221 * @param nameOnCard cardholder name as encoded on the track 222 * @return this builder 223 */ 224 public Builder nameOnCard (String nameOnCard) { 225 this.nameOnCard = nameOnCard; 226 return this; 227 } 228 229 /** 230 * Sets the expiration date. 231 * 232 * @param exp expiration date in {@code YYMM} form 233 * @return this builder 234 */ 235 public Builder exp (String exp) { 236 this.exp = exp; return this; 237 } 238 239 /** 240 * Sets the CVV/CVC value. 241 * 242 * @param cvv CVV/CVC value 243 * @return this builder 244 */ 245 public Builder cvv (String cvv) { 246 this.cvv = cvv; return this; 247 } 248 249 /** 250 * Sets the service code. 251 * 252 * @param serviceCode three-digit service code 253 * @return this builder 254 */ 255 public Builder serviceCode (String serviceCode) { 256 this.serviceCode = serviceCode; return this; 257 } 258 259 /** 260 * Sets the discretionary data trailing the service code. 261 * 262 * @param discretionaryData discretionary data trailing the service code 263 * @return this builder 264 */ 265 public Builder discretionaryData (String discretionaryData) { 266 this.discretionaryData = discretionaryData; 267 return this; 268 } 269 270 /** 271 * Optional method, can be used to override default pattern 272 * @param pattern overrides default pattern 273 * @return this builder 274 */ 275 public Builder pattern (Pattern pattern) { 276 this.pattern = pattern; 277 return this; 278 } 279 280 /** 281 * Parses a raw track1 string and populates the builder fields. 282 * 283 * @param s raw track1 data 284 * @return this builder 285 * @throws InvalidCardException if {@code s} is null or does not match 286 * the configured pattern 287 */ 288 public Builder track (String s) 289 throws InvalidCardException 290 { 291 if (s == null) 292 throw new InvalidCardException ("null track1 data"); 293 294 track = s; 295 Matcher matcher = pattern.matcher(s); 296 int cnt = matcher.groupCount(); 297 if (matcher.find() && cnt >= 2) { 298 pan = matcher.group(1); 299 nameOnCard = matcher.group(2); 300 if (cnt > 2) 301 exp = matcher.group(3); 302 if (cnt > 3) 303 serviceCode = matcher.group(4); 304 if (cnt > 4) 305 cvv = matcher.group(5); 306 if (cnt > 5) 307 discretionaryData = matcher.group(6); 308 } else { 309 throw new InvalidCardException ("invalid track1"); 310 } 311 return this; 312 } 313 314 /** 315 * Constructs the Track1 data based on the card data provided. 316 * The generated Track1 data is validated using the pattern. 317 * If the Track1 data doesn't match the pattern, the track attribute keeps the original value. 318 * @return this builder. 319 */ 320 public Builder buildTrackData() { 321 StringBuilder track1 = new StringBuilder("%"); 322 track1.append("B"); 323 track1.append(this.pan); 324 track1.append("^"); 325 track1.append(this.nameOnCard); 326 track1.append("^"); 327 track1.append(this.exp); 328 track1.append(this.serviceCode); 329 track1.append(this.cvv); 330 track1.append(this.discretionaryData); 331 332 Matcher matcher = this.pattern.matcher(track1); 333 int cnt = matcher.groupCount(); 334 if (matcher.find() && cnt >= 2) 335 this.track = track1.toString(); 336 337 return this; 338 } 339 340 /** 341 * Builds the immutable {@link Track1}. If no raw track string was set, 342 * one is assembled from the individual fields via {@link #buildTrackData()}. 343 * 344 * @return the built {@link Track1} 345 */ 346 public Track1 build() { 347 if (this.track == null) 348 buildTrackData(); 349 return new Track1(this); 350 } 351 } 352}