Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
net.solarnetwork.central.user.billing.snf.DefaultSnfInvoicingSystem Maven / Gradle / Ivy
/* ==================================================================
* DefaultSnfInvoicingSystem.java - 1/11/2021 11:38:22 AM
*
* Copyright 2021 SolarNetwork.net Dev Team
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
* ==================================================================
*/
package net.solarnetwork.central.user.billing.snf;
import static java.lang.String.format;
import static net.solarnetwork.central.user.billing.snf.SnfBillingUtils.invoiceForSnfInvoice;
import static net.solarnetwork.central.user.billing.snf.SnfBillingUtils.usageMetadata;
import static net.solarnetwork.central.user.billing.snf.domain.InvoiceItemType.Usage;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.DATUM_DAYS_STORED_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.DATUM_OUT_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.DATUM_PROPS_IN_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.DNP3_DATA_POINTS_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.FLUX_DATA_IN_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.FLUX_DATA_OUT_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.INSTRUCTIONS_ISSUED_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OAUTH_CLIENT_CREDENTIALS_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OCPP_CHARGERS_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_GROUPS_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.NodeUsages.OSCP_CAPACITY_KEY;
import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.META_AVAILABLE_CREDIT;
import static net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem.newItem;
import static net.solarnetwork.util.ObjectUtils.requireNonNullArgument;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.MimeType;
import net.solarnetwork.central.RepeatableTaskException;
import net.solarnetwork.central.dao.VersionedMessageDao;
import net.solarnetwork.central.dao.VersionedMessageDao.VersionedMessages;
import net.solarnetwork.central.security.AuthorizationException;
import net.solarnetwork.central.security.AuthorizationException.Reason;
import net.solarnetwork.central.support.VersionedMessageDaoMessageSource;
import net.solarnetwork.central.user.billing.domain.Invoice;
import net.solarnetwork.central.user.billing.snf.dao.AccountDao;
import net.solarnetwork.central.user.billing.snf.dao.NodeUsageDao;
import net.solarnetwork.central.user.billing.snf.dao.SnfInvoiceDao;
import net.solarnetwork.central.user.billing.snf.dao.SnfInvoiceItemDao;
import net.solarnetwork.central.user.billing.snf.dao.SnfInvoiceNodeUsageDao;
import net.solarnetwork.central.user.billing.snf.dao.TaxCodeDao;
import net.solarnetwork.central.user.billing.snf.domain.Account;
import net.solarnetwork.central.user.billing.snf.domain.AccountBalance;
import net.solarnetwork.central.user.billing.snf.domain.Address;
import net.solarnetwork.central.user.billing.snf.domain.InvoiceItemType;
import net.solarnetwork.central.user.billing.snf.domain.NamedCost;
import net.solarnetwork.central.user.billing.snf.domain.NodeUsage;
import net.solarnetwork.central.user.billing.snf.domain.NodeUsages;
import net.solarnetwork.central.user.billing.snf.domain.SnfInvoice;
import net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceFilter;
import net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceItem;
import net.solarnetwork.central.user.billing.snf.domain.SnfInvoiceNodeUsage;
import net.solarnetwork.central.user.billing.snf.domain.TaxCode;
import net.solarnetwork.central.user.billing.snf.domain.TaxCodeFilter;
import net.solarnetwork.central.user.billing.snf.domain.UsageInfo;
import net.solarnetwork.central.user.billing.snf.util.SnfBillingUtils;
import net.solarnetwork.central.user.billing.support.LocalizedInvoice;
import net.solarnetwork.central.user.domain.UserLongPK;
import net.solarnetwork.domain.Result;
import net.solarnetwork.service.TemplateRenderer;
/**
* Default implementation of {@link SnfInvoicingSystem}.
*
* @author matt
* @version 1.5
*/
public class DefaultSnfInvoicingSystem implements SnfInvoicingSystem, SnfTaxCodeResolver {
/** The message bundle name to use for versioned messages. */
public static final String MESSAGE_BUNDLE_NAME = "snf.billing";
/** The message bundle name to use for global versioned messages. */
public static final String GLOBAL_MESSAGE_BUNDLE_NAME = "snf.global";
/** The default {@code deliveryTimeoutSecs} property value. */
public static final int DEFAULT_DELIVERY_TIMEOUT = 60;
/** The invoice ID used for dry-run (draft) invoice generation. */
public static final Long DRAFT_INVOICE_ID = Long.valueOf(Invoice.DRAFT_INVOICE_ID);
/** The invoice number used for dry-run (draft) invoice generation. */
public static final String DRAFT_INVOICE_NUMBER = SnfBillingUtils.invoiceNumForId(DRAFT_INVOICE_ID);
private static final String[] MESSAGE_BUNDLE_NAMES = new String[] { GLOBAL_MESSAGE_BUNDLE_NAME,
MESSAGE_BUNDLE_NAME };
private final Logger log = LoggerFactory.getLogger(getClass());
private final AccountDao accountDao;
private final SnfInvoiceDao invoiceDao;
private final SnfInvoiceItemDao invoiceItemDao;
private final SnfInvoiceNodeUsageDao invoiceNodeUsageDao;
private final NodeUsageDao usageDao;
private final TaxCodeDao taxCodeDao;
private final VersionedMessageDao messageDao;
private SnfTaxCodeResolver taxCodeResolver;
private List deliveryServices;
private List rendererResolvers;
private Cache messageCache;
private String datumPropertiesInKey = DATUM_PROPS_IN_KEY;
private String datumOutKey = DATUM_OUT_KEY;
private String datumDaysStoredKey = DATUM_DAYS_STORED_KEY;
private String instructionsIssuedKey = INSTRUCTIONS_ISSUED_KEY;
private String fluxDataInKey = FLUX_DATA_IN_KEY;
private String fluxDataOutKey = FLUX_DATA_OUT_KEY;
private String ocppChargersKey = OCPP_CHARGERS_KEY;
private String oscpCapacityGroupsKey = OSCP_CAPACITY_GROUPS_KEY;
private String oscpCapacityKey = OSCP_CAPACITY_KEY;
private String dnp3DataPointsKey = DNP3_DATA_POINTS_KEY;
private String oauthClientCredentialsKey = OAUTH_CLIENT_CREDENTIALS_KEY;
private String accountCreditKey = AccountBalance.ACCOUNT_CREDIT_KEY;
private int deliveryTimeoutSecs = DEFAULT_DELIVERY_TIMEOUT;
/**
* Constructor.
*
* @param accountDao
* the account DAO
* @param invoiceDao
* the invoice DAO
* @param invoiceItemDao
* the invoice item DAO
* @param invoiceNodeUsageDao
* the invoice node usage DAO
* @param usageDao
* the usage DAO
* @param taxCodeDao
* the tax code DAO
* @param messageDao
* the message DAO
* @throws IllegalArgumentException
* if any argument is {@literal null}
*/
public DefaultSnfInvoicingSystem(AccountDao accountDao, SnfInvoiceDao invoiceDao,
SnfInvoiceItemDao invoiceItemDao, SnfInvoiceNodeUsageDao invoiceNodeUsageDao,
NodeUsageDao usageDao, TaxCodeDao taxCodeDao, VersionedMessageDao messageDao) {
super();
this.accountDao = requireNonNullArgument(accountDao, "accountDao");
this.invoiceDao = requireNonNullArgument(invoiceDao, "invoiceDao");
this.invoiceItemDao = requireNonNullArgument(invoiceItemDao, "invoiceItemDao");
this.invoiceNodeUsageDao = requireNonNullArgument(invoiceNodeUsageDao, "invoiceNodeUsageDao");
this.usageDao = requireNonNullArgument(usageDao, "usageDao");
this.taxCodeDao = requireNonNullArgument(taxCodeDao, "taxCodeDao");
this.messageDao = requireNonNullArgument(messageDao, "messageDao");
}
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
@Override
public Account accountForUser(Long userId) {
return accountDao.getForUser(userId);
}
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
@Override
public SnfInvoice findLatestInvoiceForAccount(UserLongPK accountId) {
SnfInvoiceFilter filter = SnfInvoiceFilter.forAccount(accountId.getId());
filter.setIgnoreCreditOnly(true);
net.solarnetwork.dao.FilterResults results = invoiceDao
.findFiltered(filter, SnfInvoiceDao.SORT_BY_INVOICE_DATE_DESCENDING, 0, 1);
Iterator itr = (results != null ? results.iterator() : null);
return (itr != null && itr.hasNext() ? itr.next() : null);
}
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
@Override
public SnfInvoice generateInvoice(Long userId, LocalDate startDate, LocalDate endDate,
SnfInvoicingSystem.InvoiceGenerationOptions options) {
// get account
Account account = accountDao.getForUser(userId, endDate);
if ( account == null ) {
throw new AuthorizationException(Reason.UNKNOWN_OBJECT, userId);
}
final boolean dryRun = (options != null ? options.isDryRun() : false);
final boolean useCredit = (options != null ? options.isUseAccountCredit()
: dryRun ? false : true);
// query for usage
final List usages = usageDao.findUsageForAccount(userId, startDate, endDate);
if ( usages == null || usages.isEmpty() ) {
// no invoice necessary
return null;
}
// check for no-cost usage, and remove
for ( Iterator itr = usages.iterator(); itr.hasNext(); ) {
NodeUsage usage = itr.next();
if ( usage.getTotalCost().compareTo(BigDecimal.ZERO) == 0 ) {
itr.remove();
}
}
if ( usages.isEmpty() ) {
// only zero cost usage, no invoice necessary
return null;
}
// turn usage into invoice items
final SnfInvoice invoice = new SnfInvoice(account.getId().getId(), userId, Instant.now());
invoice.setAddress(account.getAddress());
invoice.setCurrencyCode(account.getCurrencyCode());
invoice.setStartDate(startDate);
invoice.setEndDate(endDate);
// query for node usage counts
final List nodeUsages = usageDao.findNodeUsageForAccount(userId, startDate, endDate);
// for dryRun support, we generate a negative invoice ID
final UserLongPK invoiceId = (dryRun ? new UserLongPK(userId, DRAFT_INVOICE_ID)
: invoiceDao.save(invoice));
invoice.getId().setId(invoiceId.getId()); // for return
List items = new ArrayList<>(usages.size());
for ( NodeUsage usage : usages ) {
if ( usage.getTotalCost().compareTo(BigDecimal.ZERO) < 1 ) {
// no cost for this node
log.debug("No usage cost for node {} invoice date {}", usage.getId(), startDate);
continue;
}
final Map> tiersBreakdown = usage.getTiersCostBreakdown();
final Map usageInfo = usage.getUsageInfo();
if ( usage.getDatumPropertiesIn().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, datumPropertiesInKey,
new BigDecimal(usage.getDatumPropertiesIn()), usage.getDatumPropertiesInCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, DATUM_PROPS_IN_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getDatumOut().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, datumOutKey,
new BigDecimal(usage.getDatumOut()), usage.getDatumOutCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, DATUM_OUT_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getDatumDaysStored().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, datumDaysStoredKey,
new BigDecimal(usage.getDatumDaysStored()), usage.getDatumDaysStoredCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, DATUM_DAYS_STORED_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getInstructionsIssued().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, instructionsIssuedKey,
new BigDecimal(usage.getInstructionsIssued()),
usage.getInstructionsIssuedCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, INSTRUCTIONS_ISSUED_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getFluxDataIn().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, fluxDataInKey,
new BigDecimal(usage.getFluxDataIn()), usage.getFluxDataInCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, FLUX_DATA_IN_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getFluxDataOut().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, fluxDataOutKey,
new BigDecimal(usage.getFluxDataOut()), usage.getFluxDataOutCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, FLUX_DATA_OUT_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getOcppChargers().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, ocppChargersKey,
new BigDecimal(usage.getOcppChargers()), usage.getOcppChargersCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OCPP_CHARGERS_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getOscpCapacityGroups().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, oscpCapacityGroupsKey,
new BigDecimal(usage.getOscpCapacityGroups()),
usage.getOscpCapacityGroupsCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OSCP_CAPACITY_GROUPS_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getOscpCapacity().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, oscpCapacityKey,
new BigDecimal(usage.getOscpCapacity()), usage.getOscpCapacityCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OSCP_CAPACITY_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getDnp3DataPoints().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, dnp3DataPointsKey,
new BigDecimal(usage.getDnp3DataPoints()), usage.getDnp3DataPointsCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, DNP3_DATA_POINTS_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
if ( usage.getOauthClientCredentials().compareTo(BigInteger.ZERO) > 0 ) {
SnfInvoiceItem item = newItem(invoiceId.getId(), Usage, oauthClientCredentialsKey,
new BigDecimal(usage.getOauthClientCredentials()),
usage.getOauthClientCredentialsCost());
item.setMetadata(usageMetadata(usageInfo, tiersBreakdown, OAUTH_CLIENT_CREDENTIALS_KEY));
if ( !dryRun ) {
invoiceItemDao.save(item);
}
items.add(item);
}
}
invoice.setItems(new LinkedHashSet<>(items));
List taxItems = computeInvoiceTaxItems(invoice);
for ( SnfInvoiceItem taxItem : taxItems ) {
if ( !dryRun ) {
invoiceItemDao.save(taxItem);
}
invoice.getItems().add(taxItem);
}
// claim credit, if available
if ( useCredit ) {
BigDecimal invoiceTotal = invoice.getTotalAmount();
if ( invoiceTotal.compareTo(BigDecimal.ZERO) > 0 ) {
BigDecimal credit = accountDao.claimAccountBalanceCredit(invoice.getAccountId(),
invoiceTotal);
if ( credit.compareTo(BigDecimal.ZERO) > 0 ) {
AccountBalance balance = accountDao.getBalanceForUser(invoice.getUserId());
SnfInvoiceItem creditItem = SnfInvoiceItem.newItem(invoice, InvoiceItemType.Credit,
accountCreditKey, BigDecimal.ONE, credit.negate());
creditItem.setMetadata(Collections.singletonMap(META_AVAILABLE_CREDIT,
balance.getAvailableCredit().toPlainString()));
if ( !dryRun ) {
invoiceItemDao.save(creditItem);
}
invoice.getItems().add(creditItem);
}
}
}
// populate node usages
if ( nodeUsages != null ) {
Collections.sort(nodeUsages, NodeUsage.SORT_BY_NODE_ID);
List invoiceNodeUsages = new ArrayList<>(nodeUsages.size());
for ( NodeUsage nodeUsage : nodeUsages ) {
SnfInvoiceNodeUsage u = new SnfInvoiceNodeUsage(invoiceId.getId(), nodeUsage.getId(),
invoice.getCreated(), nodeUsage.getDescription(),
nodeUsage.getDatumPropertiesIn(), nodeUsage.getDatumOut(),
nodeUsage.getDatumDaysStored(), nodeUsage.getInstructionsIssued(),
nodeUsage.getFluxDataIn());
invoiceNodeUsages.add(u);
if ( !dryRun ) {
invoiceNodeUsageDao.save(u);
}
}
invoice.setUsages(new LinkedHashSet<>(invoiceNodeUsages));
} else {
invoice.setUsages(Collections.emptySet());
}
log.info("Generated invoice for user {} for date {}: {}", userId, startDate, invoice);
return invoice;
}
@Transactional(readOnly = false, propagation = Propagation.REQUIRED)
@Override
public boolean deliverInvoice(final UserLongPK invoiceId) {
// get account
final Account account = accountDao.getForUser(invoiceId.getUserId());
if ( account == null ) {
throw new AuthorizationException(Reason.UNKNOWN_OBJECT, invoiceId.getUserId());
}
final SnfInvoice invoice = invoiceDao.get(invoiceId);
if ( invoice == null ) {
throw new AuthorizationException(Reason.UNKNOWN_OBJECT, invoiceId);
}
SnfInvoiceDeliverer deliverer = invoiceDeliverer(invoice.getUserId());
if ( deliverer == null ) {
String msg = format("No invoice delivery service available to delivery invoice %d",
invoiceId.getId());
log.error(msg);
throw new RepeatableTaskException(msg);
}
// TODO: support configuration for account
try {
final int maxSeconds = getDeliveryTimeoutSecs();
CompletableFuture> future = deliverer.deliverInvoice(invoice, account, null);
Result result;
if ( maxSeconds > 0 ) {
result = future.get(maxSeconds, TimeUnit.SECONDS);
} else {
result = future.get();
}
return (result != null && result.getSuccess() != null && result.getSuccess().booleanValue());
} catch ( TimeoutException e ) {
throw new RepeatableTaskException("Tiemout delivering invoice", e);
} catch ( Exception e ) {
Throwable root = e;
while ( root.getCause() != null ) {
root = root.getCause();
}
if ( root instanceof IOException ) {
throw new RepeatableTaskException("Communication error delivering invoice.", e);
}
if ( !(e instanceof RuntimeException) ) {
e = new RuntimeException(e);
}
throw (RuntimeException) e;
}
}
private SnfInvoiceDeliverer invoiceDeliverer(Long userId) {
Iterable iterable = getDeliveryServices();
Iterator itr = (iterable != null ? iterable.iterator() : null);
return (itr != null && itr.hasNext() ? itr.next() : null);
}
@Override
public MessageSource messageSourceForInvoice(SnfInvoice invoice) {
requireNonNullArgument(invoice, "invoice");
final Instant version = invoice.getStartDate().atStartOfDay(invoice.getTimeZone()).toInstant();
return messageSourceForDate(version);
}
@Override
public MessageSource messageSourceForDate(Instant date) {
return new VersionedMessageDaoMessageSource(messageDao, MESSAGE_BUNDLE_NAMES, date,
messageCache);
}
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
@Override
public Resource renderInvoice(SnfInvoice invoice, MimeType outputType, Locale locale) {
TemplateRenderer renderer = renderer(invoice, outputType, locale);
VersionedMessageDaoMessageSource messageSource = (VersionedMessageDaoMessageSource) messageSourceForInvoice(
invoice);
LocalizedInvoice localizedInvoice = new LocalizedInvoice(
invoiceForSnfInvoice(invoice, messageSource, locale), locale);
Properties messages = messageSource.propertiesForLocale(locale);
Map parameters = new LinkedHashMap<>(4);
parameters.put("invoice", localizedInvoice);
parameters.put("address", invoice.getAddress());
parameters.put("messages", messages);
try (ByteArrayOutputStream out = new ByteArrayOutputStream(4096)) {
renderer.render(locale, outputType, parameters, out);
return invoiceResource(out.toByteArray(), invoice, messageSource, outputType, locale);
} catch ( IOException e ) {
throw new RuntimeException(e);
}
}
private Resource invoiceResource(byte[] data, SnfInvoice invoice, MessageSource messageSource,
MimeType outputType, Locale locale) {
String extension = ".txt";
if ( outputType.isCompatibleWith(MimeType.valueOf("application/pdf")) ) {
extension = ".pdf";
} else {
extension = "." + outputType.getSubtype();
}
Object[] filenameArgs = new Object[] {
DRAFT_INVOICE_ID.equals(invoice.getId().getId())
? messageSource.getMessage("draftInvoice", null, "DRAFT", locale)
: SnfBillingUtils.invoiceNumForId(invoice.getId().getId()),
YearMonth.from(invoice.getStartDate()).toString(), extension };
String filename = messageSource.getMessage("invoice.filename", filenameArgs,
"SolarNetwork Invoice {0} - {1}{2}", locale);
return new ByteArrayResource(data) {
@Override
public String getFilename() {
return filename;
}
};
}
/**
* Resolve tax codes for a given invoice.
*
*
* This implementation creates tax zone names out of the invoice's address,
* with the following patterns:
*
*
*
* country
via {@link Address#getCountry()} (required)
* country.state
via {@link Address#getCountry()} and
* {@link Address#getStateOrProvince()} (only if state available)
*
*
*
* The tax date is resolved as the invoice's start date, or the current date
* if that is not available.
*
*
* {@inheritDoc}
*/
@Override
public TaxCodeFilter taxCodeFilterForInvoice(SnfInvoice invoice) {
SnfTaxCodeResolver service = this.taxCodeResolver;
if ( service != null ) {
return service.taxCodeFilterForInvoice(invoice);
}
if ( invoice == null ) {
throw new IllegalArgumentException("The invoice argument must be provided.");
}
Address addr = invoice.getAddress();
if ( addr == null ) {
throw new IllegalArgumentException("The invoice must provide an address.");
}
if ( addr.getCountry() == null || addr.getCountry().trim().isEmpty() ) {
throw new IllegalArgumentException("The address must provide a country.");
}
List zones = new ArrayList<>(2);
zones.add(addr.getCountry());
if ( addr.getStateOrProvince() != null && !addr.getStateOrProvince().trim().isEmpty() ) {
zones.add(String.format("%s.%s", addr.getCountry(), addr.getStateOrProvince()));
}
ZoneId tz = invoice.getTimeZone();
if ( tz == null ) {
throw new IllegalArgumentException("The invoice must provide a time zone.");
}
TaxCodeFilter filter = new TaxCodeFilter();
filter.setZones(zones.toArray(new String[zones.size()]));
LocalDate date = invoice.getStartDate();
if ( date == null ) {
date = LocalDate.now();
}
filter.setDate(date.atStartOfDay(tz).toInstant());
return filter;
}
private TemplateRenderer renderer(SnfInvoice invoice, MimeType mimeType, Locale locale) {
if ( rendererResolvers != null ) {
for ( SnfInvoiceRendererResolver resolver : rendererResolvers ) {
TemplateRenderer r = resolver.rendererForInvoice(invoice, mimeType, locale);
if ( r != null ) {
return r;
}
}
}
String msg = String.format("MIME %s not supported for invoice rendering.", mimeType);
throw new IllegalArgumentException(msg);
}
/**
* Compute the set of tax items for a given invoice.
*
*
* Existing tax items are ignored in the given invoice. The invoice is not
* mutated in any way.
*
*
* @param invoice
* the invoice to compute tax items for
* @return the list of tax items, never {@literal null}
*/
public List computeInvoiceTaxItems(SnfInvoice invoice) {
SnfTaxCodeResolver taxResolver = (taxCodeResolver != null ? taxCodeResolver : this);
TaxCodeFilter taxFilter = taxResolver.taxCodeFilterForInvoice(invoice);
List taxItems = new ArrayList<>(8);
if ( taxFilter != null ) {
net.solarnetwork.dao.FilterResults taxes = taxCodeDao.findFiltered(taxFilter,
null, null, null);
if ( taxes != null && taxes.getReturnedResultCount() > 0 ) {
Map taxAmounts = new LinkedHashMap<>(taxes.getReturnedResultCount());
for ( SnfInvoiceItem item : invoice.getItems() ) {
final InvoiceItemType itemType = item.getItemType();
if ( itemType == InvoiceItemType.Tax ) {
continue;
}
final String itemKey = item.getKey();
final BigDecimal itemAmount = item.getAmount();
if ( itemKey == null || itemAmount == null ) {
continue;
}
for ( TaxCode tax : taxes ) {
final String taxCode = tax.getCode();
final BigDecimal taxRate = tax.getRate();
if ( taxCode == null || taxRate == null ) {
continue;
}
if ( itemKey.equalsIgnoreCase(tax.getItemKey()) ) {
taxAmounts.merge(taxCode, taxRate.multiply(itemAmount), BigDecimal::add);
}
}
}
for ( Map.Entry me : taxAmounts.entrySet() ) {
SnfInvoiceItem taxItem = newItem(invoice, InvoiceItemType.Tax, me.getKey(),
BigDecimal.ONE, me.getValue().setScale(2, RoundingMode.HALF_UP));
taxItems.add(taxItem);
}
}
}
return taxItems;
}
/**
* Get the item key for datum properties input usage.
*
* @return the key; defaults to {@link NodeUsage#DATUM_PROPS_IN_KEY}
*/
public String getDatumPropertiesInKey() {
return datumPropertiesInKey;
}
/**
* Set the item key for datum properties input usage.
*
* @param datumPropertiesInKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
*/
public void setDatumPropertiesInKey(String datumPropertiesInKey) {
if ( datumPropertiesInKey == null ) {
throw new IllegalArgumentException("The datumPropertiesInKey argumust must not be null.");
}
this.datumPropertiesInKey = datumPropertiesInKey;
}
/**
* Get the item key for datum output usage.
*
* @return the key; defaults to {@link NodeUsage#DATUM_OUT_KEY}
*/
public String getDatumOutKey() {
return datumOutKey;
}
/**
* Set the item key for datum output usage.
*
* @param datumOutKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
*/
public void setDatumOutKey(String datumOutKey) {
if ( datumOutKey == null ) {
throw new IllegalArgumentException("The datumOutKey argumust must not be null.");
}
this.datumOutKey = datumOutKey;
}
/**
* Get the item key for datum days stored usage.
*
* @return the key; defaults to {@link NodeUsage#DATUM_DAYS_STORED_KEY}
*/
public String getDatumDaysStoredKey() {
return datumDaysStoredKey;
}
/**
* Set the item key for datum days stored usage.
*
* @param datumDaysStoredKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
*/
public void setDatumDaysStoredKey(String datumDaysStoredKey) {
if ( datumDaysStoredKey == null ) {
throw new IllegalArgumentException("The datumDaysStoredKey argumust must not be null.");
}
this.datumDaysStoredKey = datumDaysStoredKey;
}
/**
* Get the instructions issued key.
*
* @return the key
* @since 1.3
*/
public String getInstructionsIssuedKey() {
return instructionsIssuedKey;
}
/**
* Set the instructions issued key.
*
* @param instructionsIssuedKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.3
*/
public void setInstructionsIssuedKey(String instructionsIssuedKey) {
this.instructionsIssuedKey = requireNonNullArgument(instructionsIssuedKey,
"instructionsIssuedKey");
}
/**
* Get the item key for SolarFlux data in.
*
* @return the the key, never {@literal null}; defaults to
* {@link NodeUsage#FLUX_DATA_IN_KEY}
* @since 1.5
*/
public final String getFluxDataInKey() {
return fluxDataInKey;
}
/**
* Get the item key for SolarFlux data in.
*
* @param fluxDataInKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.5
*/
public final void setFluxDataInKey(String fluxDataInKey) {
this.fluxDataInKey = requireNonNullArgument(fluxDataInKey, "fluxDataInKey");
}
/**
* Get the item key for SolarFlux data out.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsage#FLUX_DATA_OUT_KEY}
* @since 1.5
*/
public final String getFluxDataOutKey() {
return fluxDataOutKey;
}
/**
* Set the item key for SolarFlux data out.
*
* @param fluxDataOutKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.5
*/
public final void setFluxDataOutKey(String fluxDataOutKey) {
this.fluxDataOutKey = requireNonNullArgument(fluxDataOutKey, "fluxDataOutKey");
}
/**
* Get the item key for OCPP chargers.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsage#OCPP_CHARGERS_KEY}
* @since 1.1
*/
public String getOcppChargersKey() {
return ocppChargersKey;
}
/**
* Set the item key for OCPP chargers.
*
* @param ocppChargersKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.1
*/
public void setOcppChargersKey(String ocppChargersKey) {
this.ocppChargersKey = requireNonNullArgument(ocppChargersKey, "ocppChargersKey");
}
/**
* Get the item key for OSCP Capacity Groups.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsages#OSCP_CAPACITY_GROUPS_KEY}
* @since 1.1
*/
public String getOscpCapacityGroupsKey() {
return oscpCapacityGroupsKey;
}
/**
* Set the item key for OSCP Capacity Groups.
*
* @param oscpCapacityGroupsKey
* the oscpCapacityGroupsKey to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.1
*/
public void setOscpCapacityGroupsKey(String oscpCapacityGroupsKey) {
this.oscpCapacityGroupsKey = requireNonNullArgument(oscpCapacityGroupsKey,
"oscpCapacityGroupsKey");
}
/**
* Get the item key for OSCP Capacity.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsages#OSCP_CAPACITY_KEY}
* @since 1.4
*/
public String getOscpCapacityKey() {
return oscpCapacityKey;
}
/**
* Set the item key for OSCP Capacity.
*
* @param oscpCapacityKey
* the oscpCapacityKey to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.4
*/
public void setOscpCapacityKey(String oscpCapacityKey) {
this.oscpCapacityKey = requireNonNullArgument(oscpCapacityKey, "oscpCapacityKey");
}
/**
* Get the item key for DNP3 Data Points.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsage#DNP3_DATA_POINTS_KEY}
* @since 1.2
*/
public String getDnp3DataPointsKey() {
return dnp3DataPointsKey;
}
/**
* Set the item key for DNP3 Data Points.
*
* @param dnp3DataPointsKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.2
*/
public void setDnp3DataPointsKey(String dnp3DataPointsKey) {
this.dnp3DataPointsKey = requireNonNullArgument(dnp3DataPointsKey, "dnp3DataPointsKey");
}
/**
* Get the item key for OAuth client credentials.
*
* @return the key, never {@literal null}; defaults to
* {@link NodeUsage#OAUTH_CLIENT_CREDENTIALS_KEY}
* @since 1.5
*/
public final String getOauthClientCredentialsKey() {
return oauthClientCredentialsKey;
}
/**
* Set the item key for OAuth client credentials.
*
* @param oauthClientCredentialsKey
* the key to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
* @since 1.5
*/
public final void setOauthClientCredentialsKey(String oauthClientCredentialsKey) {
this.oauthClientCredentialsKey = requireNonNullArgument(oauthClientCredentialsKey,
"oauthClientCredentialsKey");
}
/**
* Get the item key for account credit.
*
* @return the key; defaults to {@link AccountBalance#ACCOUNT_CREDIT_KEY}
*/
public String getAccountCreditKey() {
return accountCreditKey;
}
/**
* Set the item key for account credit.
*
* @param accountCreditKey
* the accountCreditKey to set
* @throws IllegalArgumentException
* if the argument is {@literal null}
*/
public void setAccountCreditKey(String accountCreditKey) {
if ( accountCreditKey == null ) {
throw new IllegalArgumentException("The accountCreditKey argumust must not be null.");
}
this.accountCreditKey = accountCreditKey;
}
/**
* Get the optional tax code resolver service to use.
*
*
* If this property is not configured or the service is not available at
* runtime, this class will act as its own resolver.
*
*
* @return the service
*/
public SnfTaxCodeResolver getTaxCodeResolver() {
return taxCodeResolver;
}
/**
* Set the optional tax code resolver service to use.
*
* @param taxCodeResolver
* the service to set
*/
public void setTaxCodeResolver(SnfTaxCodeResolver taxCodeResolver) {
this.taxCodeResolver = taxCodeResolver;
}
/**
* Get the optional message cache.
*
* @return the cache
*/
public Cache getMessageCache() {
return messageCache;
}
/**
* Set the optional message cache.
*
* @param messageCache
* the cache to set
*/
public void setMessageCache(Cache messageCache) {
this.messageCache = messageCache;
}
/**
* Get the invoice delivery services.
*
* @return the services
*/
public List getDeliveryServices() {
return deliveryServices;
}
/**
* Set the invoice delivery services.
*
* @param deliveryServices
* the services to set
*/
public void setDeliveryServices(List deliveryServices) {
this.deliveryServices = deliveryServices;
}
/**
* Get the invoice renderer resolver services.
*
* @return the services
*/
public List getRendererResolvers() {
return rendererResolvers;
}
/**
* Set the invoice renderer resolver services.
*
* @param rendererResolvers
* the services to set
*/
public void setRendererResolvers(List rendererResolvers) {
this.rendererResolvers = rendererResolvers;
}
/**
* Get the maximum amount of time to wait for invoice delivery to complete,
* in seconds.
*
* @return the timeout, in seconds
*/
public int getDeliveryTimeoutSecs() {
return deliveryTimeoutSecs;
}
/**
* Set the maximum amount of time to wait for invoice delivery to complete.
*
* @param deliveryTimeoutSecs
* the timeout to set, in seconds, or {@literal 0} for no timeout
*/
public void setDeliveryTimeoutSecs(int deliveryTimeoutSecs) {
this.deliveryTimeoutSecs = deliveryTimeoutSecs;
}
}