/*
This file is part of Cyclos (www.cyclos.org).
A project of the Social Trade Organisation (www.socialtrade.org).
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.controls.invoices;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import nl.strohalm.cyclos.annotations.Inject;
import nl.strohalm.cyclos.controls.ActionContext;
import nl.strohalm.cyclos.controls.BaseFormAction;
import nl.strohalm.cyclos.controls.payments.SchedulingType;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.accounts.AccountOwner;
import nl.strohalm.cyclos.entities.accounts.AccountType;
import nl.strohalm.cyclos.entities.accounts.Currency;
import nl.strohalm.cyclos.entities.accounts.MemberAccountType;
import nl.strohalm.cyclos.entities.accounts.SystemAccountOwner;
import nl.strohalm.cyclos.entities.accounts.transactions.Invoice;
import nl.strohalm.cyclos.entities.accounts.transactions.InvoicePayment;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferType;
import nl.strohalm.cyclos.entities.accounts.transactions.TransferTypeQuery;
import nl.strohalm.cyclos.entities.customization.fields.CustomFieldValue;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomField;
import nl.strohalm.cyclos.entities.customization.fields.PaymentCustomFieldValue;
import nl.strohalm.cyclos.entities.groups.Group;
import nl.strohalm.cyclos.entities.groups.MemberGroup;
import nl.strohalm.cyclos.entities.groups.MemberGroupSettings;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.entities.settings.LocalSettings;
import nl.strohalm.cyclos.services.accounts.AccountTypeService;
import nl.strohalm.cyclos.services.accounts.CurrencyService;
import nl.strohalm.cyclos.services.accounts.MemberAccountTypeQuery;
import nl.strohalm.cyclos.services.transactions.InvoiceService;
import nl.strohalm.cyclos.services.transactions.TransactionContext;
import nl.strohalm.cyclos.services.transactions.exceptions.SendingInvoiceWithMultipleTransferTypesWithCustomFields;
import nl.strohalm.cyclos.services.transfertypes.TransferTypeService;
import nl.strohalm.cyclos.utils.ActionHelper;
import nl.strohalm.cyclos.utils.RelationshipHelper;
import nl.strohalm.cyclos.utils.ResponseHelper;
import nl.strohalm.cyclos.utils.TimePeriod;
import nl.strohalm.cyclos.utils.binding.BeanBinder;
import nl.strohalm.cyclos.utils.binding.BeanCollectionBinder;
import nl.strohalm.cyclos.utils.binding.DataBinder;
import nl.strohalm.cyclos.utils.binding.PropertyBinder;
import nl.strohalm.cyclos.utils.conversion.AccountOwnerConverter;
import nl.strohalm.cyclos.utils.conversion.CoercionHelper;
import nl.strohalm.cyclos.utils.conversion.HtmlConverter;
import nl.strohalm.cyclos.utils.conversion.IdConverter;
import nl.strohalm.cyclos.utils.conversion.ReferenceConverter;
import nl.strohalm.cyclos.utils.conversion.UnitsConverter;
import nl.strohalm.cyclos.utils.validation.ValidationException;
import org.apache.struts.action.ActionForward;
/**
* Action used to send an invoice
* @author luis
*/
public class SendInvoiceAction extends BaseFormAction {
private AccountTypeService accountTypeService;
private DataBinder<Invoice> dataBinder;
private InvoiceService invoiceService;
private TransferTypeService transferTypeService;
private CurrencyService currencyService;
public AccountTypeService getAccountTypeService() {
return accountTypeService;
}
public DataBinder<Invoice> getDataBinder() {
if (dataBinder == null) {
final BeanBinder<? extends CustomFieldValue> customValueBinder = BeanBinder.instance(PaymentCustomFieldValue.class);
customValueBinder.registerBinder("field", PropertyBinder.instance(PaymentCustomField.class, "field"));
customValueBinder.registerBinder("value", PropertyBinder.instance(String.class, "value", HtmlConverter.instance()));
final LocalSettings localSettings = settingsService.getLocalSettings();
final BeanBinder<Invoice> binder = BeanBinder.instance(Invoice.class);
binder.registerBinder("from", PropertyBinder.instance(AccountOwner.class, "from", AccountOwnerConverter.instance()));
binder.registerBinder("to", PropertyBinder.instance(AccountOwner.class, "to", AccountOwnerConverter.instance()));
binder.registerBinder("transferType", PropertyBinder.instance(TransferType.class, "type", ReferenceConverter.instance(TransferType.class)));
binder.registerBinder("destinationAccountType", PropertyBinder.instance(AccountType.class, "destType", ReferenceConverter.instance(AccountType.class)));
binder.registerBinder("amount", PropertyBinder.instance(BigDecimal.class, "amount", localSettings.getNumberConverter()));
binder.registerBinder("description", PropertyBinder.instance(String.class, "description"));
binder.registerBinder("customValues", BeanCollectionBinder.instance(customValueBinder, "customValues"));
final BeanBinder<InvoicePayment> scheduledPayments = BeanBinder.instance(InvoicePayment.class);
scheduledPayments.registerBinder("date", PropertyBinder.instance(Calendar.class, "date", localSettings.getRawDateConverter()));
scheduledPayments.registerBinder("amount", PropertyBinder.instance(BigDecimal.class, "amount", localSettings.getNumberConverter()));
binder.registerBinder("payments", BeanCollectionBinder.instance(scheduledPayments, "payments"));
dataBinder = binder;
}
return dataBinder;
}
public InvoiceService getInvoiceService() {
return invoiceService;
}
public TransferTypeService getTransferTypeService() {
return transferTypeService;
}
@Inject
public void setAccountTypeService(final AccountTypeService accountTypeService) {
this.accountTypeService = accountTypeService;
}
@Inject
public void setCurrencyService(final CurrencyService currencyService) {
this.currencyService = currencyService;
}
@Inject
public void setInvoiceService(final InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@Inject
public void setTransferTypeService(final TransferTypeService transferTypeService) {
this.transferTypeService = transferTypeService;
}
@Override
protected ActionForward handleDisplay(final ActionContext context) throws Exception {
final HttpServletRequest request = context.getRequest();
final SendInvoiceForm form = context.getForm();
final boolean toSystem = form.isToSystem();
final boolean selectMember = form.isSelectMember();
AccountOwner to;
final Member fromMember = (form.getFrom() == null) ? null : (Member) elementService.load(Long.valueOf(form.getFrom()));
final Element loggedElement = context.getElement();
if (toSystem) {
// System invoice
to = SystemAccountOwner.instance();
} else {
if (!selectMember) {
// Retrieve the member to send invoice for
Member member = null;
final Long memberId = IdConverter.instance().valueOf(form.getTo());
if (memberId != null && memberId != loggedElement.getId()) {
final Element element = elementService.load(memberId, Element.Relationships.USER);
if (element instanceof Member) {
member = (Member) element;
}
}
if (member == null) {
throw new ValidationException();
}
request.setAttribute("member", member);
to = member;
} else {
// The member will be selected later
to = null;
}
}
// If we know who will receive the invoice, get the transfer types or dest account types
if (to != null) {
if (context.isAdmin() && fromMember == null) {
// Only admins may select the transfer type
final TransferTypeQuery query = new TransferTypeQuery();
query.setChannel(Channel.WEB);
query.setContext(TransactionContext.PAYMENT);
query.setFromOwner(to);
query.setToOwner(context.getAccountOwner());
query.setUsePriority(true);
request.setAttribute("transferTypes", transferTypeService.search(query));
} else {
// Members may select the destination account type
final MemberAccountTypeQuery query = new MemberAccountTypeQuery();
query.setOwner(fromMember == null ? (Member) loggedElement.getAccountOwner() : fromMember);
query.setCanPay(to);
final List<? extends AccountType> accountTypes = accountTypeService.search(query);
if (accountTypes.isEmpty()) {
return context.sendError("invoice.error.noAccountType");
}
request.setAttribute("accountTypes", accountTypes);
}
}
// Resolve the possible currencies
final MemberGroup group = getMemberGroup(context);
final List<Currency> currencies;
if (group != null) {
currencies = currencyService.listByMemberGroup(group);
final MemberAccountType defaultAccountType = accountTypeService.getDefault(group, AccountType.Relationships.CURRENCY);
// Preselect the default currency
if (defaultAccountType != null) {
form.setCurrency(CoercionHelper.coerce(String.class, defaultAccountType.getCurrency()));
}
} else {
currencies = currencyService.listAll();
}
request.setAttribute("currencies", currencies);
if (currencies.isEmpty()) {
// No currencies means no possible payment!!!
throw new ValidationException("payment.error.noTransferType");
} else if (currencies.size() == 1) {
// Special case: There is a single currency. The JSP will use this object
request.setAttribute("singleCurrency", currencies.get(0));
}
request.setAttribute("toSystem", toSystem);
request.setAttribute("toMember", !toSystem);
request.setAttribute("selectMember", selectMember);
request.setAttribute("from", fromMember);
final boolean useTransferType = context.isAdmin() && fromMember == null;
request.setAttribute("useTransferType", useTransferType);
// Check whether scheduled payments may be performed
boolean allowsScheduling = false;
boolean allowsMultipleScheduling = false;
if (context.isAdmin() && fromMember == null) {
allowsScheduling = true;
allowsMultipleScheduling = true;
} else {
MemberGroup memberGroup;
if (fromMember == null) {
memberGroup = ((Member) context.getAccountOwner()).getMemberGroup();
} else {
memberGroup = fromMember.getMemberGroup();
}
final MemberGroupSettings memberSettings = memberGroup.getMemberSettings();
allowsScheduling = memberSettings.isAllowsScheduledPayments();
allowsMultipleScheduling = memberSettings.isAllowsMultipleScheduledPayments();
}
if (allowsScheduling) {
request.setAttribute("allowsScheduling", allowsScheduling);
request.setAttribute("allowsMultipleScheduling", allowsMultipleScheduling);
final Collection<SchedulingType> schedulingTypes = EnumSet.of(SchedulingType.IMMEDIATELY, SchedulingType.SINGLE_FUTURE);
if (allowsMultipleScheduling) {
schedulingTypes.add(SchedulingType.MULTIPLE_FUTURE);
}
request.setAttribute("schedulingTypes", schedulingTypes);
request.setAttribute("schedulingFields", Arrays.asList(TimePeriod.Field.MONTHS, TimePeriod.Field.WEEKS, TimePeriod.Field.DAYS));
}
return context.getInputForward();
}
@Override
protected ActionForward handleSubmit(final ActionContext context) throws Exception {
final SendInvoiceForm form = context.getForm();
final boolean fromProfile = !form.isToSystem() && !form.isSelectMember();
try {
final Invoice invoice = invoiceService.send(resolveInvoice(context));
context.sendMessage("invoice.sent");
ActionForward forward = null;
final Map<String, Object> params = new HashMap<String, Object>();
if (fromProfile) {
forward = context.findForward("profile");
params.put("memberId", invoice.getToMember().getId());
} else {
forward = context.findForward("newInvoice");
}
final Member fromMember = invoice.getFromMember();
if (fromMember != null && !fromMember.equals(context.getMember())) {
// From another member
params.put("from", form.getFrom());
} else if (fromProfile) {
params.put("to", form.getTo());
}
if (form.isToSystem()) {
params.put("toSystem", true);
} else if (form.isSelectMember()) {
params.put("selectMember", true);
}
return ActionHelper.redirectWithParams(context.getRequest(), forward, params);
} catch (final SendingInvoiceWithMultipleTransferTypesWithCustomFields e) {
return context.sendError("invoice.error.sendingWithMultipleTransferTypesWithCustomFields");
}
}
@Override
protected ActionForward handleValidation(final ActionContext context) {
try {
final Invoice invoice = resolveInvoice(context);
invoiceService.validate(invoice);
// Retrive and fetch the destination account type
AccountType accountType = invoice.getDestinationAccountType();
if (accountType == null) {
final TransferType tt = transferTypeService.load(invoice.getTransferType().getId(), RelationshipHelper.nested(TransferType.Relationships.TO, AccountType.Relationships.CURRENCY));
accountType = tt.getTo();
} else {
accountType = accountTypeService.load(accountType.getId());
}
// If the validation passed, resolve the confirmation message
final LocalSettings localSettings = settingsService.getLocalSettings();
final UnitsConverter unitsConverter = localSettings.getUnitsConverter(accountType.getCurrency().getPattern());
final AccountOwner toOwner = invoice.getTo();
final boolean toSystem = toOwner instanceof SystemAccountOwner;
// Retrieve the message arguments
String to;
if (toSystem) {
to = localSettings.getApplicationUsername();
} else {
final Member member = elementService.load(((Member) toOwner).getId());
to = member.getName();
}
final String amount = unitsConverter.toString(invoice.getAmount());
final String confirmationKey = "invoice.sendConfirmationMessage";
final Map<String, Object> fields = new HashMap<String, Object>();
fields.put("confirmationMessage", context.message(confirmationKey, to, amount));
responseHelper.writeStatus(context.getResponse(), ResponseHelper.Status.SUCCESS, fields);
} catch (final ValidationException e) {
responseHelper.writeValidationErrors(context.getResponse(), e);
}
return null;
}
private MemberGroup getMemberGroup(final ActionContext context) {
final SendInvoiceForm form = context.getForm();
final Long fromId = IdConverter.instance().valueOf(form.getFrom());
final Long toId = IdConverter.instance().valueOf(form.getTo());
Group group = null;
if (fromId == null && toId == null) {
group = context.getGroup();
} else if (fromId != null) {
final Element element = elementService.load(fromId, Element.Relationships.GROUP);
group = element.getGroup();
} else {
final Element element = elementService.load(toId, Element.Relationships.GROUP);
group = element.getGroup();
}
if (group instanceof MemberGroup) {
return (MemberGroup) group;
}
return null;
}
private Invoice resolveInvoice(final ActionContext context) {
final SendInvoiceForm form = context.getForm();
final Invoice invoice = getDataBinder().readFromString(form);
if ((context.isMember() && invoice.getFromMember() == null) || context.isOperator()) {
invoice.setFrom(context.getAccountOwner());
}
return invoice;
}
}