public static final String DEFAULT_TAX_ITEM_DESC = "tax"; public static final String TAX_COUNTRY_CUSTOM_FIELD_NAME = "taxCountry"; public static final String TAX_CODES_FIELD_NAME = "taxCodes"; public static final String PLUGIN_NAME = "killbill-simple-tax"; @Nonnull
public static BigDecimal amountWithAdjustments(@Nonnull InvoiceItem item,
        @Nonnull Multimap<UUID, InvoiceItem> allAdjustments) {
    Iterable<InvoiceItem> adjustments = allAdjustments.get(item.getId());
    BigDecimal amount = sumItems(adjustments);
    if (item.getAmount() != null) {
        amount = amount.add(item.getAmount());
    }
    return amount;
} public static BigDecimal sumAmounts(Iterable<BigDecimal> amounts) {
    BigDecimal sum = ZERO;
    for (BigDecimal amount : amounts) {
        sum = sum.add(amount);
    }
    return sum;
} public class SimpleTaxConfig {

    private static final char PROP_NAME_SEGMENT_SEPARATOR = '.';

    /**
     * The prefix to use for configuration properties (either system properties,
     * or per-tenant plugin configuration properties).
     */
    public static final String PROPERTY_PREFIX = "org.killbill.billing.plugin.simpletax.";

    private static final String TAXATION_TIME_ZONE_PROPERTY = PROPERTY_PREFIX + "taxationTimeZone";
    private static final String TAX_AMOUNT_PRECISION_PROPERTY = PROPERTY_PREFIX + "taxItem.amount.precision";
    private static final String TAX_RESOLVER_PROPERTY = PROPERTY_PREFIX + "taxResolver";

    private static final String PRODUCT_TAX_CODE_PREFIX = PROPERTY_PREFIX + "products.";
    private static final String TAX_CODES_PREFIX = PROPERTY_PREFIX + "taxCodes.";

    private static final String TAX_ITEM_DESCRIPTION_SUFFIX = ".taxItem.description";
    private static final String RATE_SUFFIX = ".rate";
    private static final String STARTING_ON_SUFFIX = ".startingOn";
    private static final String STOPPING_ON_SUFFIX = ".stoppingOn";
    private static final String COUNTRY_SUFFIX = ".country";

    /**
     * The default description for
     * {@linkplain org.killbill.billing.invoice.api.InvoiceItemType#TAX TAX
     * items}.
     */
    public static final String DEFAULT_TAX_ITEM_DESC = "tax";
    private static final DateTimeZone DEFAULT_TAXATION_TIME_ZONE = null;
    private static final int DEFAULT_TAX_AMOUNT_PRECISION = 2;
    private static final BigDecimal DEFAULT_TAX_RATE = new BigDecimal("0.00");
    private static final Class<? extends TaxResolver> DEFAULT_RESOLVER = NullTaxResolver.class;
    private static final Constructor<? extends TaxResolver> DEFAULT_RESOLVER_CONSTRUCTOR;
    static {
        try {
            DEFAULT_RESOLVER_CONSTRUCTOR = DEFAULT_RESOLVER.getConstructor(TaxComputationContext.class);
        } catch (NoSuchMethodException shouldNeverHappen) {
            throw new RuntimeException(shouldNeverHappen);
        }
    }

    private Map<String, String> cfg;
    private OSGIKillbillLogService logService;

    private Map<String, TaxCode> taxCodesByName;

    private DateTimeZone taxationTimeZone;
    private int taxAmountPrecision;
    private Constructor<? extends TaxResolver> taxResolverConstructor;

    /**
     * Construct a new configuration accessor for the given configuration
     * properties.
     *
     * @param cfg
     *            The configuration properties to use. No {@code null} values
     *            are allowed, which should be the case when this Map is
     *            constructed out of a {@link java.util.Properties} instance.
     * @param logService
     *            The logging service to use.
     * @throws NullPointerException
     *             when any value in {@code cfg} is {@code null}.
     */
    public SimpleTaxConfig(Map<String, String> cfg, OSGIKillbillLogService logService) {
        this.cfg = cfg;
        this.logService = logService;

        parseConfig();
        earlyConsistencyChecks();
    }

    private void parseConfig() {
        taxationTimeZone = timeZone(cfg, TAXATION_TIME_ZONE_PROPERTY, DEFAULT_TAXATION_TIME_ZONE);
        taxAmountPrecision = integer(cfg, TAX_AMOUNT_PRECISION_PROPERTY, DEFAULT_TAX_AMOUNT_PRECISION);
        taxResolverConstructor = resolverConstructor(cfg, TAX_RESOLVER_PROPERTY, DEFAULT_RESOLVER_CONSTRUCTOR);

        taxCodesByName = parseTaxCodes(cfg);
    }

    private static final String DEFAULT_TAXATION_MSG = " Default taxation of [" + DEFAULT_RESOLVER
            + "] will be applied, which disables any new taxation.";

    /**
     * A bunch of consistency checks, in order for plugin users to acknowledge
     * any configuration issues as early as possible.
     */
    private void earlyConsistencyChecks() {
        String resolverClassName = cfg.get(TAX_RESOLVER_PROPERTY);
        if (isBlank(resolverClassName)) {
            logService.log(LOG_WARNING, "Blank property [" + TAX_RESOLVER_PROPERTY
                    + "], whereas it should not be blank." + DEFAULT_TAXATION_MSG);
        } else {
            Class<?> clazz = null;
            try {
                clazz = currentThread().getContextClassLoader().loadClass(resolverClassName);
            } catch (ClassNotFoundException exc) {
                logService.log(LOG_ERROR, "Cannot load class [" + resolverClassName + "] as specified by the ["
                        + TAX_RESOLVER_PROPERTY + "] configuration property." + DEFAULT_TAXATION_MSG);
            }
            if (clazz != null) {
                String invalidClassMsgPfx = "Invalid class [" + resolverClassName + "] specified by the ["
                        + TAX_RESOLVER_PROPERTY + "] configuration property,";
                if (!TaxResolver.class.isAssignableFrom(clazz)) {
                    logService.log(LOG_ERROR, invalidClassMsgPfx + " which must designate a sub-class of ["
                            + TaxResolver.class.getName() + "]." + DEFAULT_TAXATION_MSG);
                }
                try {
                    clazz.getConstructor(TaxComputationContext.class);
                } catch (NoSuchMethodException exc) {
                    logService.log(LOG_ERROR, invalidClassMsgPfx
                            + " which must define a public constructor accepting a single argument of ["
                            + TaxComputationContext.class.getName() + "]." + DEFAULT_TAXATION_MSG);
                }
            }
        }

        for (Entry<String, String> prop : cfg.entrySet()) {
            String propName = prop.getKey();
            if (!startsWith(propName, PRODUCT_TAX_CODE_PREFIX)) {
                continue;
            }
            String taxCodes = prop.getValue();
            // Here 'taxCodes' is not supposed to be null because that's a
            // requirement we detail in the constructor javadoc.
            for (String taxCode : splitTaxCodes(taxCodes)) {
                if (findTaxCode(taxCode) == null) {
                    logService.log(LOG_ERROR, "Inconsistent config property [" + propName + "] with value [" + taxCodes
                            + "], because the tax code [" + taxCode + "] is not defined anywhere."
                            + " Config spelling error? You should fix this!");
                }
            }
        }
    }

    /**
     * @return A new immutable map of configured tax codes, identified by their
     *         unique names. Never {@code null}, with no {@code null} elements.
     */
    private static Map<String, TaxCode> parseTaxCodes(Map<String, String> cfg) {
        Set<String> names = newLinkedHashSet();
        String lastPropNamePfx = null;
        for (String propName : cfg.keySet()) {
            if (!startsWith(propName, TAX_CODES_PREFIX)) {
                continue;
            }
            if (startsWith(propName, lastPropNamePfx)) {
                continue;
            }
            String taxCodeName = extractName(propName, TAX_CODES_PREFIX.length());
            names.add(taxCodeName);
            lastPropNamePfx = propName.substring(0, TAX_CODES_PREFIX.length() + taxCodeName.length())
                    + PROP_NAME_SEGMENT_SEPARATOR;
        }
        ImmutableMap.Builder<String, TaxCode> codes = ImmutableMap.builder();
        for (String name : names) {
            String prefix = TAX_CODES_PREFIX + name;
            String taxItemDescription = string(cfg, prefix + TAX_ITEM_DESCRIPTION_SUFFIX, DEFAULT_TAX_ITEM_DESC);
            BigDecimal rate = bigDecimal(cfg, prefix + RATE_SUFFIX, DEFAULT_TAX_RATE);
            LocalDate startingOn = localDate(cfg, prefix + STARTING_ON_SUFFIX, null);
            LocalDate stoppingOn = localDate(cfg, prefix + STOPPING_ON_SUFFIX, null);
            Country country = country(cfg, prefix + COUNTRY_SUFFIX, null);
            codes.put(name, new TaxCode(name, taxItemDescription, rate, startingOn, stoppingOn, country));
        }
        return codes.build();
    }

    /**
     * Utility method to extract a portion of a property named, supposed to be
     * delimited by {@linkplain #PROP_NAME_SEGMENT_SEPARATOR dots}.
     */
    private static String extractName(String propName, int prefixLength) {
        int endOfNameSegment = indexOf(propName, PROP_NAME_SEGMENT_SEPARATOR, prefixLength);
        if (endOfNameSegment == INDEX_NOT_FOUND) {
            endOfNameSegment = propName.length();
        }
        return propName.substring(TAX_CODES_PREFIX.length(), endOfNameSegment);
    }

    /**
     * @return The time zone to use when considering instants.
     */
    @Nullable
    public DateTimeZone getTaxationTimeZone() {
        return taxationTimeZone;
    }

    /**
     * @return The {@linkplain BigDecimal#setScale scale} to use for amounts in
     *         invoices.
     */
    @Nonnull
    public int getTaxAmountPrecision() {
        return taxAmountPrecision;
    }

    /**
     * A factory for building the configured {@link TaxResolver} implementation.
     *
     * @return The <em>public</em> constructor to use when building the
     *         applicable {@linkplain TaxResolver tax resolver}. Never
     *         {@code null}.
     */
    @Nonnull
    public Constructor<? extends TaxResolver> getTaxResolverConstructor() {
        return taxResolverConstructor;
    }

    /**
     * Finds the definition of a tax code, as identified by its (unique) name.
     *
     * @param name
     *            A name for a tax code.
     * @return A matching tax code from the configuration, or {@code null} if
     *         none matches.
     */
    @Nullable
    public TaxCode findTaxCode(@Nullable String name) {
        return taxCodesByName.get(name);
    }

    /**
     * Lists the configured tax codes for a given product of the catalog.
     *
     * @param productName
     *            The name of a product in the catalog. Should not be
     *            {@code null}.
     * @return A new immutable set of configured tax code definitions. Never
     *         {@code null}, with no {@code null} elements.
     */
    @Nonnull
    public Set<TaxCode> getConfiguredTaxCodes(@Nonnull String productName) {
        String names = cfg.get(PRODUCT_TAX_CODE_PREFIX + productName);
        if (names == null) {
            return ImmutableSet.of();
        }
        return findTaxCodesInternal(names, "taxCode [%1$s] configured for product [" + productName
                + "] is undefined. Config spelling error? Ignoring it.");
    }

    /**
     * Converts a comma-separated list of tax codes into a set of tax code
     * definitions.
     * <p>
     * Tax codes are identified by their name, which are supposed to be unique.
     * The resulting set is ordered in the same way as tax codes were listed,
     * with no duplicates.
     *
     * @param names
     *            A comma-separated list of tax codes names. Must not be
     *            {@code null}.
     * @param errMsgContext
     *            An context message for errors, that tells where the
     *            {@code names} value comes from. Should not be {@code null}.
     * @return A new immutable set of configured tax code definitions that match
     *         the given list of names. Never {@code null}, with no {@code null}
     *         elements.
     * @throws NullPointerException
     *             when {@code names} is null.
     */
    @Nonnull
    public Set<TaxCode> findTaxCodes(@Nonnull String names, @Nonnull String errMsgContext) {
        return findTaxCodesInternal(names, "taxCode [%1$s] " + errMsgContext + " is undefined."
                + " Erroneously removed from config? Ignoring it.");
    }

    /**
     * @param names
     *            A comma-separated list of tax code names. Must not be
     *            {@code null}.
     * @param errMsgFmt
     *            An error message {@linkplain java.util.Formatter format} where
     *            {@code %1$s} will be replaced by the tax code that cannot be
     *            found in the config. Must not be {@code null}.
     * @return A set of configured tax code definitions. Never {@code null}.
     * @throws NullPointerException
     *             when {@code names} is {@code null} or when {@code errMsgFmt}
     *             is {@code null}.
     */
    @Nonnull
    private Set<TaxCode> findTaxCodesInternal(@Nonnull String names, @Nonnull String errMsgFmt) {
        ImmutableSet.Builder<TaxCode> taxCodes = ImmutableSet.builder();
        for (String name : splitTaxCodes(names)) {
            TaxCode taxCode = findTaxCode(name);
            if (taxCode == null) {
                logService.log(LOG_ERROR, String.format(errMsgFmt, name));
                continue;
            }
            taxCodes.add(taxCode);
        }
        return taxCodes.build();
    }
} public class CustomFieldService {
    /** Name of custom fields for account VAT Identification Numbers. */
    public static final String VATIN_CUSTOM_FIELD_NAME = "VATIdNum";
    /** Name of custom fields for account tax countries. */
    public static final String TAX_COUNTRY_CUSTOM_FIELD_NAME = "taxCountry";

    private static final long START_OFFSET = 0L;
    private static final long PAGE_SIZE = 100L;

    private CustomFieldUserApi customFieldApi;
    private OSGIKillbillLogService logService;

    /**
     * Constructs a service for manipulating custom fields.
     *
     * @param customFieldApi
     *            The Kill Bill service API class to use.
     * @param logService
     *            The Kill Bill logging service to use.
     */
    public CustomFieldService(CustomFieldUserApi customFieldApi, OSGIKillbillLogService logService) {
        super();
        this.customFieldApi = customFieldApi;
        this.logService = logService;
    }

    /**
     * A predicate that only accepts custom fields with a specific name.
     */
    private static class RetainFieldsWithName implements Predicate<CustomField> {
        private String fieldName;

        public RetainFieldsWithName(String fieldName) {
            super();
            checkNotNull(fieldName, "Field name must not be null");
            this.fieldName = fieldName;
        }

        @Override
        public boolean apply(CustomField field) {
            return fieldName.equals(field.getFieldName());
        }
    }

    /**
     * A predicate that only accepts custom fields of a given
     * {@linkplain org.killbill.billing.ObjectType object type} and a specific
     * name.
     */
    private static class RetainFieldsWithNameAndObjectType extends RetainFieldsWithName {
        private ObjectType objectType;

        public RetainFieldsWithNameAndObjectType(String fieldName, ObjectType objectType) {
            super(fieldName);
            checkNotNull(objectType, "Object type must not be null");
            this.objectType = objectType;
        }

        @Override
        public boolean apply(CustomField field) {
            return objectType.equals(field.getObjectType()) && super.apply(field);
        }
    }

    /**
     * Finds all custom fields on account objects that match a specific field
     * name in the context of a given tenant.
     *
     * @param fieldName
     *            A specific field name that returned fields will match.
     * @param tenantContext
     *            The tenant on which to operate.
     * @return The list of matching custom fields. Never {@code null}.
     */
    @Nonnull
    public List<CustomField> findAllAccountFieldsByFieldNameAndTenant(String fieldName, TenantContext tenantContext) {
        List<CustomField> fields = newArrayList();
        Predicate<CustomField> onlyAccountFieldsWithExpectedName = new RetainFieldsWithNameAndObjectType(fieldName,
                ACCOUNT);
        Pagination<CustomField> page;
        Long nextOffset = START_OFFSET;
        do {
            page = customFieldApi.searchCustomFields(fieldName, nextOffset, PAGE_SIZE, tenantContext);
            addAll(fields, filter(page, onlyAccountFieldsWithExpectedName));
            nextOffset = page.getNextOffset();
        } while (nextOffset != null);
        return fields;
    }

    @Nullable
    public CustomField findFieldByNameAndInvoiceItemAndTenant(String fieldName, UUID invoiceItemId,
            TenantContext tenantContext) {
        List<CustomField> invoiceItemFields = customFieldApi.getCustomFieldsForObject(invoiceItemId, INVOICE_ITEM,
                tenantContext);
        if (invoiceItemFields == null) {
            return null;
        }
        return tryFind(invoiceItemFields, new RetainFieldsWithName(fieldName)).orNull();
    }

    /**
     * Finds a custom field on a given account object that matches a specific
     * field name in the context of a given tenant.
     *
     * @param fieldName
     *            A specific field name that any returned field will match.
     * @param accountId
     *            An identifier for an account.
     * @param tenantContext
     *            The tenant on which to operate.
     * @return The matching custom field if any, or {@code null} if no such
     *         field exists.
     */
    @Nullable
    public CustomField findFieldByNameAndAccountAndTenant(String fieldName, UUID accountId, TenantContext tenantContext) {
        List<CustomField> accountFields = customFieldApi.getCustomFieldsForObject(accountId, ACCOUNT, tenantContext);
        if (accountFields == null) {
            return null;
        }
        return tryFind(accountFields, new RetainFieldsWithName(fieldName)).orNull();
    }

    /**
     * Persists a new value for a custom field on a given account object.
     *
     * @param fieldValue
     *            The new field value.
     * @param fieldName
     *            The field name.
     * @param accountId
     *            The identifier for the account object.
     * @param tenantContext
     *            The tenant on which to operate.
     * @return {@code true} when the new value is properly saved, or
     *         {@code false} otherwise.
     */
    public boolean saveAccountField(String fieldValue, String fieldName, UUID accountId, TenantContext tenantContext) {
        return saveAccountField(fieldValue, fieldName, accountId, ACCOUNT, tenantContext);
    }

    public boolean saveInvoiceItemField(String fieldValue, String fieldName, UUID invoiceItemId,
            TenantContext tenantContext) {
        return saveAccountField(fieldValue, fieldName, invoiceItemId, INVOICE_ITEM, tenantContext);
    }

    private boolean saveAccountField(String fieldValue, String fieldName, UUID objectId, ObjectType objectType,
            TenantContext tenantContext) {
        CustomField existing = null;
        List<CustomField> accountFields = customFieldApi.getCustomFieldsForObject(objectId, objectType, tenantContext);
        if (accountFields != null) {
            for (CustomField field : accountFields) {
                if (field.getFieldName().equals(fieldName)) {
                    existing = field;
                    break;
                }
            }
        }
        CallContext context = new PluginCallContext(PLUGIN_NAME, new DateTime(), tenantContext.getTenantId());
        if (existing != null) {
            try {
                customFieldApi.removeCustomFields(ImmutableList.of(existing), context);
            } catch (CustomFieldApiException exc) {
                logService.log(
                        LOG_ERROR,
                        "while removing custom field '" + existing.getFieldName() + "' with value ["
                                + existing.getFieldValue() + "] on " + existing.getObjectType() + " object ["
                                + existing.getObjectId() + "]", exc);
                return false;
            }
        }
        CustomField newField = ImmutableCustomField.builder()//
                .withObjectType(objectType).withObjectId(objectId)//
                .withFieldName(fieldName).withFieldValue(fieldValue)//
                .build();
        try {
            customFieldApi.addCustomFields(ImmutableList.of(newField), context);
            return true;
        } catch (CustomFieldApiException exc) {
            logService.log(LOG_ERROR, "while adding custom field '" + fieldName + "' with value [" + fieldValue
                    + "] on " + objectType + " object [" + objectId + "]", exc);
            try {
                // Add back the removed field
                customFieldApi.addCustomFields(ImmutableList.of(existing), context);
            } catch (CustomFieldApiException exc2) {
                logService.log(LOG_ERROR,
                        "while adding back the previously removed custom field '" + existing.getFieldName()
                                + "' with value [" + existing.getFieldValue() + "] on " + existing.getObjectType()
                                + " object [" + existing.getObjectId() + "]", exc);
            }
            return false;
        }
    }
} public class Country {

    private static final Supplier<Set<String>> COUNTRIES = new ConcurrentLazyValue<Set<String>>() {
        @Override
        protected Set<String> initialize() {
            return ImmutableSet.copyOf(getISOCountries());
        }
    };

    private String code;

    /**
     * Constructs a new country. The country code must be an element of
     * {@link Locale#getISOCountries()}.
     *
     * @param code
     *            The ISO 3166-1 alpha-2 country code.
     * @throws IllegalArgumentException
     *             when the country code is not an element of
     *             {@link Locale#getISOCountries()}.
     */
    @JsonCreator
    public Country(String code) throws IllegalArgumentException {
        super();
        checkArgument(COUNTRIES.get().contains(code), "Illegal country code: [%s]", code);
        this.code = code;
    }

    /**
     * Returns the ISO 3166-1 alpha-2 code for this country.
     *
     * @return The code of this country. Never {@code null}.
     */
    @JsonValue
    public String getCode() {
        return code;
    }

    /**
     * Computes the name of this country, in the specified language, or in
     * English if the language is not {@linkplain Locale#getAvailableLocales()
     * supported}.
     *
     * @param language
     *            The preferred language in which the country name should be
     *            expressed.
     * @return The name of the country in the specified language, or in English.
     */
    public String computeName(Locale language) {
        return new Locale("", code).getDisplayCountry(language);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != getClass()) {
            return false;
        }
        Country rhs = (Country) obj;
        return new EqualsBuilder().append(code, rhs.code).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(code).toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, SHORT_STYLE)//
                .append("code", code)//
                .toString();
    }
} public class TaxCode {

    private String name;
    private String taxItemDescription;
    private BigDecimal rate;
    private LocalDate startingOn;
    private LocalDate stoppingOn;
    private Country country;

    /**
     * constructs a new tax code definition.
     *
     * @param name
     *            The unique name for the tax code.
     * @param taxItemDescription
     *            The description for tax items that will be generated by the
     *            tax code.
     * @param rate
     *            The tax rate to apply when computing tax amounts.
     * @param startingOn
     *            The first day on which the tax code will <em>start</em> being
     *            applicable, or {@code null} if the tax code has no sunrise
     *            date.
     * @param stoppingOn
     *            The first day on which this tax code will <em>cease</em> to be
     *            applicable, or {@code null} if the tax code has no sunset
     *            date.
     * @param country
     *            The country on which this tax code applies.
     */
    public TaxCode(String name, String taxItemDescription, BigDecimal rate, LocalDate startingOn, LocalDate stoppingOn,
            Country country) {
        super();
        this.name = name;
        this.taxItemDescription = taxItemDescription;
        this.rate = rate;
        this.startingOn = startingOn;
        this.stoppingOn = stoppingOn;
        this.country = country;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj == this) {
            return true;
        }
        if (obj.getClass() != getClass()) {
            return false;
        }
        TaxCode rhs = (TaxCode) obj;
        if (!new EqualsBuilder()//
                .append(name, rhs.name)//
                .append(taxItemDescription, rhs.taxItemDescription)//
                .append(startingOn, rhs.startingOn)//
                .append(stoppingOn, rhs.stoppingOn)//
                .append(country, rhs.country)//
                .isEquals()) {
            return false;
        }
        // Custom processing BigDecimal equality ignoring scale
        if (rate == null) {
            return rhs.rate == null;
        }
        return rate.compareTo(rhs.rate) == 0;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()//
                .append(name)//
                .append(taxItemDescription)//
                // Custom processing BigDecimal hash ignoring scale
                .append(rate == null ? 0 : rate.toString())//
                .append(startingOn)//
                .append(stoppingOn)//
                .append(country)//
                .toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this, SHORT_STYLE)//
                .append("name", name)//
                .append("taxItemDescription", taxItemDescription)//
                .append("rate", rate)//
                .append("startingOn", startingOn)//
                .append("stoppingOn", stoppingOn)//
                .append("country", country)//
                .toString();
    }

    /**
     * @return The unique name of this tax code.
     */
    public String getName() {
        return name;
    }

    /**
     * @return The description for tax items that are generated by this tax
     *         code.
     */
    public String getTaxItemDescription() {
        return taxItemDescription;
    }

    /**
     * @return The tax rate to apply when computing tax amounts.
     */
    public BigDecimal getRate() {
        return rate;
    }

    /**
     * @return The first day on which this tax code <em>starts</em> being
     *         applicable.
     */
    public LocalDate getStartingOn() {
        return startingOn;
    }

    /**
     * @return The first day on which this tax code <em>ceases</em> to be
     *         applicable.
     */
    public LocalDate getStoppingOn() {
        return stoppingOn;
    }

    /**
     * @return The country on which this tax code applies.
     */
    public Country getCountry() {
        return country;
    }
} public class TaxCodeService {

    /**
     * The name of a custom field on invoice items, that can specify any
     * relevant tax code to apply.
     */
    public static final String TAX_CODES_FIELD_NAME = "taxCodes";

    private CheckedSupplier<StaticCatalog, CatalogApiException> catalog;
    private SimpleTaxConfig cfg;
    private SetMultimap<UUID, CustomField> taxFieldsOfInvoices;

    /**
     * Creates a service that helps listing tax codes.
     *
     * @param catalog
     *            The Kill Bill catalog to use.
     * @param cfg
     *            The plugin configuration.
     * @param taxFieldsOfInvoices
     *            The tax fields of all account invoices, grouped by their
     *            related taxable items.
     */
    public TaxCodeService(CheckedSupplier<StaticCatalog, CatalogApiException> catalog, SimpleTaxConfig cfg,
            SetMultimap<UUID, CustomField> taxFieldsOfInvoices) {
        super();
        this.catalog = catalog;
        this.cfg = cfg;
        this.taxFieldsOfInvoices = taxFieldsOfInvoices;
    }

    /**
     * Enumerate configured tax codes for the items of a given invoice. The
     * order of configured tax codes is preserved.
     * <p>
     * Final resolution is not done here because it represents custom logic that
     * is regulation-dependent.
     *
     * @param invoice
     *            the invoice the items of which need to be taxed.
     * @return An immutable multi-map of unique applicable tax codes, grouped by
     *         the identifiers of their related invoice items. Never
     *         {@code null}, and guaranteed not having any {@code null} values.
     * @throws NullPointerException
     *             when {@code invoice} is {@code null}.
     */
    @Nonnull
    public SetMultimap<UUID, TaxCode> resolveTaxCodesFromConfig(Invoice invoice) {
        ImmutableSetMultimap.Builder<UUID, TaxCode> taxCodesOfInvoiceItems = ImmutableSetMultimap.builder();

        // This lazy map helps us in building a cache for the values we've
        // already met, and allows us an easy-to-understand syntax below.
        Map<String, Product> productOfPlanName = lazyMap(new HashMap<String, Product>(),
                new Transformer<String, Product>() {
                    @Override
                    public Product transform(String planName) {
                        try {
                            Plan plan = catalog.get().findCurrentPlan(planName);
                            return plan.getProduct();
                        } catch (CatalogApiException notFound) {
                            return null;
                        }
                    }
                });

        for (InvoiceItem invoiceItem : invoice.getInvoiceItems()) {
            String planName = invoiceItem.getPlanName();
            if (planName == null) {
                continue;
            }

            Product product = productOfPlanName.get(planName);
            if (product == null) {
                continue;
            }

            Set<TaxCode> taxCodes = cfg.getConfiguredTaxCodes(product.getName());
            if (taxCodes.isEmpty()) {
                continue;
            }
            taxCodesOfInvoiceItems.putAll(invoiceItem.getId(), taxCodes);
        }
        return taxCodesOfInvoiceItems.build();
    }

    /**
     * Find tax codes that apply to the items of a given invoice, looking for
     * custom fields named {@value #TAX_CODES_FIELD_NAME} that can be attached
     * to these items.
     *
     * @param invoice
     *            An invoice in which existing tax codes are to be found.
     * @return The existing tax codes, grouped by the identifiers of their
     *         related invoice items. Never {@code null}, and guaranteed not
     *         having any {@code null} values.
     * @throws NullPointerException
     *             when {@code invoice} is {@code null}.
     */
    @Nonnull
    public SetMultimap<UUID, TaxCode> findExistingTaxCodes(Invoice invoice) {
        Set<CustomField> taxFields = taxFieldsOfInvoices.get(invoice.getId());
        // Note: taxFields is not null, by Multimap contract

        ImmutableSetMultimap.Builder<UUID, TaxCode> taxCodesOfInvoiceItems = ImmutableSetMultimap.builder();
        for (CustomField taxField : taxFields) {
            if (!TAX_CODES_FIELD_NAME.equals(taxField.getFieldName())) {
                continue;
            }
            String taxCodesCSV = taxField.getFieldValue();
            if (taxCodesCSV == null) {
                continue;
            }
            UUID invoiceItemId = taxField.getObjectId();
            Set<TaxCode> taxCodes = cfg.findTaxCodes(taxCodesCSV, "from custom field '" + TAX_CODES_FIELD_NAME
                    + "' of invoice item [" + invoiceItemId + "]");
            taxCodesOfInvoiceItems.putAll(invoiceItemId, taxCodes);
        }
        return taxCodesOfInvoiceItems.build();
    }
} public class SimpleTaxConfigurationHandler extends PluginTenantConfigurableConfigurationHandler<SimpleTaxConfig> {

    private OSGIKillbillLogService logService;

    /**
     * Constructs a new configuration handler.
     *
     * @param pluginName
     *            The plugin name to use when accessing per-tenant
     *            configuration.
     * @param services
     *            The Kill Bill meta-API.
     * @param logService
     *            The service to use when logging events.
     */
    public SimpleTaxConfigurationHandler(String pluginName, OSGIKillbillAPI services, OSGIKillbillLogService logService) {
        super(pluginName, services, logService);
        this.logService = logService;
    }

    @Override
    protected SimpleTaxConfig createConfigurable(Properties pluginConfig) {
        Map<String, String> props = Maps.fromProperties(pluginConfig);
        return new SimpleTaxConfig(props, logService);
    }
} public class NullTaxResolver implements TaxResolver {

    /**
     * The mandatory public constructor for tax resolvers, which accepts a
     * single {@link TaxComputationContext} argument.
     *
     * @param ctx
     *            The tax computation context.
     */
    public NullTaxResolver(TaxComputationContext ctx) {
        super();
    }

    @Override
    public TaxCode applicableCodeForItem(Iterable<TaxCode> taxCodes, InvoiceItem item) {
        return null;
    }
} public interface TaxResolver {

    /**
     * Retain only one applicable tax code, so that taxation of invoice item can
     * be done, based on the returned tax code.
     *
     * @param taxCodes
     *            The candidate tax codes for this invoice item.
     * @param item
     *            The invoice item to tax.
     * @return The tax code to apply to this item, or {@code null} if none
     *         applies.
     */
    @Nullable
    public abstract TaxCode applicableCodeForItem(Iterable<TaxCode> taxCodes, InvoiceItem item);

} public abstract class CheckedLazyValue<T, E extends Exception> implements CheckedSupplier<T, E> {

    /** Whether the value is initialized. */
    private boolean initialized = false;

    /** Stores the managed object. */
    private T value;

    /**
     * Returns the value wrapped by this instance, initializing it on first
     * access. Subsequent access return the cached value.
     *
     * @return The object initialized by this {@code CheckedSupplier}.
     * @throws E
     *             If an error occurred during initialization of the object.
     */
    @Override
    public T get() throws E {
        if (!initialized) {
            value = initialize();
            initialized = true;
        }
        return value;
    }

    /**
     * Initializes the value managed by this instance. This method is called by
     * {@link #get()} when the object is accessed for the first time.
     * <p>
     * This method is guaranteed to be called only once, even if the initialized
     * value is {@code null}.
     *
     * @return the managed data object
     * @throws E
     *             if an error occurs during object creation
     */
    protected abstract T initialize() throws E;
} public interface CheckedSupplier<T, E extends Exception> {

    /**
     * Retrieves an instance of the appropriate type. The returned object may or
     * may not be a new instance, depending on the implementation.
     *
     * @return An instance of the appropriate type.
     * @throws E
     *             If an error occurred when retrieving the requested instance.
     */
    public abstract T get() throws E;
} public class ImmutableCustomField implements CustomField {

    private UUID id;
    private DateTime createdDate, updatedDate;
    private UUID objectId;
    private ObjectType objectType;
    private String fieldName, fieldValue;

    private ImmutableCustomField() {
        super();
    }

    @Override
    public UUID getId() {
        return id;
    }

    @Override
    public DateTime getCreatedDate() {
        return createdDate;
    }

    @Override
    public DateTime getUpdatedDate() {
        return updatedDate;
    }

    @Override
    public UUID getObjectId() {
        return objectId;
    }

    @Override
    public ObjectType getObjectType() {
        return objectType;
    }

    @Override
    public String getFieldName() {
        return fieldName;
    }

    @Override
    public String getFieldValue() {
        return fieldValue;
    }

    /**
     * @return A new {@link ImmutableCustomField} builder.
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * @param field
     *            A template custom field, the data of which should be copied.
     * @return A new {@link ImmutableCustomField} builder.
     */
    public static Builder builder(CustomField field) {
        return new Builder(field);
    }

    /**
     * A builder class for {@link ImmutableCustomField}s.
     *
     * @author Benjamin Gandon
     */
    public static class Builder {

        private UUID id;
        private DateTime createdDate, updatedDate;
        private UUID objectId;
        private ObjectType objectType;
        private String fieldName, fieldValue;

        private Builder() {
            super();
            id = randomUUID();
            createdDate = now();
            updatedDate = now();
        }

        private Builder(CustomField src) {
            this();
            id = src.getId();
            createdDate = src.getCreatedDate();
            updatedDate = src.getUpdatedDate();
            objectId = src.getObjectId();
            objectType = src.getObjectType();
            fieldName = src.getFieldName();
            fieldValue = src.getFieldValue();
        }

        /**
         * @return A new {@link ImmutableCustomField}, with the properties set
         *         in this builder.
         */
        public CustomField build() {
            ImmutableCustomField taxField = new ImmutableCustomField();
            taxField.id = id;
            taxField.createdDate = createdDate;
            taxField.updatedDate = updatedDate;
            taxField.objectId = objectId;
            taxField.objectType = objectType;
            taxField.fieldName = fieldName;
            taxField.fieldValue = fieldValue;
            return taxField;
        }

        /**
         * @param objectId
         *            The object identifier to use.
         * @return this builder
         */
        public Builder withObjectId(UUID objectId) {
            this.objectId = objectId;
            updatedDate = now();
            return this;
        }

        /**
         * @param objectType
         *            The object type to use.
         * @return this builder
         */
        public Builder withObjectType(ObjectType objectType) {
            this.objectType = objectType;
            updatedDate = now();
            return this;
        }

        /**
         * @param fieldName
         *            The field name to use.
         * @return this builder
         */
        public Builder withFieldName(String fieldName) {
            this.fieldName = fieldName;
            updatedDate = now();
            return this;
        }

        /**
         * @param fieldValue
         *            The field value to use.
         * @return this builder
         */
        public Builder withFieldValue(String fieldValue) {
            this.fieldValue = fieldValue;
            updatedDate = now();
            return this;
        }
    }
}
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSetMultimap.builder;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newHashMap;
import static com.google.common.collect.Ordering.natural;
import static java.math.BigDecimal.ZERO;
import static java.math.RoundingMode.HALF_UP;
import static org.killbill.billing.ObjectType.INVOICE;
import static org.killbill.billing.ObjectType.INVOICE_ITEM;
import static org.killbill.billing.notification.plugin.api.ExtBusEventType.INVOICE_CREATION;
import static org.killbill.billing.plugin.api.invoice.PluginInvoiceItem.createAdjustmentItem;
import static org.killbill.billing.plugin.api.invoice.PluginInvoiceItem.createTaxItem;
import static org.killbill.billing.plugin.simpletax.config.SimpleTaxConfig.DEFAULT_TAX_ITEM_DESC;
import static org.killbill.billing.plugin.simpletax.config.http.CustomFieldService.TAX_COUNTRY_CUSTOM_FIELD_NAME;
import static org.killbill.billing.plugin.simpletax.internal.TaxCodeService.TAX_CODES_FIELD_NAME;
import static org.killbill.billing.plugin.simpletax.plumbing.SimpleTaxActivator.PLUGIN_NAME;
import static org.killbill.billing.plugin.simpletax.util.InvoiceHelpers.amountWithAdjustments;
import static org.killbill.billing.plugin.simpletax.util.InvoiceHelpers.sumAmounts;
import static org.osgi.service.log.LogService.LOG_DEBUG;
import static org.osgi.service.log.LogService.LOG_ERROR;
import static org.osgi.service.log.LogService.LOG_INFO;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.killbill.billing.account.api.Account;
import org.killbill.billing.catalog.api.CatalogApiException;
import org.killbill.billing.catalog.api.StaticCatalog;
import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.InvoiceApiException;
import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.notification.plugin.api.ExtBusEvent;
import org.killbill.billing.payment.api.PluginProperty;
import org.killbill.billing.plugin.api.PluginCallContext;
import org.killbill.billing.plugin.api.PluginTenantContext;
import org.killbill.billing.plugin.api.invoice.PluginInvoicePluginApi;
import org.killbill.billing.plugin.simpletax.config.SimpleTaxConfig;
import org.killbill.billing.plugin.simpletax.config.http.CustomFieldService;
import org.killbill.billing.plugin.simpletax.internal.Country;
import org.killbill.billing.plugin.simpletax.internal.TaxCode;
import org.killbill.billing.plugin.simpletax.internal.TaxCodeService;
import org.killbill.billing.plugin.simpletax.plumbing.SimpleTaxConfigurationHandler;
import org.killbill.billing.plugin.simpletax.resolving.NullTaxResolver;
import org.killbill.billing.plugin.simpletax.resolving.TaxResolver;
import org.killbill.billing.plugin.simpletax.util.CheckedLazyValue;
import org.killbill.billing.plugin.simpletax.util.CheckedSupplier;
import org.killbill.billing.plugin.simpletax.util.ImmutableCustomField;
import org.killbill.billing.util.api.CustomFieldApiException;
import org.killbill.billing.util.api.CustomFieldUserApi;
import org.killbill.billing.util.callcontext.CallContext;
import org.killbill.billing.util.callcontext.TenantContext;
import org.killbill.billing.util.customfield.CustomField;
import org.killbill.clock.Clock;
import org.killbill.killbill.osgi.libs.killbill.OSGIConfigPropertiesService;
import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillAPI;
import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillEventDispatcher.OSGIKillbillEventHandler;
import org.killbill.killbill.osgi.libs.killbill.OSGIKillbillLogService;
import org.killbill.killbill.osgi.libs.killbill.OSGIServiceNotAvailable;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
/*
 * Copyright 2015 Benjamin Gandon
 *
 * Licensed 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.killbill.billing.plugin.simpletax;






/**
 * Main class for the Kill Bill Simple Tax Plugin.
 * <p>
 * This plugin adds tax items to each new invoice that is being created, based
 * on the tax codes that {@linkplain SimpleTaxConfig have been configured} and
 * then {@linkplain TaxResolver resolved}.
 * <p>
 * Tax codes that are directly set on invoice items, before this plugin run,
 * take precedence and won't be overridden.
 * <p>
 * Tax codes can also be added, removed or changed on historical invoices. The
 * plugin adds missing taxes or adjusts existing tax items accordingly.
 * <p>
 * Country-specific rules can be implemented by configuring custom
 * {@link TaxResolver} implementations.
 * <p>
 * The implementation is idempotent. Subsequent calls with the same inputs and
 * server state will results in no new item being created.
 *
 * @author Benjamin Gandon
 * @see SimpleTaxConfig
 * @see TaxResolver
 */
