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}