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