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