001: /*******************************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: *******************************************************************************/package org.ofbiz.order.finaccount;
019:
020: import java.math.BigDecimal;
021: import java.sql.Timestamp;
022: import java.util.List;
023: import java.util.Random;
024: import java.util.regex.Pattern;
025:
026: import org.ofbiz.base.util.*;
027: import org.ofbiz.entity.GenericDelegator;
028: import org.ofbiz.entity.GenericEntity;
029: import org.ofbiz.entity.GenericEntityException;
030: import org.ofbiz.entity.GenericValue;
031: import org.ofbiz.entity.condition.EntityConditionList;
032: import org.ofbiz.entity.condition.EntityExpr;
033: import org.ofbiz.entity.condition.EntityOperator;
034: import org.ofbiz.entity.model.ModelEntity;
035: import org.ofbiz.entity.util.EntityUtil;
036:
037: /**
038: * A package of methods for improving efficiency of financial accounts services
039: *
040: */
041: public class FinAccountHelper {
042:
043: public static final String module = FinAccountHelper.class
044: .getName();
045: /**
046: * 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.
047: */
048: public static int decimals = UtilNumber
049: .getBigDecimalScale("finaccount.decimals");
050: public static int rounding = UtilNumber
051: .getBigDecimalRoundingMode("finaccount.rounding");
052: public static final BigDecimal ZERO = (new BigDecimal("0.0"))
053: .setScale(decimals, rounding);
054:
055: public static final String giftCertFinAccountTypeId = "GIFTCERT_ACCOUNT";
056: public static final boolean defaultPinRequired = false;
057:
058: // pool of available characters for account codes, here numbers plus uppercase characters
059: static char[] char_pool = new char[10 + 26];
060: static {
061: int j = 0;
062: for (int i = "0".charAt(0); i <= "9".charAt(0); i++) {
063: char_pool[j++] = (char) i;
064: }
065: for (int i = "A".charAt(0); i <= "Z".charAt(0); i++) {
066: char_pool[j++] = (char) i;
067: }
068: }
069:
070: /**
071: * A convenience method which adds transactions.get(0).get(fieldName) to initialValue, all done in BigDecimal to decimals and rounding
072: * @param initialValue
073: * @param transactions
074: * @param fieldName
075: * @param decimals
076: * @param rounding
077: * @return
078: * @throws GenericEntityException
079: */
080: public static BigDecimal addFirstEntryAmount(
081: BigDecimal initialValue, List transactions,
082: String fieldName, int decimals, int rounding)
083: throws GenericEntityException {
084: if ((transactions != null) && (transactions.size() == 1)) {
085: GenericValue firstEntry = (GenericValue) transactions
086: .get(0);
087: if (firstEntry.get(fieldName) != null) {
088: BigDecimal valueToAdd = new BigDecimal(firstEntry
089: .getDouble(fieldName).doubleValue());
090: return initialValue.add(valueToAdd).setScale(decimals,
091: rounding);
092: } else {
093: return initialValue;
094: }
095: } else {
096: return initialValue;
097: }
098: }
099:
100: /**
101: * Returns a unique randomly generated account code for FinAccount.finAccountCode composed of uppercase letters and numbers
102: * @param codeLength length of code in number of characters
103: * @param delegator
104: * @return
105: * @throws GenericEntityException
106: */
107: public static String getNewFinAccountCode(int codeLength,
108: GenericDelegator delegator) throws GenericEntityException {
109:
110: // keep generating new account codes until a unique one is found
111: Random r = new Random();
112: boolean foundUniqueNewCode = false;
113: StringBuffer newAccountCode = null;
114: long count = 0;
115:
116: while (!foundUniqueNewCode) {
117: newAccountCode = new StringBuffer(codeLength);
118: for (int i = 0; i < codeLength; i++) {
119: newAccountCode.append(char_pool[r
120: .nextInt(char_pool.length)]);
121: }
122:
123: List existingAccountsWithCode = delegator.findByAnd(
124: "FinAccount", UtilMisc.toMap("finAccountCode",
125: newAccountCode.toString()));
126: if (existingAccountsWithCode.size() == 0) {
127: foundUniqueNewCode = true;
128: }
129:
130: count++;
131: if (count > 999999) {
132: throw new GenericEntityException(
133: "Unable to locate unique FinAccountCode! Length ["
134: + codeLength + "]");
135: }
136: }
137:
138: return newAccountCode.toString();
139: }
140:
141: /**
142: * Gets the first (and should be only) FinAccount based on finAccountCode, which will be cleaned up to be only uppercase and alphanumeric
143: * @param finAccountCode
144: * @param delegator
145: * @return
146: * @throws GenericEntityException
147: */
148: public static GenericValue getFinAccountFromCode(
149: String finAccountCode, GenericDelegator delegator)
150: throws GenericEntityException {
151: // regex magic to turn all letters in code to uppercase and then remove all non-alphanumeric letters
152: if (finAccountCode == null) {
153: return null;
154: }
155:
156: Pattern filterRegex = Pattern.compile("[^0-9A-Z]");
157: finAccountCode = finAccountCode.toUpperCase().replaceAll(
158: filterRegex.pattern(), "");
159:
160: // now we need to get the encrypted version of the fin account code the user passed in to look up against FinAccount
161: // we do this by making a temporary generic entity with same finAccountCode and then doing a match
162: ModelEntity finAccountEntity = delegator
163: .getModelEntity("FinAccount");
164: GenericEntity encryptedFinAccount = GenericEntity
165: .createGenericEntity(finAccountEntity, UtilMisc.toMap(
166: "finAccountCode", finAccountCode));
167: delegator.encryptFields(encryptedFinAccount);
168: String encryptedFinAccountCode = encryptedFinAccount
169: .getString("finAccountCode");
170:
171: // now look for the account
172: List accounts = delegator.findByAnd("FinAccount", UtilMisc
173: .toMap("finAccountCode", encryptedFinAccountCode));
174: accounts = EntityUtil.filterByDate(accounts);
175:
176: if ((accounts == null) || (accounts.size() == 0)) {
177: // OK to display - not a code anyway
178: Debug.logWarning("No fin account found for account code ["
179: + finAccountCode + "]", module);
180: return null;
181: } else if (accounts.size() > 1) {
182: // This should never happen, but don't display the code if it does -- it is supposed to be encrypted!
183: Debug.logError("Multiple fin accounts found", module);
184: return null;
185: } else {
186: return (GenericValue) accounts.get(0);
187: }
188: }
189:
190: /**
191: * Sum of all DEPOSIT and ADJUSTMENT transactions minus all WITHDRAWAL transactions whose transactionDate is before asOfDateTime
192: * @param finAccountId
193: * @param asOfDateTime
194: * @param delegator
195: * @return
196: * @throws GenericEntityException
197: */
198: public static BigDecimal getBalance(String finAccountId,
199: Timestamp asOfDateTime, GenericDelegator delegator)
200: throws GenericEntityException {
201: if (asOfDateTime == null)
202: asOfDateTime = UtilDateTime.nowTimestamp();
203:
204: BigDecimal incrementTotal = ZERO; // total amount of transactions which increase balance
205: BigDecimal decrementTotal = ZERO; // decrease balance
206:
207: GenericValue finAccount = delegator.findByPrimaryKeyCache(
208: "FinAccount", UtilMisc.toMap("finAccountId",
209: finAccountId));
210: String currencyUomId = finAccount.getString("currencyUomId");
211:
212: // find the sum of all transactions which increase the value
213: EntityConditionList incrementConditions = new EntityConditionList(
214: UtilMisc.toList(new EntityExpr("finAccountId",
215: EntityOperator.EQUALS, finAccountId),
216: new EntityExpr("transactionDate",
217: EntityOperator.LESS_THAN_EQUAL_TO,
218: asOfDateTime), new EntityConditionList(
219: UtilMisc.toList(new EntityExpr(
220: "finAccountTransTypeId",
221: EntityOperator.EQUALS,
222: "DEPOSIT"), new EntityExpr(
223: "finAccountTransTypeId",
224: EntityOperator.EQUALS,
225: "ADJUSTMENT")),
226: EntityOperator.OR)), EntityOperator.AND);
227: List transSums = delegator.findByCondition(
228: "FinAccountTransSum", incrementConditions, UtilMisc
229: .toList("amount"), null);
230: incrementTotal = addFirstEntryAmount(incrementTotal, transSums,
231: "amount", (decimals + 1), rounding);
232:
233: // now find sum of all transactions with decrease the value
234: EntityConditionList decrementConditions = new EntityConditionList(
235: UtilMisc.toList(new EntityExpr("finAccountId",
236: EntityOperator.EQUALS, finAccountId),
237: new EntityExpr("transactionDate",
238: EntityOperator.LESS_THAN_EQUAL_TO,
239: asOfDateTime), new EntityExpr(
240: "currencyUomId", EntityOperator.EQUALS,
241: currencyUomId), new EntityExpr(
242: "finAccountTransTypeId",
243: EntityOperator.EQUALS, "WITHDRAWAL")),
244: EntityOperator.AND);
245: transSums = delegator.findByCondition("FinAccountTransSum",
246: decrementConditions, UtilMisc.toList("amount"), null);
247: decrementTotal = addFirstEntryAmount(decrementTotal, transSums,
248: "amount", (decimals + 1), rounding);
249:
250: // the net balance is just the incrementTotal minus the decrementTotal
251: return incrementTotal.subtract(decrementTotal).setScale(
252: decimals, rounding);
253: }
254:
255: /**
256: * 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
257: * @param finAccountId
258: * @param asOfDateTime
259: * @param delegator
260: * @return
261: * @throws GenericEntityException
262: */
263: public static BigDecimal getAvailableBalance(String finAccountId,
264: Timestamp asOfDateTime, GenericDelegator delegator)
265: throws GenericEntityException {
266: if (asOfDateTime == null)
267: asOfDateTime = UtilDateTime.nowTimestamp();
268:
269: BigDecimal netBalance = getBalance(finAccountId, asOfDateTime,
270: delegator);
271:
272: // find sum of all authorizations which are not expired and which were authorized before as of time
273: EntityConditionList authorizationConditions = new EntityConditionList(
274: UtilMisc.toList(new EntityExpr("finAccountId",
275: EntityOperator.EQUALS, finAccountId),
276: new EntityExpr("authorizationDate",
277: EntityOperator.LESS_THAN_EQUAL_TO,
278: asOfDateTime), EntityUtil
279: .getFilterByDateExpr(asOfDateTime)),
280: EntityOperator.AND);
281:
282: List authSums = delegator.findByCondition("FinAccountAuthSum",
283: authorizationConditions, UtilMisc.toList("amount"),
284: null);
285:
286: BigDecimal authorizationsTotal = addFirstEntryAmount(ZERO,
287: authSums, "amount", (decimals + 1), rounding);
288:
289: // the total available balance is transactions total minus authorizations total
290: return netBalance.subtract(authorizationsTotal).setScale(
291: decimals, rounding);
292: }
293:
294: public static boolean validateFinAccount(GenericValue finAccount) {
295: return false;
296: }
297:
298: /**
299: * Validates a FinAccount's PIN number
300: * @param delegator
301: * @param finAccountId
302: * @param pinNumber
303: * @return true if the bin is valid
304: */
305: public static boolean validatePin(GenericDelegator delegator,
306: String finAccountId, String pinNumber) {
307: GenericValue finAccount = null;
308: try {
309: finAccount = delegator.findByPrimaryKey("FinAccount",
310: UtilMisc.toMap("finAccountId", finAccountId));
311: } catch (GenericEntityException e) {
312: Debug.logError(e, module);
313: }
314:
315: if (finAccount != null) {
316: String dbPin = finAccount.getString("finAccountCode");
317: Debug.logInfo("FinAccount Pin Validation: [Sent: "
318: + pinNumber + "] [Actual: " + dbPin + "]", module);
319: if (dbPin != null && dbPin.equals(pinNumber)) {
320: return true;
321: }
322: } else {
323: Debug.logInfo("FinAccount record not found ("
324: + finAccountId + ")", module);
325: }
326: return false;
327: }
328:
329: /**
330: *
331: * @param delegator
332: * @param length length of the number to generate (up to 19 digits)
333: * @param isId to be used as an ID (will check the DB to make sure it doesn't already exist)
334: * @return String generated number
335: * @throws GenericEntityException
336: */
337: public static String generateRandomFinNumber(
338: GenericDelegator delegator, int length, boolean isId)
339: throws GenericEntityException {
340: if (length > 19) {
341: length = 19;
342: }
343:
344: Random rand = new Random();
345: boolean isValid = false;
346: String number = null;
347: while (!isValid) {
348: number = "";
349: for (int i = 0; i < length; i++) {
350: int randInt = rand.nextInt(9);
351: number = number + randInt;
352: }
353:
354: if (isId) {
355: int check = UtilValidate.getLuhnCheckDigit(number);
356: number = number + check;
357:
358: // validate the number
359: if (checkFinAccountNumber(number)) {
360: // make sure this number doens't already exist
361: isValid = checkIsNumberInDatabase(delegator, number);
362: }
363: } else {
364: isValid = true;
365: }
366: }
367: return number;
368: }
369:
370: private static boolean checkIsNumberInDatabase(
371: GenericDelegator delegator, String number)
372: throws GenericEntityException {
373: GenericValue finAccount = delegator.findByPrimaryKey(
374: "FinAccount", UtilMisc.toMap("finAccountId", number));
375: return finAccount == null;
376: }
377:
378: public static boolean checkFinAccountNumber(String number) {
379: number = number.replaceAll("\\D", "");
380: return UtilValidate.sumIsMod10(UtilValidate.getLuhnSum(number));
381: }
382: }
|