001// -------------------------------------------------------------------------------- 002// Copyright 2002-2024 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.model.control.payment.server.logic; 018 019import com.echothree.control.user.payment.common.edit.PartyPaymentMethodEdit; 020import com.echothree.model.control.contact.server.control.ContactControl; 021import com.echothree.model.control.party.common.PartyTypes; 022import com.echothree.model.control.party.server.control.PartyControl; 023import com.echothree.model.control.payment.common.PaymentMethodTypes; 024import com.echothree.model.control.payment.common.exception.UnknownPartyPaymentMethodNameException; 025import com.echothree.model.control.payment.server.control.PartyPaymentMethodControl; 026import com.echothree.model.control.payment.server.control.PaymentMethodControl; 027import com.echothree.model.control.user.server.control.UserControl; 028import com.echothree.model.data.contact.server.entity.ContactMechanism; 029import com.echothree.model.data.contact.server.entity.PartyContactMechanism; 030import com.echothree.model.data.party.common.pk.PartyPK; 031import com.echothree.model.data.party.server.entity.NameSuffix; 032import com.echothree.model.data.party.server.entity.Party; 033import com.echothree.model.data.party.server.entity.PersonalTitle; 034import com.echothree.model.data.party.server.entity.TimeZone; 035import com.echothree.model.data.payment.server.entity.PartyPaymentMethod; 036import com.echothree.model.data.payment.server.entity.PaymentMethod; 037import com.echothree.model.data.payment.server.entity.PaymentMethodCreditCard; 038import com.echothree.model.data.payment.server.entity.PaymentMethodType; 039import com.echothree.model.data.user.server.entity.UserVisit; 040import com.echothree.util.common.message.ExecutionErrors; 041import com.echothree.util.server.control.BaseLogic; 042import com.echothree.util.server.message.ExecutionErrorAccumulator; 043import com.echothree.util.server.persistence.EntityPermission; 044import com.echothree.util.server.persistence.Session; 045import java.time.Instant; 046import java.time.ZoneId; 047import java.time.ZonedDateTime; 048import java.util.regex.Matcher; 049import java.util.regex.Pattern; 050 051public class PartyPaymentMethodLogic 052 extends BaseLogic { 053 054 private PartyPaymentMethodLogic() { 055 super(); 056 } 057 058 private static class PartyPaymentMethodLogicHolder { 059 static PartyPaymentMethodLogic instance = new PartyPaymentMethodLogic(); 060 } 061 062 public static PartyPaymentMethodLogic getInstance() { 063 return PartyPaymentMethodLogicHolder.instance; 064 } 065 066 private String getDigitsOnly(String s) { 067 StringBuilder digitsOnly = new StringBuilder(); 068 069 for(int i = 0; i < s.length(); i++) { 070 char c = s.charAt(i); 071 072 if(Character.isDigit(c)) { 073 digitsOnly.append(c); 074 } 075 } 076 077 return digitsOnly.toString(); 078 } 079 080 private boolean isValid(String number) { 081 String digitsOnly = getDigitsOnly(number); 082 int sum = 0; 083 boolean timesTwo = false; 084 085 for(int i = digitsOnly.length() - 1; i >= 0; i--) { 086 int digit = Integer.parseInt(digitsOnly.substring(i, i + 1)); 087 int addend; 088 089 if(timesTwo) { 090 addend = digit * 2; 091 092 if (addend > 9) { 093 addend -= 9; 094 } 095 } else { 096 addend = digit; 097 } 098 099 sum += addend; 100 timesTwo = !timesTwo; 101 } 102 103 int modulus = sum % 10; 104 105 return modulus == 0; 106 } 107 108 public void checkPartyType(final ExecutionErrorAccumulator ema, final Party party) { 109 String partyTypeName = party.getLastDetail().getPartyType().getPartyTypeName(); 110 111 if(!partyTypeName.equals(PartyTypes.CUSTOMER.name())) { 112 ema.addExecutionError(ExecutionErrors.InvalidPartyType.name(), partyTypeName); 113 } 114 } 115 116 public void checkNameOnCard(final ExecutionErrorAccumulator ema, final PartyPaymentMethodEdit ppme, final PaymentMethodCreditCard paymentMethodCreditCard) { 117 var partyControl = Session.getModelController(PartyControl.class); 118 String personalTitleId = ppme.getPersonalTitleId(); 119 PersonalTitle personalTitle = personalTitleId == null? null: partyControl.convertPersonalTitleIdToEntity(personalTitleId, EntityPermission.READ_ONLY); 120 121 if(personalTitleId == null || personalTitle != null) { 122 String nameSuffixId = ppme.getNameSuffixId(); 123 NameSuffix nameSuffix = nameSuffixId == null? null: partyControl.convertNameSuffixIdToEntity(nameSuffixId, EntityPermission.READ_ONLY); 124 125 if(nameSuffixId == null || nameSuffix != null) { 126 if(paymentMethodCreditCard.getRequireNameOnCard()) { 127 if(ppme.getFirstName() == null || ppme.getLastName() == null) { 128 ema.addExecutionError(ExecutionErrors.MissingNameOnCard.name()); 129 } 130 } 131 } else { 132 ema.addExecutionError(ExecutionErrors.UnknownNameSuffixId.name()); 133 } 134 } else { 135 ema.addExecutionError(ExecutionErrors.UnknownPersonalTitleId.name()); 136 } 137 } 138 139 public void checkNumber(final ExecutionErrorAccumulator ema, final PartyPaymentMethodEdit ppme, final PaymentMethodCreditCard paymentMethodCreditCard) { 140 String number = ppme.getNumber(); 141 142 if(number != null) { 143 String cardNumberValidationPattern = paymentMethodCreditCard.getCardNumberValidationPattern(); 144 boolean validCardNumber = true; 145 146 if(cardNumberValidationPattern != null) { 147 Matcher m = Pattern.compile(cardNumberValidationPattern).matcher(number); 148 149 if(!m.matches()) { 150 validCardNumber = false; 151 } 152 } 153 154 if(!validCardNumber || !isValid(number)) { 155 ema.addExecutionError(ExecutionErrors.InvalidNumber.name()); 156 } 157 } else { 158 ema.addExecutionError(ExecutionErrors.MissingNumber.name()); 159 } 160 } 161 162 public void checkExpirationDate(final Session session, final ExecutionErrorAccumulator ema, final Party party, final PartyPaymentMethodEdit ppme, 163 final PaymentMethodCreditCard paymentMethodCreditCard) { 164 String strExpirationMonth = ppme.getExpirationMonth(); 165 String strExpirationYear = ppme.getExpirationYear(); 166 167 if(strExpirationMonth != null && strExpirationYear != null) { 168 if(paymentMethodCreditCard.getCheckExpirationDate()) { 169 var userControl = Session.getModelController(UserControl.class); 170 TimeZone timeZone = userControl.getPreferredTimeZoneFromParty(party); 171 int expirationMonth = Integer.valueOf(strExpirationMonth); 172 int expirationYear = Integer.valueOf(strExpirationYear); 173 ZonedDateTime dt = ZonedDateTime.ofInstant(Instant.ofEpochMilli(session.START_TIME), ZoneId.of(timeZone.getLastDetail().getJavaTimeZoneName())); 174 boolean validExpirationDate = true; 175 int thisYear = dt.getYear(); 176 177 if(!(expirationYear < thisYear)) { 178 if(expirationYear == thisYear && expirationMonth < dt.getMonthValue()) { 179 validExpirationDate = false; 180 } 181 } else { 182 validExpirationDate = false; 183 } 184 185 if(!validExpirationDate) { 186 ema.addExecutionError(ExecutionErrors.InvalidExpirationDate.name(), strExpirationMonth, strExpirationYear); 187 } 188 } 189 } else if(paymentMethodCreditCard.getRequireExpirationDate()) { 190 if(strExpirationMonth == null) { 191 ema.addExecutionError(ExecutionErrors.MissingExpirationMonth.name()); 192 } 193 194 if(strExpirationYear == null) { 195 ema.addExecutionError(ExecutionErrors.MissingExpirationYear.name()); 196 } 197 } 198 } 199 200 public void checkSecurityCode(final ExecutionErrorAccumulator ema, final PartyPaymentMethodEdit ppme, final PaymentMethodCreditCard paymentMethodCreditCard) { 201 String securityCode = ppme.getSecurityCode(); 202 203 if(securityCode != null) { 204 String securityCodeValidationPattern = paymentMethodCreditCard.getSecurityCodeValidationPattern(); 205 boolean validSecurityCode = true; 206 207 if(securityCodeValidationPattern != null) { 208 Matcher m = Pattern.compile(securityCodeValidationPattern).matcher(securityCode); 209 210 if(!m.matches()) { 211 validSecurityCode = false; 212 } 213 } 214 215 if(!validSecurityCode) { 216 ema.addExecutionError(ExecutionErrors.InvalidSecurityCode.name()); 217 } 218 } else if(paymentMethodCreditCard.getRequireSecurityCode()) { 219 ema.addExecutionError(ExecutionErrors.MissingSecurityCode.name()); 220 } 221 } 222 223 public void checkBillingContactMechanism(final ExecutionErrorAccumulator ema, final Party party, final PartyPaymentMethodEdit ppme, 224 final PaymentMethodCreditCard paymentMethodCreditCard) { 225 var contactControl = Session.getModelController(ContactControl.class); 226 String billingContactMechanismName = ppme.getBillingContactMechanismName(); 227 ContactMechanism billingContactMechanism = billingContactMechanismName == null? null: contactControl.getContactMechanismByName(billingContactMechanismName); 228 229 if(billingContactMechanism != null) { 230 PartyContactMechanism billingPartyContactMechanism = billingContactMechanism == null? null: contactControl.getPartyContactMechanism(party, billingContactMechanism); 231 232 if(billingPartyContactMechanism == null) { 233 ema.addExecutionError(ExecutionErrors.UnknownPartyContactMechanism.name(), party.getLastDetail().getPartyName(), billingContactMechanismName); 234 } 235 } else { 236 if(billingContactMechanismName != null && billingContactMechanism == null) { 237 ema.addExecutionError(ExecutionErrors.UnknownBillingContactMechanismName.name(), billingContactMechanismName); 238 } else if(paymentMethodCreditCard.getRequireBilling()) { 239 ema.addExecutionError(ExecutionErrors.MissingBillingContactMechanismName.name()); 240 } 241 } 242 } 243 244 public void checkIssuer(final ExecutionErrorAccumulator ema, final Party party, final PartyPaymentMethodEdit ppme, 245 final PaymentMethodCreditCard paymentMethodCreditCard) { 246 var contactControl = Session.getModelController(ContactControl.class); 247 String issuerName = ppme.getIssuerName(); 248 String issuerContactMechanismName = ppme.getIssuerContactMechanismName(); 249 ContactMechanism issuerContactMechanism = issuerContactMechanismName == null ? null : contactControl.getContactMechanismByName(issuerContactMechanismName); 250 251 if(issuerName != null && issuerContactMechanism != null) { 252 PartyContactMechanism issuerPartyContactMechanism = issuerContactMechanism == null ? null : contactControl.getPartyContactMechanism(party, issuerContactMechanism); 253 254 if(issuerPartyContactMechanism == null) { 255 ema.addExecutionError(ExecutionErrors.UnknownPartyContactMechanism.name(), party.getLastDetail().getPartyName(), issuerContactMechanismName); 256 } 257 } else { 258 if(paymentMethodCreditCard.getRequireIssuer()) { 259 if(issuerName == null) { 260 ema.addExecutionError(ExecutionErrors.MissingIssuerName.name()); 261 } 262 263 if(issuerContactMechanismName != null && issuerContactMechanism == null) { 264 ema.addExecutionError(ExecutionErrors.UnknownIssuerContactMechanismName.name(), issuerContactMechanismName); 265 } else { 266 ema.addExecutionError(ExecutionErrors.MissingIssuerContactMechanismName.name()); 267 } 268 } else { 269 if(issuerContactMechanismName != null && issuerContactMechanism == null) { 270 ema.addExecutionError(ExecutionErrors.UnknownIssuerContactMechanismName.name(), issuerContactMechanismName); 271 } 272 } 273 } 274 } 275 276 public void checkCreditCard(final Session session, final ExecutionErrorAccumulator ema, final Party party, final PaymentMethod paymentMethod, 277 final PartyPaymentMethodEdit ppme) { 278 var paymentMethodControl = Session.getModelController(PaymentMethodControl.class); 279 PaymentMethodCreditCard paymentMethodCreditCard = paymentMethodControl.getPaymentMethodCreditCard(paymentMethod); 280 281 if(paymentMethodCreditCard.getRequestNameOnCard()) { 282 checkNameOnCard(ema, ppme, paymentMethodCreditCard); 283 } else { 284 ppme.setPersonalTitleId(null); 285 ppme.setFirstName(null); 286 ppme.setMiddleName(null); 287 ppme.setLastName(null); 288 ppme.setNameSuffixId(null); 289 ppme.setName(null); 290 } 291 292 if(paymentMethodCreditCard.getCheckCardNumber()) { 293 checkNumber(ema, ppme, paymentMethodCreditCard); 294 } 295 296 if(paymentMethodCreditCard.getRequestExpirationDate()) { 297 checkExpirationDate(session, ema, party, ppme, paymentMethodCreditCard); 298 } else { 299 ppme.setExpirationMonth(null); 300 ppme.setExpirationYear(null); 301 } 302 303 if(paymentMethodCreditCard.getRequestSecurityCode()) { 304 checkSecurityCode(ema, ppme, paymentMethodCreditCard); 305 } else { 306 ppme.setSecurityCode(null); 307 } 308 309 if(paymentMethodCreditCard.getRequestBilling()) { 310 checkBillingContactMechanism(ema, party, ppme, paymentMethodCreditCard); 311 } else { 312 ppme.setBillingContactMechanismName(null); 313 } 314 315 if(paymentMethodCreditCard.getRequestIssuer()) { 316 checkIssuer(ema, party, ppme, paymentMethodCreditCard); 317 } else { 318 ppme.setIssuerName(null); 319 ppme.setIssuerContactMechanismName(null); 320 } 321 } 322 323 public void checkPaymentMethodType(final Session session, final ExecutionErrorAccumulator ema, final Party party, final PaymentMethod paymentMethod, 324 final PartyPaymentMethodEdit ppme) { 325 PaymentMethodType paymentMethodType = paymentMethod.getLastDetail().getPaymentMethodType(); 326 String paymentMethodTypeName = paymentMethodType.getLastDetail().getPaymentMethodTypeName(); 327 328 if(paymentMethodTypeName.equals(PaymentMethodTypes.CREDIT_CARD.name())) { 329 checkCreditCard(session, ema, party, paymentMethod, ppme); 330 } else { 331 ema.addExecutionError(ExecutionErrors.InvalidPaymentMethodType.name(), paymentMethodTypeName); 332 } 333 } 334 335 public void checkPartyPaymentMethod(final Session session, final UserVisit userVisit, final ExecutionErrorAccumulator ema, final Party party, 336 final PaymentMethod paymentMethod, final PartyPaymentMethodEdit ppme) { 337 checkPartyType(ema, party); 338 339 if(!ema.hasExecutionErrors()) { 340 checkPaymentMethodType(session, ema, party, paymentMethod, ppme); 341 } 342 } 343 344 private PartyPaymentMethod getPartyPaymentMethodByName(final ExecutionErrorAccumulator eea, final String partyPaymentMethodName, 345 final EntityPermission entityPermission) { 346 var partyPaymentMethodControl = Session.getModelController(PartyPaymentMethodControl.class); 347 var partyPaymentMethod = partyPaymentMethodControl.getPartyPaymentMethodByName(partyPaymentMethodName, entityPermission); 348 349 if(partyPaymentMethod == null) { 350 handleExecutionError(UnknownPartyPaymentMethodNameException.class, eea, ExecutionErrors.UnknownPartyPaymentMethodName.name(), partyPaymentMethodName); 351 } 352 353 return partyPaymentMethod; 354 } 355 356 public PartyPaymentMethod getPartyPaymentMethodByName(final ExecutionErrorAccumulator eea, 357 final String partyPaymentMethodName) { 358 return getPartyPaymentMethodByName(eea, partyPaymentMethodName, EntityPermission.READ_ONLY); 359 } 360 361 public PartyPaymentMethod getPartyPaymentMethodByNameForUpdate(final ExecutionErrorAccumulator eea, 362 final String partyPaymentMethodName) { 363 return getPartyPaymentMethodByName(eea, partyPaymentMethodName, EntityPermission.READ_WRITE); 364 } 365 366 public void deletePartyPaymentMethod(final ExecutionErrorAccumulator eea, final PartyPaymentMethod partyPaymentMethod, 367 final PartyPK deletedBy) { 368 var partyPaymentMethodControl = Session.getModelController(PartyPaymentMethodControl.class); 369 370 // TODO: Check to see if this payment method is in use on any open orders, 371 // or orders that currently are allowing returns to be made against them. 372 // If that's the case, the PPM shouldn't be deleted. 373 partyPaymentMethodControl.deletePartyPaymentMethod(partyPaymentMethod, deletedBy); 374 } 375 376 public void deletePartyPaymentMethod(final ExecutionErrorAccumulator eea, final String partyPaymentMethodName, 377 final PartyPK deletedBy) { 378 var partyPaymentMethod = getPartyPaymentMethodByNameForUpdate(eea, partyPaymentMethodName); 379 380 if(!eea.hasExecutionErrors()) { 381 deletePartyPaymentMethod(eea, partyPaymentMethod, deletedBy); 382 } 383 } 384 385}