/**
   * Create an adjustment for a given invoice item. This just creates the object in memory, it
   * doesn't write it to disk.
   *
   * @param invoiceToBeAdjusted the invoice
   * @param invoiceItemId the invoice item id to adjust
   * @param positiveAdjAmount the amount to adjust. Pass null to adjust the full amount of the
   *     original item
   * @param currency the currency of the amount. Pass null to default to the original currency used
   * @param effectiveDate adjustment effective date, in the account timezone
   * @return the adjustment item
   */
  public InvoiceItem createAdjustmentItem(
      final Invoice invoiceToBeAdjusted,
      final UUID invoiceItemId,
      @Nullable final BigDecimal positiveAdjAmount,
      @Nullable final Currency currency,
      final LocalDate effectiveDate,
      final InternalCallContext context)
      throws InvoiceApiException {
    final InvoiceItem invoiceItemToBeAdjusted =
        Iterables.<InvoiceItem>tryFind(
                invoiceToBeAdjusted.getInvoiceItems(),
                new Predicate<InvoiceItem>() {
                  @Override
                  public boolean apply(final InvoiceItem input) {
                    return input.getId().equals(invoiceItemId);
                  }
                })
            .orNull();
    if (invoiceItemToBeAdjusted == null) {
      throw new InvoiceApiException(ErrorCode.INVOICE_ITEM_NOT_FOUND, invoiceItemId);
    }

    // Check the specified currency matches the one of the existing invoice
    final Currency currencyForAdjustment =
        Objects.firstNonNull(currency, invoiceItemToBeAdjusted.getCurrency());
    if (invoiceItemToBeAdjusted.getCurrency() != currencyForAdjustment) {
      throw new InvoiceApiException(
          ErrorCode.CURRENCY_INVALID, currency, invoiceItemToBeAdjusted.getCurrency());
    }

    // Reuse the same logic we have for refund with item adjustment
    final Map<UUID, BigDecimal> input = new HashMap<UUID, BigDecimal>();
    input.put(invoiceItemId, positiveAdjAmount);

    final Map<UUID, BigDecimal> output =
        dao.computeItemAdjustments(invoiceToBeAdjusted.getId().toString(), input, context);

    // If we pass that stage, it means the validation succeeded so we just need to extract resulting
    // amount and negate the result.
    final BigDecimal amountToAdjust = output.get(invoiceItemId).negate();
    // Finally, create the adjustment
    return new ItemAdjInvoiceItem(
        UUIDs.randomUUID(),
        context.getCreatedDate(),
        invoiceItemToBeAdjusted.getInvoiceId(),
        invoiceItemToBeAdjusted.getAccountId(),
        effectiveDate,
        null,
        amountToAdjust,
        currencyForAdjustment,
        invoiceItemToBeAdjusted.getId());
  }
 public DefaultBlockingState(
     final UUID blockingId,
     final BlockingStateType type,
     final String stateName,
     final String service,
     final boolean blockChange,
     final boolean blockEntitlement,
     final boolean blockBilling,
     final DateTime effectiveDate) {
   this(
       UUIDs.randomUUID(),
       blockingId,
       type,
       stateName,
       service,
       blockChange,
       blockEntitlement,
       blockBilling,
       effectiveDate,
       null,
       null,
       0L);
 }
  @Override
  public void leavingState(final State state) throws OperationException {
    final DateTime utcNow = pluginControlPaymentAutomatonRunner.getClock().getUTCNow();

    // Retrieve the associated payment transaction, if any
    PaymentTransactionModelDao paymentTransactionModelDaoCandidate = null;
    if (stateContext.getTransactionId() != null) {
      paymentTransactionModelDaoCandidate =
          paymentDao.getPaymentTransaction(
              stateContext.getTransactionId(), stateContext.getInternalCallContext());
      Preconditions.checkNotNull(
          paymentTransactionModelDaoCandidate,
          "paymentTransaction cannot be null for id " + stateContext.getTransactionId());
    } else if (stateContext.getPaymentTransactionExternalKey() != null) {
      final List<PaymentTransactionModelDao> paymentTransactionModelDaos =
          paymentDao.getPaymentTransactionsByExternalKey(
              stateContext.getPaymentTransactionExternalKey(),
              stateContext.getInternalCallContext());
      if (!paymentTransactionModelDaos.isEmpty()) {
        paymentTransactionModelDaoCandidate =
            paymentTransactionModelDaos.get(paymentTransactionModelDaos.size() - 1);
      }
    }
    final PaymentTransactionModelDao paymentTransactionModelDao =
        paymentTransactionModelDaoCandidate != null
                && TRANSIENT_TRANSACTION_STATUSES.contains(
                    paymentTransactionModelDaoCandidate.getTransactionStatus())
            ? paymentTransactionModelDaoCandidate
            : null;

    if (stateContext.getPaymentId() != null && stateContext.getPaymentExternalKey() == null) {
      final PaymentModelDao payment =
          paymentDao.getPayment(stateContext.getPaymentId(), stateContext.getInternalCallContext());
      Preconditions.checkNotNull(
          payment, "payment cannot be null for id " + stateContext.getPaymentId());
      stateContext.setPaymentExternalKey(payment.getExternalKey());
      stateContext.setPaymentMethodId(payment.getPaymentMethodId());
    } else if (stateContext.getPaymentExternalKey() == null) {
      stateContext.setPaymentExternalKey(UUIDs.randomUUID().toString());
    }

    if (paymentTransactionModelDao != null) {
      stateContext.setPaymentTransactionExternalKey(
          paymentTransactionModelDao.getTransactionExternalKey());
    } else if (stateContext.getPaymentTransactionExternalKey() == null) {
      stateContext.setPaymentTransactionExternalKey(UUIDs.randomUUID().toString());
    }

    if (stateContext.getPaymentMethodId() == null) {
      // Similar logic in PaymentAutomatonRunner
      stateContext.setPaymentMethodId(stateContext.getAccount().getPaymentMethodId());
    }

    if (state.getName().equals(initialState.getName())
        || state.getName().equals(retriedState.getName())) {
      try {
        final PaymentAttemptModelDao attempt;
        if (paymentTransactionModelDao != null
            && paymentTransactionModelDao.getAttemptId() != null) {
          attempt =
              pluginControlPaymentAutomatonRunner
                  .getPaymentDao()
                  .getPaymentAttempt(
                      paymentTransactionModelDao.getAttemptId(),
                      stateContext.getInternalCallContext());
          Preconditions.checkNotNull(
              attempt,
              "attempt cannot be null for id " + paymentTransactionModelDao.getAttemptId());
        } else {
          //
          // We don't serialize any properties at this stage to avoid serializing sensitive
          // information.
          // However, if after going through the control plugins, the attempt end up in RETRIED
          // state,
          // the properties will be serialized in the enteringState callback (any plugin that sets a
          // retried date is responsible to correctly remove sensitive information such as CVV, ...)
          //
          final byte[] serializedProperties =
              PluginPropertySerializer.serialize(ImmutableList.<PluginProperty>of());

          attempt =
              new PaymentAttemptModelDao(
                  stateContext.getAccount().getId(),
                  stateContext.getPaymentMethodId(),
                  utcNow,
                  utcNow,
                  stateContext.getPaymentExternalKey(),
                  stateContext.getTransactionId(),
                  stateContext.getPaymentTransactionExternalKey(),
                  transactionType,
                  initialState.getName(),
                  stateContext.getAmount(),
                  stateContext.getCurrency(),
                  stateContext.getPaymentControlPluginNames(),
                  serializedProperties);
          pluginControlPaymentAutomatonRunner
              .getPaymentDao()
              .insertPaymentAttemptWithProperties(attempt, stateContext.getInternalCallContext());
        }

        stateContext.setAttemptId(attempt.getId());
      } catch (final PluginPropertySerializerException e) {
        throw new OperationException(e);
      }
    }
  }