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