public class SimpleTaxPlugin extends PluginInvoicePluginApi implements OSGIKillbillEventHandler {

    private SimpleTaxConfigurationHandler configHandler;
    private CustomFieldService customFieldService;

    /**
     * Creates a new simple-tax plugin.
     *
     * @param configHandler
     *            The configuration handler to use for this plugin instance.
     * @param customFieldService
     *            The service to use when accessing custom fields.
     * @param metaApi
     *            The Kill Bill meta-API.
     * @param configService
     *            The service to use for accessing the plugin configuration
     *            properties.
     * @param logService
     *            The service to use when logging events.
     * @param clockService
     *            The clock service to use when accessing the current time.
     */
    public SimpleTaxPlugin(SimpleTaxConfigurationHandler configHandler, CustomFieldService customFieldService,
            OSGIKillbillAPI metaApi, OSGIConfigPropertiesService configService, OSGIKillbillLogService logService,
            Clock clockService) {
        super(metaApi, configService, logService, clockService);
        this.configHandler = configHandler;
        this.customFieldService = customFieldService;
    }

    /**
     * @return The Kill Bill services that constitute the API.
     */
    protected OSGIKillbillAPI services() {
        return killbillAPI;
    }

    /**
     * Returns additional invoice items to be added to the invoice upon
     * creation, based on the tax codes that have been configured, or directly
     * set on invoice items.
     * <p>
     * This method produces two types of additional invoice items.
     * <p>
     * First, this method lists any missing tax items in the new invoice.
     * <p>
     * Then, adjustments might have been created on any historical invoice of
     * the account. Thus, this method also lists any necessary adjustments to
     * any tax items in any historical invoices.
     * <p>
     * Plus, tax codes can be added, changed or removed on historical invoices.
     * The affected tax amounts will be adjusted accordingly.
     *
     * @param newInvoice
     *            The invoice that is being created.
     * @param properties
     *            Any user-specified plugin properties, coming straight out of
     *            the API request that has triggered this code to run. See their
     *            <a href=
     *            "http://docs.killbill.io/0.15/userguide_payment.html#_plugin_properties"
     *            >documentation for payment plugins</a>.
     * @param callCtx
     *            The context in which this code is running.
     * @return A new immutable list of new tax items, or adjustments on existing
     *         tax items. Never {@code null}, and guaranteed not having any
     *         {@code null} elements.
     */
    @Override
    public List<InvoiceItem> getAdditionalInvoiceItems(Invoice newInvoice, Iterable<PluginProperty> properties,
            CallContext callCtx) {

        TaxComputationContext taxCtx = createTaxComputationContext(newInvoice, callCtx);
        TaxResolver taxResolver = instanciateTaxResolver(taxCtx);
        Map<UUID, TaxCode> newTaxCodes = addMissingTaxCodes(newInvoice, taxResolver, taxCtx, callCtx);

        ImmutableList.Builder<InvoiceItem> additionalItems = ImmutableList.builder();
        for (Invoice invoice : taxCtx.getAllInvoices()) {

            List<InvoiceItem> newItems;
            if (invoice.equals(newInvoice)) {
                newItems = computeTaxOrAdjustmentItemsForNewInvoice(invoice, taxCtx, newTaxCodes);
            } else {
                newItems = computeTaxOrAdjustmentItemsForHistoricalInvoice(invoice, taxCtx);
            }
            additionalItems.addAll(newItems);
        }
        return additionalItems.build();
    }

