/*
jBilling - The Enterprise Open Source Billing System
Copyright (C) 2003-2011 Enterprise jBilling Software Ltd. and Emiliano Conde
This file is part of jbilling.
jbilling is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
jbilling is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with jbilling. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sapienter.jbilling.server.payment;
import java.math.BigDecimal;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import org.apache.log4j.Logger;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.sapienter.jbilling.common.SessionInternalError;
import com.sapienter.jbilling.server.invoice.InvoiceBL;
import com.sapienter.jbilling.server.invoice.db.InvoiceDTO;
import com.sapienter.jbilling.server.item.CurrencyBL;
import com.sapienter.jbilling.server.notification.INotificationSessionBean;
import com.sapienter.jbilling.server.notification.MessageDTO;
import com.sapienter.jbilling.server.notification.NotificationBL;
import com.sapienter.jbilling.server.payment.blacklist.CsvProcessor;
import com.sapienter.jbilling.server.payment.db.PaymentDTO;
import com.sapienter.jbilling.server.payment.db.PaymentMethodDAS;
import com.sapienter.jbilling.server.payment.db.PaymentResultDAS;
import com.sapienter.jbilling.server.payment.event.PaymentFailedEvent;
import com.sapienter.jbilling.server.payment.event.PaymentSuccessfulEvent;
import com.sapienter.jbilling.server.process.AgeingBL;
import com.sapienter.jbilling.server.process.ConfigurationBL;
import com.sapienter.jbilling.server.system.event.EventManager;
import com.sapienter.jbilling.server.user.UserBL;
import com.sapienter.jbilling.server.user.db.CustomerDTO;
import com.sapienter.jbilling.server.user.db.UserDAS;
import com.sapienter.jbilling.server.user.partner.PartnerBL;
import com.sapienter.jbilling.server.user.partner.db.Partner;
import com.sapienter.jbilling.server.util.Constants;
import com.sapienter.jbilling.server.util.Context;
import com.sapienter.jbilling.server.util.PreferenceBL;
import com.sapienter.jbilling.server.util.audit.EventLogger;
import com.sapienter.jbilling.server.util.db.CurrencyDAS;
import java.util.ArrayList;
/**
*
* This is the session facade for the payments in general. It is a statless
* bean that provides services not directly linked to a particular operation
*
* @author emilc
*/
@Transactional( propagation = Propagation.REQUIRED )
public class PaymentSessionBean implements IPaymentSessionBean {
private final Logger LOG = Logger.getLogger(PaymentSessionBean.class);
/**
* This method goes over all the over due invoices for a given entity and
* generates a payment record for each of them.
*/
public void processPayments(Integer entityId) throws SessionInternalError {
try {
entityId.intValue(); // just to avoid the warning ;)
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
/**
* This is meant to be called from the billing process, where the information
* about how the payment is going to be done is not known. This method will
* call a pluggable task that finds this information (usually a cc) before
* calling the realtime processing.
* Later, this will have to be changed for some file creation with all the
* payment information to be sent in a batch mode to the processor at the
* end of the billing process.
* This is called only if the user being process has as a preference to
* process the payment with billing process, meaning that a payment has
* to be created and processed real-time.
* @return If the payment was not successful for any reason, null,
* otherwise the payment method used for the payment
*/
public Integer generatePayment(InvoiceDTO invoice)
throws SessionInternalError {
LOG.debug("Generating payment for invoice " + invoice.getId());
// go fetch the entity for this invoice
Integer userId = invoice.getBaseUser().getUserId();
UserDAS userDas = new UserDAS();
Integer entityId = userDas.find(userId).getCompany().getId();
Integer retValue = null;
// create the dto with the information of the payment to create
try {
// get this payment information. Now we only expect one pl.tsk
// to get the info, I don't see how more could help
PaymentDTOEx dto = PaymentBL.findPaymentInstrument(entityId,
invoice.getBaseUser().getUserId());
boolean noInstrument = false;
if (dto == null) {
noInstrument = true;
dto = new PaymentDTOEx();
}
dto.setIsRefund(new Integer(0)); //it is not a refund
dto.setUserId(userId);
dto.setAmount(invoice.getBalance());
dto.setCurrency(new CurrencyDAS().find(invoice.getCurrency().getId()));
dto.setAttempt(new Integer(invoice.getPaymentAttempts() + 1));
// when the payment is generated by the system (instead of
// entered manually by a user), the payment date is sysdate
dto.setPaymentDate(Calendar.getInstance().getTime());
LOG.debug("Prepared payment " + dto);
// it could be that the user doesn't have a payment
// instrument (cc) in the db, or that is invalid (expired).
if (!noInstrument) {
Integer result = processAndUpdateInvoice(dto, invoice);
LOG.debug("After processing. Result=" + result);
if (result != null && result.equals(Constants.RESULT_OK)) {
retValue = dto.getPaymentMethod().getId();
}
} else {
// audit that this guy was about to get a payment
EventLogger logger = new EventLogger();
logger.auditBySystem(entityId, userId, Constants.TABLE_BASE_USER, userId,
EventLogger.MODULE_PAYMENT_MAINTENANCE, EventLogger.PAYMENT_INSTRUMENT_NOT_FOUND,
null, null, null);
// update the invoice attempts
invoice.setPaymentAttempts(dto.getAttempt() == null ?
new Integer(1) : dto.getAttempt());
// treat this as a failed payment
PaymentFailedEvent event = new PaymentFailedEvent(entityId, dto);
EventManager.process(event);
}
} catch (Exception e) {
LOG.fatal("Problems generating payment.", e);
throw new SessionInternalError(
"Problems generating payment.");
}
LOG.debug("Done. Returning:" + retValue);
return retValue;
}
/**
* This method soft deletes a payment
*
* @param paymentId
* @throws SessionInternalError
*/
public void deletePayment(Integer paymentId) throws SessionInternalError {
try {
PaymentBL bl = new PaymentBL(paymentId);
bl.delete();
} catch (Exception e) {
LOG.warn("Problem deleteing payment.", e);
throw new SessionInternalError("Problem deleteing payment");
}
}
/**
* It creates the payment record, makes the calls to the authorization
* processor and updates the invoice if successfull.
*
* @param dto
* @param invoice
* @throws SessionInternalError
*/
public Integer processAndUpdateInvoice(PaymentDTOEx dto,
InvoiceDTO invoice) throws SessionInternalError {
try {
PaymentBL bl = new PaymentBL();
Integer entityId = invoice.getBaseUser().getEntity().getId();
// set the attempt
if (dto.getIsRefund() == 0) {
// take the attempt from the invoice
dto.setAttempt(new Integer(invoice.getPaymentAttempts() + 1));
} else { // is a refund
dto.setAttempt(new Integer(1));
}
// payment notifications require some fields from the related
// invoice
dto.getInvoiceIds().add(invoice.getId());
// process the payment (will create the db record as well, if
// there is any actual processing). Do not process negative
// payments (from negative invoices), unless allowed.
Integer result = null;
if (dto.getAmount().compareTo(BigDecimal.ZERO) > 0) {
result = bl.processPayment(entityId, dto);
} else {
// only process if negative payments are allowed
PreferenceBL preferenceBL = new PreferenceBL();
try {
preferenceBL.set(entityId,
Constants.PREFERENCE_ALLOW_NEGATIVE_PAYMENTS);
} catch (EmptyResultDataAccessException fe) {
// use default
}
if (preferenceBL.getInt() == 1) {
LOG.warn("Processing payment with negative amount " +
dto.getAmount());
result = bl.processPayment(entityId, dto);
} else {
LOG.warn("Skiping payment processing. Payment with " +
"negative amount " + dto.getAmount());
}
}
// only if there was any processing at all
if (result != null) {
// update the dto with the created id
dto.setId(bl.getEntity().getId());
// the balance will be the same as the amount
// if the payment failed, it won't be applied to the invoice
// so the amount will be ignored
dto.setBalance(dto.getAmount());
// after the process, update the payment record
bl.getEntity().setPaymentResult(new PaymentResultDAS().find(result));
// Note: I could use the return of the last call to fetch another
// dto with a different cc number to retry the payment
// get all the invoice's fields updated with this payment
BigDecimal paid = applyPayment(dto, invoice, result.equals(Constants.RESULT_OK));
if (dto.getIsRefund() == 0) {
// now update the link between invoice and payment
bl.createMap(invoice, paid);
}
}
return result;
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
/**
* This is called from the client to process real-time a payment, usually
* cc.
*
* @param dto
* @param invoiceId
* @throws SessionInternalError
*/
public Integer processAndUpdateInvoice(PaymentDTOEx dto,
Integer invoiceId, Integer entityId) throws SessionInternalError {
try {
if (dto.getIsRefund() == 0 && invoiceId != null) {
InvoiceBL bl = new InvoiceBL(invoiceId);
List inv = new ArrayList();
inv.add(invoiceId);
dto.setInvoiceIds(inv);
return processAndUpdateInvoice(dto, bl.getEntity());
} else if (dto.getIsRefund() == 1 &&
dto.getPayment() != null &&
!dto.getPayment().getInvoiceIds().isEmpty()) {
InvoiceBL bl = new InvoiceBL((Integer) dto.
getPayment().getInvoiceIds().get(0));
return processAndUpdateInvoice(dto, bl.getEntity());
} else {
// without an invoice, it's just creating the payment row
// and calling the processor
LOG.info("method called without invoice");
PaymentBL bl = new PaymentBL();
Integer result = bl.processPayment(entityId, dto);
if (result != null) {
bl.getEntity().setPaymentResult(new PaymentResultDAS().find(result));
}
if (result != null && result.equals(Constants.RESULT_OK)) {
// if the configured, pay any unpaid invoices
ConfigurationBL config = new ConfigurationBL(entityId);
if (config.getEntity().getAutoPaymentApplication() == 1) {
bl.automaticPaymentApplication();
}
}
return result;
}
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
/**
* This is called from the client to apply an existing payment to
* an invoice.
*/
public void applyPayment(Integer paymentId, Integer invoiceId) {
LOG.debug("Applying payment " + paymentId + " to invoice "
+ invoiceId);
if (paymentId == null || invoiceId == null) {
LOG.warn("Got null parameters to apply a payment");
return;
}
try {
PaymentBL payment = new PaymentBL(paymentId);
InvoiceBL invoice = new InvoiceBL(invoiceId);
BigDecimal paid = applyPayment(payment.getDTO(), invoice.getEntity(), true);
// link it with the invoice
payment.createMap(invoice.getEntity(), paid);
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
/**
* Applys a payment to an invoice, updating the invoices fields with
* this payment.
* @param payment
* @param invoice
* @param success
* @throws SessionInternalError
*/
public BigDecimal applyPayment(PaymentDTO payment, InvoiceDTO invoice, boolean success) throws SQLException {
BigDecimal totalPaid = BigDecimal.ZERO;
if (invoice != null) {
// set the attempt of the invoice
LOG.debug("applying payment to invoice " + invoice.getId());
if (payment.getIsRefund() == 0) {
//invoice can't take nulls. Default to 1 if so.
invoice.setPaymentAttempts(payment.getAttempt() == null ? new Integer(1) : payment.getAttempt());
}
if (success) {
// update the invoice's balance if applicable
BigDecimal balance = invoice.getBalance();
if (balance != null) {
boolean balanceSign = (balance.compareTo(BigDecimal.ZERO) < 0) ? false : true;
BigDecimal newBalance = null;
if (payment.getIsRefund() == 0) {
newBalance = balance.subtract(payment.getBalance());
// I need the payment record to update its balance
if (payment.getId() == 0) {
throw new SessionInternalError("The ID of the payment to has to be present in the DTO");
}
PaymentBL paymentBL = new PaymentBL(payment.getId());
BigDecimal paymentBalance = payment.getBalance().subtract(balance);
// payment balance cannot be negative, must be at least zero
if (BigDecimal.ZERO.compareTo(paymentBalance) > 0) {
paymentBalance = BigDecimal.ZERO;
}
totalPaid = payment.getBalance().subtract(paymentBalance);
paymentBL.getEntity().setBalance(paymentBalance);
payment.setBalance(paymentBalance);
} else { // refunds add to the invoice
newBalance = balance.add(payment.getAmount());
}
// only level the balance if the original balance wasn't negative
if (newBalance.compareTo(Constants.BIGDECIMAL_ONE_CENT) < 0 && balanceSign) {
// the payment balance was greater than the invoice's
newBalance = BigDecimal.ZERO;
}
invoice.setBalance(newBalance);
LOG.debug("Set invoice balance to: " + invoice.getBalance());
if (BigDecimal.ZERO.compareTo(newBalance) == 0) {
// update the to_process flag if the balance is 0
invoice.setToProcess(new Integer(0));
} else {
// a refund might make this invoice payabale again
invoice.setToProcess(new Integer(1));
}
} else {
// with no balance, we assume the the invoice got all paid
invoice.setToProcess(new Integer(0));
}
// if the user is in the ageing process, she should be out
if (new Integer(invoice.getToProcess()).equals(new Integer(0))) {
AgeingBL ageing = new AgeingBL();
ageing.out(invoice.getBaseUser(), invoice.getId());
}
// update the partner if this customer belongs to one
CustomerDTO customer = invoice.getBaseUser().getCustomer();
if (customer != null && customer.getPartner() != null) {
Partner partner = customer.getPartner();
BigDecimal pBalance = partner.getBalance();
BigDecimal paymentAmount = payment.getAmount();
if (payment.getIsRefund() == 0) {
pBalance = pBalance.add(paymentAmount);
paymentAmount = paymentAmount.add(partner.getTotalPayments());
partner.setTotalPayments(paymentAmount);
} else {
pBalance = pBalance.subtract(paymentAmount);
paymentAmount = paymentAmount.add(partner.getTotalRefunds());
partner.setTotalRefunds(paymentAmount);
}
partner.setBalance(pBalance);
}
}
}
return totalPaid;
}
/**
* This method is called from the client, when a payment needs only to
* be applyed without realtime authorization by a processor
* Finds this invoice entity, creates the payment record and calls the
* apply payment
* Id does suport invoiceId = null because it is possible to get a payment
* that is not paying a specific invoice, a deposit for prepaid models.
*/
public Integer applyPayment(PaymentDTOEx payment, Integer invoiceId)
throws SessionInternalError {
try {
// create the payment record
PaymentBL paymentBl = new PaymentBL();
// set the attempt to an initial value, if the invoice is there,
// it's going to be updated
payment.setAttempt(new Integer(1));
// a payment that is applied, has always the same result
payment.setPaymentResult(new PaymentResultDAS().find(Constants.RESULT_ENTERED));
payment.setBalance(payment.getAmount());
paymentBl.create(payment);
// this is necessary for the caller to get the Id of the
// payment just created
payment.setId(paymentBl.getEntity().getId());
if (payment.getIsRefund() == 0) { // normal payment
if (invoiceId != null) {
// find the invoice
InvoiceBL invoiceBl = new InvoiceBL(invoiceId);
// set the attmpts from the invoice
payment.setAttempt(new Integer(invoiceBl.getEntity().getPaymentAttempts() + 1));
// apply the payment to the invoice
BigDecimal paid = applyPayment(payment, invoiceBl.getEntity(), true);
// link it with the invoice
paymentBl.createMap(invoiceBl.getEntity(), paid);
} else {
// this payment was done without an explicit invoice
// We'll try to link it to invoices with balances then
paymentBl.automaticPaymentApplication();
}
// let know about this payment with an event
PaymentSuccessfulEvent event = new PaymentSuccessfulEvent(
paymentBl.getEntity().getBaseUser().getEntity().getId(),payment);
EventManager.process(event);
} else {
if (payment.getPayment() != null && !payment.getPayment().
getInvoiceIds().isEmpty()) {
// so this refund is linked to a payment, and that payment
// is linked to at least one invoice.
InvoiceBL invoiceBL = new InvoiceBL((Integer) payment.
getPayment().getInvoiceIds().get(0));
applyPayment(payment, invoiceBL.getEntity(), true);
}
}
return paymentBl.getEntity().getId();
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
public PaymentDTOEx getPayment(Integer id, Integer languageId)
throws SessionInternalError {
try {
PaymentBL bl = new PaymentBL(id);
return bl.getDTOEx(languageId);
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
public boolean isMethodAccepted(Integer entityId,
Integer paymentMethodId)
throws SessionInternalError {
if (paymentMethodId == null) {
// if this is a credit card and it has not been
// identified by the first digit, the method will be null
return false;
}
try {
PaymentBL bl = new PaymentBL();
return bl.isMethodAccepted(entityId, paymentMethodId);
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
@Transactional( propagation = Propagation.REQUIRES_NEW )
public Integer processPayout(PaymentDTOEx payment,
Date start, Date end, Integer partnerId, Boolean process)
throws SessionInternalError {
try {
PartnerBL partner = new PartnerBL();
return partner.processPayout(partnerId, start, end, payment,
process);
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
@Transactional( propagation = Propagation.REQUIRES_NEW )
public Boolean processPaypalPayment(Integer invoiceId, String entityEmail,
BigDecimal amount, String currency, Integer paramUserId, String userEmail)
throws SessionInternalError {
if (userEmail == null && invoiceId == null && paramUserId == null) {
LOG.debug("Too much null, returned");
return false;
}
try {
boolean ret = false;
InvoiceBL invoice = null;
Integer entityId = null;
Integer userId = null;
CurrencyBL curr = null;
if (invoiceId != null) {
invoice = new InvoiceBL(invoiceId);
entityId = invoice.getEntity().getBaseUser().getEntity().getId();
userId = invoice.getEntity().getBaseUser().getUserId();
curr = new CurrencyBL(
invoice.getEntity().getCurrency().getId());
} else {
UserBL user = new UserBL();
// identify the user some other way
if (paramUserId != null) {
// easy
userId = paramUserId;
} else {
// find a user by the email address
userId = user.getByEmail(userEmail);
if (userId == null) {
LOG.debug("Could not find a user for email " + userEmail);
return false;
}
}
user = new UserBL(userId);
entityId = user.getEntityId(userId);
curr = new CurrencyBL(user.getCurrencyId());
}
// validate the entity
PreferenceBL pref = new PreferenceBL();
pref.set(entityId, Constants.PREFERENCE_PAYPAL_ACCOUNT);
String paypalAccount = pref.getString();
if (paypalAccount != null && paypalAccount.equals(entityEmail)) {
// now the currency
if (curr.getEntity().getCode().equals(currency)) {
// all good, make the payment
PaymentDTOEx payment = new PaymentDTOEx();
payment.setAmount(amount);
payment.setPaymentMethod(new PaymentMethodDAS().find(Constants.PAYMENT_METHOD_PAYPAL));
payment.setUserId(userId);
payment.setCurrency(curr.getEntity());
payment.setCreateDatetime(Calendar.getInstance().getTime());
payment.setPaymentDate(Calendar.getInstance().getTime());
payment.setIsRefund(new Integer(0));
applyPayment(payment, invoiceId);
ret = true;
// notify the customer that the payment was received
NotificationBL notif = new NotificationBL();
MessageDTO message = notif.getPaymentMessage(entityId,
payment, true);
INotificationSessionBean notificationSess =
(INotificationSessionBean) Context.getBean(
Context.Name.NOTIFICATION_SESSION);
notificationSess.notify(payment.getUserId(), message);
// link to unpaid invoices
// TODO avoid sending two emails
PaymentBL bl = new PaymentBL(payment);
bl.automaticPaymentApplication();
} else {
LOG.debug("wrong currency " + currency);
}
} else {
LOG.debug("wrong entity paypal account " + paypalAccount + " "
+ entityEmail);
}
return new Boolean(ret);
} catch (Exception e) {
throw new SessionInternalError(e);
}
}
/**
* Clients with the right priviliges can update payments with result
* 'entered' that are not linked to an invoice
*/
public void update(Integer executorId, PaymentDTOEx dto)
throws SessionInternalError, EmptyResultDataAccessException {
if (dto.getId() == 0) {
throw new SessionInternalError("ID missing in payment to update");
}
LOG.debug("updateting payment " + dto.getId());
PaymentBL bl = new PaymentBL(dto.getId());
if (new Integer(bl.getEntity().getPaymentResult().getId()).equals(Constants.RESULT_ENTERED)) {
} else {
throw new SessionInternalError("Payment update only available" +
" for entered payments");
}
bl.update(executorId, dto);
}
/**
* Removes a payment-invoice link
*/
public void removeInvoiceLink(Integer mapId) {
PaymentBL payment = new PaymentBL();
payment.removeInvoiceLink(mapId);
}
/**
* Processes the blacklist CSV file specified by filePath.
* It will either add to or replace the existing uploaded
* blacklist for the given entity (company). Returns the number
* of new blacklist entries created.
*/
public int processCsvBlacklist(String filePath, boolean replace,
Integer entityId) throws CsvProcessor.ParseException {
CsvProcessor processor = new CsvProcessor();
return processor.process(filePath, replace, entityId);
}
}