/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.ofbiz.order.finaccount; import java.math.BigDecimal; import java.sql.Timestamp; import java.util.List; import java.util.Random; import org.apache.ofbiz.base.util.Debug; import org.apache.ofbiz.base.util.UtilDateTime; import org.apache.ofbiz.base.util.UtilMisc; import org.apache.ofbiz.base.util.UtilNumber; import org.apache.ofbiz.base.util.UtilValidate; import org.apache.ofbiz.entity.Delegator; import org.apache.ofbiz.entity.GenericEntityException; import org.apache.ofbiz.entity.GenericValue; import org.apache.ofbiz.entity.condition.EntityCondition; import org.apache.ofbiz.entity.condition.EntityOperator; import org.apache.ofbiz.entity.util.EntityQuery; /** * A package of methods for improving efficiency of financial accounts services * */ public class FinAccountHelper { public static final String module = FinAccountHelper.class.getName(); /** * A word on precision: since we're just adding and subtracting, the interim figures should have one more decimal place of precision than the final numbers. */ public static int decimals = UtilNumber.getBigDecimalScale("finaccount.decimals"); public static int rounding = UtilNumber.getBigDecimalRoundingMode("finaccount.rounding"); public static final BigDecimal ZERO = BigDecimal.ZERO.setScale(decimals, rounding); public static final String giftCertFinAccountTypeId = "GIFTCERT_ACCOUNT"; // pool of available characters for account codes, here numbers plus uppercase characters static char[] char_pool = new char[10+26]; static { int j = 0; for (int i = "0".charAt(0); i <= "9".charAt(0); i++) { char_pool[j++] = (char) i; } for (int i = "A".charAt(0); i <= "Z".charAt(0); i++) { char_pool[j++] = (char) i; } } /** * A convenience method which adds transactions.get(0).get(fieldName) to initialValue, all done in BigDecimal to decimals and rounding * @param initialValue the initial value * @param transactions a List of GenericValue objects of transactions * @param fieldName the field name to get the value from the transaction * @param decimals number of decimals * @param rounding how to rounding * @return the new value in a BigDecimal field * @throws GenericEntityException */ public static BigDecimal addFirstEntryAmount(BigDecimal initialValue, List<GenericValue> transactions, String fieldName, int decimals, int rounding) throws GenericEntityException { if ((transactions != null) && (transactions.size() == 1)) { GenericValue firstEntry = transactions.get(0); if (firstEntry.get(fieldName) != null) { BigDecimal valueToAdd = firstEntry.getBigDecimal(fieldName); return initialValue.add(valueToAdd).setScale(decimals, rounding); } else { return initialValue; } } else { return initialValue; } } /** * Returns a unique randomly generated account code for FinAccount.finAccountCode composed of uppercase letters and numbers * @param codeLength length of code in number of characters * @param delegator the delegator * @return returns a unique randomly generated account code for FinAccount.finAccountCode composed of uppercase letters and numbers * @throws GenericEntityException */ public static String getNewFinAccountCode(int codeLength, Delegator delegator) throws GenericEntityException { // keep generating new account codes until a unique one is found Random r = new Random(); boolean foundUniqueNewCode = false; StringBuilder newAccountCode = null; long count = 0; while (!foundUniqueNewCode) { newAccountCode = new StringBuilder(codeLength); for (int i = 0; i < codeLength; i++) { newAccountCode.append(char_pool[r.nextInt(char_pool.length)]); } GenericValue existingAccountWithCode = EntityQuery.use(delegator).from("FinAccount") .where("finAccountCode", newAccountCode.toString()).queryFirst(); if (existingAccountWithCode == null) { foundUniqueNewCode = true; } count++; if (count > 999999) { throw new GenericEntityException("Unable to locate unique FinAccountCode! Length [" + codeLength + "]"); } } return newAccountCode.toString(); } /** * Gets the first (and should be only) FinAccount based on finAccountCode, which will be cleaned up to be only uppercase and alphanumeric * @param finAccountCode the financial account code * @param delegator the delegator * @return gets the first financial account by code * @throws GenericEntityException */ public static GenericValue getFinAccountFromCode(String finAccountCode, Delegator delegator) throws GenericEntityException { if (finAccountCode == null) { return null; } finAccountCode = finAccountCode.toUpperCase().replaceAll("[^0-9A-Z]", ""); // now look for the account List<GenericValue> accounts = EntityQuery.use(delegator).from("FinAccount") .where("finAccountCode", finAccountCode) .filterByDate().queryList(); if (UtilValidate.isEmpty(accounts)) { // OK to display - not a code anyway Debug.logWarning("No fin account found for account code [" + finAccountCode + "]", module); return null; } else if (accounts.size() > 1) { // This should never happen, but don't display the code if it does -- it is supposed to be encrypted! Debug.logError("Multiple fin accounts found", module); return null; } else { return accounts.get(0); } } /** * Sum of all DEPOSIT and ADJUSTMENT transactions minus all WITHDRAWAL transactions whose transactionDate is before asOfDateTime * @param finAccountId the financial account id * @param asOfDateTime the validity date * @param delegator the delegator * @return returns the sum of all DEPOSIT and ADJUSTMENT transactions minus all WITHDRAWAL transactions * @throws GenericEntityException */ public static BigDecimal getBalance(String finAccountId, Timestamp asOfDateTime, Delegator delegator) throws GenericEntityException { if (asOfDateTime == null) asOfDateTime = UtilDateTime.nowTimestamp(); BigDecimal incrementTotal = ZERO; // total amount of transactions which increase balance BigDecimal decrementTotal = ZERO; // decrease balance // find the sum of all transactions which increase the value List<GenericValue> transSums = EntityQuery.use(delegator) .select("amount") .from("FinAccountTransSum") .where(EntityCondition.makeCondition("finAccountId", EntityOperator.EQUALS, finAccountId), EntityCondition.makeCondition("transactionDate", EntityOperator.LESS_THAN_EQUAL_TO, asOfDateTime), EntityCondition.makeCondition(UtilMisc.toList( EntityCondition.makeCondition("finAccountTransTypeId", EntityOperator.EQUALS, "DEPOSIT"), EntityCondition.makeCondition("finAccountTransTypeId", EntityOperator.EQUALS, "ADJUSTMENT")), EntityOperator.OR)) .queryList(); incrementTotal = addFirstEntryAmount(incrementTotal, transSums, "amount", (decimals+1), rounding); // now find sum of all transactions with decrease the value transSums = EntityQuery.use(delegator) .select("amount") .from("FinAccountTransSum") .where(EntityCondition.makeCondition("finAccountId", EntityOperator.EQUALS, finAccountId), EntityCondition.makeCondition("transactionDate", EntityOperator.LESS_THAN_EQUAL_TO, asOfDateTime), EntityCondition.makeCondition("finAccountTransTypeId", EntityOperator.EQUALS, "WITHDRAWAL")) .queryList(); decrementTotal = addFirstEntryAmount(decrementTotal, transSums, "amount", (decimals+1), rounding); // the net balance is just the incrementTotal minus the decrementTotal return incrementTotal.subtract(decrementTotal).setScale(decimals, rounding); } /** * Returns the net balance (see above) minus the sum of all authorization amounts which are not expired and were authorized by the as of date * @param finAccountId the financial account id * @param asOfDateTime the validity date * @param delegator the delegator * @return returns the net balance (see above) minus the sum of all authorization amounts which are not expired * @throws GenericEntityException */ public static BigDecimal getAvailableBalance(String finAccountId, Timestamp asOfDateTime, Delegator delegator) throws GenericEntityException { if (asOfDateTime == null) asOfDateTime = UtilDateTime.nowTimestamp(); BigDecimal netBalance = getBalance(finAccountId, asOfDateTime, delegator); // find sum of all authorizations which are not expired and which were authorized before as of time List<GenericValue> authSums = EntityQuery.use(delegator) .select("amount") .from("FinAccountAuthSum") .where(EntityCondition.makeCondition("finAccountId", EntityOperator.EQUALS, finAccountId), EntityCondition.makeCondition("authorizationDate", EntityOperator.LESS_THAN_EQUAL_TO, asOfDateTime)) .queryList(); BigDecimal authorizationsTotal = addFirstEntryAmount(ZERO, authSums, "amount", (decimals+1), rounding); // the total available balance is transactions total minus authorizations total return netBalance.subtract(authorizationsTotal).setScale(decimals, rounding); } public static boolean validateFinAccount(GenericValue finAccount) { return false; } /** * Validates a FinAccount's PIN number * @param delegator the delegator * @param finAccountId the financial account id * @param pinNumber a pin number * @return true if the bin is valid */ public static boolean validatePin(Delegator delegator, String finAccountId, String pinNumber) { GenericValue finAccount = null; try { finAccount = EntityQuery.use(delegator).from("FinAccount").where("finAccountId", finAccountId).queryOne(); } catch (GenericEntityException e) { Debug.logError(e, module); } if (finAccount != null) { String dbPin = finAccount.getString("finAccountCode"); Debug.logInfo("FinAccount Pin Validation: [Sent: " + pinNumber + "] [Actual: " + dbPin + "]", module); if (dbPin != null && dbPin.equals(pinNumber)) { return true; } } else { Debug.logInfo("FinAccount record not found (" + finAccountId + ")", module); } return false; } /** * Generate a random financial number * @param delegator the delegator * @param length length of the number to generate (up to 19 digits) * @param isId to be used as an ID (will check the DB to make sure it doesn't already exist) * @return Generated number * @throws GenericEntityException */ public static String generateRandomFinNumber(Delegator delegator, int length, boolean isId) throws GenericEntityException { if (length > 19) { length = 19; } Random rand = new Random(); boolean isValid = false; String number = null; while (!isValid) { number = ""; for (int i = 0; i < length; i++) { int randInt = rand.nextInt(9); number = number + randInt; } if (isId) { int check = UtilValidate.getLuhnCheckDigit(number); number = number + check; // validate the number if (checkFinAccountNumber(number)) { // make sure this number doens't already exist isValid = checkIsNumberInDatabase(delegator, number); } } else { isValid = true; } } return number; } private static boolean checkIsNumberInDatabase(Delegator delegator, String number) throws GenericEntityException { GenericValue finAccount = EntityQuery.use(delegator).from("FinAccount").where("finAccountId", number).queryOne(); return finAccount == null; } public static boolean checkFinAccountNumber(String number) { number = number.replaceAll("\\D", ""); return UtilValidate.sumIsMod10(UtilValidate.getLuhnSum(number)); } }