    @Override
    public void handleKillbillEvent(ExtBusEvent event) {
        logService.log(LOG_DEBUG, "Received event [" + event.getEventType() + "] for object [" + event.getObjectId()
                + "] of type [" + event.getObjectType() + "] belonging to account [" + event.getAccountId()
                + "] in tenant [" + event.getTenantId() + "]");

        if (!INVOICE_CREATION.equals(event.getEventType())) {
            return;
        }
        if (!INVOICE.equals(event.getObjectType())) {
            return;
        }
        UUID invoiceId = event.getObjectId();
        UUID tenantId = event.getTenantId();
        logService.log(LOG_INFO, "Adding tax codes to invoice [" + invoiceId
                + "] as post-creation treatment for tenant [" + tenantId + "]");

        Invoice newInvoice;
        try {
            newInvoice = getInvoiceUserApi().getInvoice(invoiceId, new PluginTenantContext(tenantId));
        } catch (OSGIServiceNotAvailable exc) {
            logService.log(LOG_ERROR, "before post-treating taxes on invoice [" + invoiceId
                    + "]: invoice user API is not available", exc);
            throw exc;
        } catch (InvoiceApiException exc) {
            logService.log(LOG_ERROR, "before post-treating taxes on invoice [" + invoiceId
                    + "]: invoice cannot be fetched", exc);
            throw new RuntimeException("unexpected error before post-treating taxes on invoice [" + invoiceId + "]",
                    exc);
        }

        CallContext callCtx = new PluginCallContext(PLUGIN_NAME, DateTime.now(), tenantId);

        TaxComputationContext taxCtx = createTaxComputationContext(newInvoice, callCtx);
        TaxResolver taxResolver = instanciateTaxResolver(taxCtx);
        Map<UUID, TaxCode> newTaxCodes = addMissingTaxCodes(newInvoice, taxResolver, taxCtx, callCtx);

        for (Entry<UUID, TaxCode> entry : newTaxCodes.entrySet()) {
            UUID invoiceItemId = entry.getKey();
            TaxCode taxCode = entry.getValue();
            persistTaxCode(taxCode, invoiceItemId, newInvoice, callCtx);
        }
    }

    /**
     * Pre-compute data that will be useful to computing tax items and tax
     * adjustment items.
     *
     * @param newInvoice
     *            The invoice that is being created.
     * @param tenantCtx
     *            The context in which this code is running.
     * @return An immutable holder for helpful pre-computed data when adding or
     *         adjusting taxes in the account invoices. Never {@code null}.
     */
    private TaxComputationContext createTaxComputationContext(Invoice newInvoice, TenantContext tenantCtx) {

        SimpleTaxConfig cfg = configHandler.getConfigurable(tenantCtx.getTenantId());

        UUID accountId = newInvoice.getAccountId();
        Account account = getAccount(accountId, tenantCtx);
        CustomField taxCountryField = customFieldService.findFieldByNameAndAccountAndTenant(
                TAX_COUNTRY_CUSTOM_FIELD_NAME, accountId, tenantCtx);