Search in sources :

Example 46 with InvoiceApiException

use of org.killbill.billing.invoice.api.InvoiceApiException in project killbill by killbill.

the class InvoicePaymentControlPluginApi method onSuccessCall.

@Override
public OnSuccessPaymentControlResult onSuccessCall(final PaymentControlContext paymentControlContext, final Iterable<PluginProperty> pluginProperties) throws PaymentControlApiException {
    final TransactionType transactionType = paymentControlContext.getTransactionType();
    Preconditions.checkArgument(transactionType == TransactionType.PURCHASE || transactionType == TransactionType.REFUND || transactionType == TransactionType.CHARGEBACK || transactionType == TransactionType.CREDIT);
    final InternalCallContext internalContext = internalCallContextFactory.createInternalCallContext(paymentControlContext.getAccountId(), paymentControlContext);
    try {
        final InvoicePayment existingInvoicePayment;
        switch(transactionType) {
            case PURCHASE:
                final UUID invoiceId = getInvoiceId(pluginProperties);
                existingInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
                if (existingInvoicePayment != null && existingInvoicePayment.isSuccess()) {
                    // Only one successful purchase per payment (the invoice could be linked to multiple successful payments though)
                    log.info("onSuccessCall was already completed for purchase paymentId='{}'", paymentControlContext.getPaymentId());
                } else {
                    final BigDecimal invoicePaymentAmount;
                    if (paymentControlContext.getCurrency() == paymentControlContext.getProcessedCurrency()) {
                        invoicePaymentAmount = paymentControlContext.getProcessedAmount();
                    } else {
                        log.warn("processedCurrency='{}' of invoice paymentId='{}' doesn't match invoice currency='{}', assuming it is a full payment", paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getCurrency());
                        invoicePaymentAmount = paymentControlContext.getAmount();
                    }
                    final PaymentTransactionModelDao paymentTransactionModelDao = paymentDao.getPaymentTransaction(paymentControlContext.getTransactionId(), internalContext);
                    // If it's not SUCCESS, it is PENDING
                    final boolean success = paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.SUCCESS;
                    log.debug("Notifying invoice of {} paymentId='{}', amount='{}', currency='{}', invoiceId='{}'", success ? "successful" : "pending", paymentControlContext.getPaymentId(), invoicePaymentAmount, paymentControlContext.getCurrency(), invoiceId);
                    // For PENDING payments, the attempt will be kept as unsuccessful and an InvoicePaymentErrorInternalEvent sent on the bus (e.g. for Overdue)
                    invoiceApi.recordPaymentAttemptCompletion(invoiceId, invoicePaymentAmount, paymentControlContext.getCurrency(), paymentControlContext.getProcessedCurrency(), paymentControlContext.getPaymentId(), paymentControlContext.getTransactionExternalKey(), paymentControlContext.getCreatedDate(), success, internalContext);
                }
                break;
            case REFUND:
                final Map<UUID, BigDecimal> idWithAmount = extractIdsWithAmountFromProperties(pluginProperties);
                final PluginProperty prop = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
                final boolean isAdjusted = prop != null ? Boolean.valueOf((String) prop.getValue()) : false;
                invoiceApi.recordRefund(paymentControlContext.getPaymentId(), paymentControlContext.getAmount(), isAdjusted, idWithAmount, paymentControlContext.getTransactionExternalKey(), internalContext);
                break;
            case CHARGEBACK:
                existingInvoicePayment = invoiceApi.getInvoicePaymentForChargeback(paymentControlContext.getPaymentId(), internalContext);
                if (existingInvoicePayment != null) {
                    // We don't support partial chargebacks (yet?)
                    log.info("onSuccessCall was already completed for chargeback paymentId='{}'", paymentControlContext.getPaymentId());
                } else {
                    final InvoicePayment linkedInvoicePayment = invoiceApi.getInvoicePaymentForAttempt(paymentControlContext.getPaymentId(), internalContext);
                    final BigDecimal amount;
                    final Currency currency;
                    if (linkedInvoicePayment.getCurrency().equals(paymentControlContext.getProcessedCurrency()) && paymentControlContext.getProcessedAmount() != null) {
                        amount = paymentControlContext.getProcessedAmount();
                        currency = paymentControlContext.getProcessedCurrency();
                    } else if (linkedInvoicePayment.getCurrency().equals(paymentControlContext.getCurrency()) && paymentControlContext.getAmount() != null) {
                        amount = paymentControlContext.getAmount();
                        currency = paymentControlContext.getCurrency();
                    } else {
                        amount = linkedInvoicePayment.getAmount();
                        currency = linkedInvoicePayment.getCurrency();
                    }
                    invoiceApi.recordChargeback(paymentControlContext.getPaymentId(), paymentControlContext.getTransactionExternalKey(), amount, currency, internalContext);
                }
                break;
            case CREDIT:
                final Map<UUID, BigDecimal> idWithAmountMap = extractIdsWithAmountFromProperties(pluginProperties);
                final PluginProperty properties = getPluginProperty(pluginProperties, PROP_IPCD_REFUND_WITH_ADJUSTMENTS);
                final boolean isInvoiceAdjusted = properties != null ? Boolean.valueOf((String) properties.getValue()) : false;
                final PluginProperty legacyPayment = getPluginProperty(pluginProperties, PROP_IPCD_PAYMENT_ID);
                final UUID paymentId = legacyPayment != null ? (UUID) legacyPayment.getValue() : paymentControlContext.getPaymentId();
                invoiceApi.recordRefund(paymentId, paymentControlContext.getAmount(), isInvoiceAdjusted, idWithAmountMap, paymentControlContext.getTransactionExternalKey(), internalContext);
                break;
            default:
                throw new IllegalStateException("Unexpected transactionType " + transactionType);
        }
    } catch (final InvoiceApiException e) {
        log.warn("onSuccessCall failed for attemptId='{}', transactionType='{}'", paymentControlContext.getAttemptPaymentId(), transactionType, e);
    }
    return new DefaultOnSuccessPaymentControlResult();
}
Also used : InvoicePayment(org.killbill.billing.invoice.api.InvoicePayment) TransactionType(org.killbill.billing.payment.api.TransactionType) DefaultOnSuccessPaymentControlResult(org.killbill.billing.payment.retry.DefaultOnSuccessPaymentControlResult) InternalCallContext(org.killbill.billing.callcontext.InternalCallContext) BigDecimal(java.math.BigDecimal) PluginProperty(org.killbill.billing.payment.api.PluginProperty) InvoiceApiException(org.killbill.billing.invoice.api.InvoiceApiException) PaymentTransactionModelDao(org.killbill.billing.payment.dao.PaymentTransactionModelDao) Currency(org.killbill.billing.catalog.api.Currency) UUID(java.util.UUID)

Example 47 with InvoiceApiException

use of org.killbill.billing.invoice.api.InvoiceApiException in project killbill by killbill.

the class InvoicePaymentControlPluginApi method getPluginPurchaseResult.

private PriorPaymentControlResult getPluginPurchaseResult(final PaymentControlContext paymentControlPluginContext, final Iterable<PluginProperty> pluginProperties, final InternalCallContext internalContext) throws PaymentControlApiException {
    try {
        final UUID invoiceId = getInvoiceId(pluginProperties);
        final Invoice invoice = getAndSanitizeInvoice(invoiceId, internalContext);
        if (!InvoiceStatus.COMMITTED.equals(invoice.getStatus())) {
            // abort payment if the invoice status is not COMMITTED
            return new DefaultPriorPaymentControlResult(true);
        }
        // Get account and check if it is child and payment is delegated to parent => abort
        final AccountData accountData = accountApi.getAccountById(invoice.getAccountId(), internalContext);
        if ((accountData != null) && (accountData.getParentAccountId() != null) && accountData.isPaymentDelegatedToParent()) {
            return new DefaultPriorPaymentControlResult(true);
        }
        final BigDecimal requestedAmount = validateAndComputePaymentAmount(invoice, paymentControlPluginContext.getAmount(), paymentControlPluginContext.isApiPayment());
        final boolean isAborted = requestedAmount.compareTo(BigDecimal.ZERO) == 0;
        if (!isAborted && paymentControlPluginContext.getPaymentMethodId() == null) {
            log.warn("Payment for invoiceId='{}' was not triggered, accountId='{}' doesn't have a default payment method", getInvoiceId(pluginProperties), paymentControlPluginContext.getAccountId());
            invoiceApi.recordPaymentAttemptCompletion(invoiceId, paymentControlPluginContext.getAmount(), paymentControlPluginContext.getCurrency(), paymentControlPluginContext.getProcessedCurrency(), paymentControlPluginContext.getPaymentId(), paymentControlPluginContext.getTransactionExternalKey(), paymentControlPluginContext.getCreatedDate(), false, internalContext);
            return new DefaultPriorPaymentControlResult(true);
        }
        if (!isAborted && insert_AUTO_PAY_OFF_ifRequired(paymentControlPluginContext, requestedAmount)) {
            return new DefaultPriorPaymentControlResult(true);
        }
        if (paymentControlPluginContext.isApiPayment() && isAborted) {
            throw new PaymentControlApiException("Abort purchase call: ", new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, String.format("Aborted Payment for invoice %s : invoice balance is = %s, requested payment amount is = %s", invoice.getId(), invoice.getBalance(), paymentControlPluginContext.getAmount())));
        } else {
            //
            // Insert attempt row with a success = false status to implement a two-phase commit strategy and guard against scenario where payment would go through
            // but onSuccessCall callback never gets called (leaving the place for a double payment if user retries the operation)
            //
            invoiceApi.recordPaymentAttemptInit(invoice.getId(), MoreObjects.firstNonNull(paymentControlPluginContext.getAmount(), BigDecimal.ZERO), paymentControlPluginContext.getCurrency(), paymentControlPluginContext.getCurrency(), // to match the operation in the checkForIncompleteInvoicePaymentAndRepair logic below
            paymentControlPluginContext.getPaymentId(), paymentControlPluginContext.getTransactionExternalKey(), paymentControlPluginContext.getCreatedDate(), internalContext);
            return new DefaultPriorPaymentControlResult(isAborted, requestedAmount);
        }
    } catch (final InvoiceApiException e) {
        throw new PaymentControlApiException(e);
    } catch (final IllegalArgumentException e) {
        throw new PaymentControlApiException(e);
    } catch (AccountApiException e) {
        throw new PaymentControlApiException(e);
    }
}
Also used : InvoiceApiException(org.killbill.billing.invoice.api.InvoiceApiException) Invoice(org.killbill.billing.invoice.api.Invoice) AccountData(org.killbill.billing.account.api.AccountData) AccountApiException(org.killbill.billing.account.api.AccountApiException) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) UUID(java.util.UUID) DefaultPriorPaymentControlResult(org.killbill.billing.payment.retry.DefaultPriorPaymentControlResult) BigDecimal(java.math.BigDecimal) PaymentControlApiException(org.killbill.billing.control.plugin.api.PaymentControlApiException)

Example 48 with InvoiceApiException

use of org.killbill.billing.invoice.api.InvoiceApiException in project killbill by killbill.

the class InvoicePaymentControlPluginApi method computeRefundAmount.

private BigDecimal computeRefundAmount(final UUID paymentId, @Nullable final BigDecimal specifiedRefundAmount, final Map<UUID, BigDecimal> invoiceItemIdsWithAmounts, final InternalTenantContext context) throws PaymentControlApiException {
    if (specifiedRefundAmount != null) {
        if (specifiedRefundAmount.compareTo(BigDecimal.ZERO) <= 0) {
            throw new PaymentControlApiException("Failed to compute refund: ", new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, "You need to specify a positive refund amount"));
        }
        return specifiedRefundAmount;
    }
    try {
        final List<InvoiceItem> items = invoiceApi.getInvoiceForPaymentId(paymentId, context).getInvoiceItems();
        BigDecimal amountFromItems = BigDecimal.ZERO;
        for (final UUID itemId : invoiceItemIdsWithAmounts.keySet()) {
            final BigDecimal specifiedItemAmount = invoiceItemIdsWithAmounts.get(itemId);
            final BigDecimal itemAmount = getAmountFromItem(items, itemId);
            if (specifiedItemAmount != null && (specifiedItemAmount.compareTo(BigDecimal.ZERO) <= 0 || specifiedItemAmount.compareTo(itemAmount) > 0)) {
                throw new PaymentControlApiException("Failed to compute refund: ", new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_EXCEPTION, "You need to specify a valid invoice item amount"));
            }
            amountFromItems = amountFromItems.add(MoreObjects.firstNonNull(specifiedItemAmount, itemAmount));
        }
        return amountFromItems;
    } catch (final InvoiceApiException e) {
        throw new PaymentControlApiException(e);
    }
}
Also used : InvoiceApiException(org.killbill.billing.invoice.api.InvoiceApiException) InvoiceItem(org.killbill.billing.invoice.api.InvoiceItem) PaymentApiException(org.killbill.billing.payment.api.PaymentApiException) UUID(java.util.UUID) PaymentControlApiException(org.killbill.billing.control.plugin.api.PaymentControlApiException) BigDecimal(java.math.BigDecimal)

Example 49 with InvoiceApiException

use of org.killbill.billing.invoice.api.InvoiceApiException in project killbill by killbill.

the class TestDefaultInvoiceDaoUnit method testComputePositiveRefundAmount.

@Test(groups = "fast")
public void testComputePositiveRefundAmount() throws Exception {
    // Verify the cases with no adjustment first
    final Map<UUID, BigDecimal> noItemAdjustment = ImmutableMap.<UUID, BigDecimal>of();
    verifyComputedRefundAmount(null, null, noItemAdjustment, BigDecimal.ZERO);
    verifyComputedRefundAmount(null, BigDecimal.ZERO, noItemAdjustment, BigDecimal.ZERO);
    verifyComputedRefundAmount(BigDecimal.TEN, null, noItemAdjustment, BigDecimal.TEN);
    verifyComputedRefundAmount(BigDecimal.TEN, BigDecimal.ONE, noItemAdjustment, BigDecimal.ONE);
    try {
        verifyComputedRefundAmount(BigDecimal.ONE, BigDecimal.TEN, noItemAdjustment, BigDecimal.TEN);
        Assert.fail("Shouldn't have been able to compute a refund amount");
    } catch (InvoiceApiException e) {
        Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_TOO_HIGH.getCode());
    }
    // Try with adjustments now
    final Map<UUID, BigDecimal> itemAdjustments = ImmutableMap.<UUID, BigDecimal>of(UUID.randomUUID(), BigDecimal.ONE, UUID.randomUUID(), BigDecimal.TEN, UUID.randomUUID(), BigDecimal.ZERO);
    verifyComputedRefundAmount(new BigDecimal("100"), new BigDecimal("11"), itemAdjustments, new BigDecimal("11"));
    try {
        verifyComputedRefundAmount(new BigDecimal("100"), BigDecimal.TEN, itemAdjustments, BigDecimal.TEN);
        Assert.fail("Shouldn't have been able to compute a refund amount");
    } catch (InvoiceApiException e) {
        Assert.assertEquals(e.getCode(), ErrorCode.REFUND_AMOUNT_DONT_MATCH_ITEMS_TO_ADJUST.getCode());
    }
}
Also used : InvoiceApiException(org.killbill.billing.invoice.api.InvoiceApiException) UUID(java.util.UUID) BigDecimal(java.math.BigDecimal) Test(org.testng.annotations.Test)

Example 50 with InvoiceApiException

use of org.killbill.billing.invoice.api.InvoiceApiException in project killbill by killbill.

the class InvoiceResource method generateDryRunInvoice.

@TimedResource
@POST
@Path("/" + DRY_RUN)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
@ApiOperation(value = "Generate a dryRun invoice", response = InvoiceJson.class)
@ApiResponses(value = { @ApiResponse(code = 400, message = "Invalid account id or target datetime supplied") })
public Response generateDryRunInvoice(@Nullable final InvoiceDryRunJson dryRunSubscriptionSpec, @QueryParam(QUERY_ACCOUNT_ID) final String accountId, @Nullable @QueryParam(QUERY_TARGET_DATE) final String targetDate, @HeaderParam(HDR_CREATED_BY) final String createdBy, @HeaderParam(HDR_REASON) final String reason, @HeaderParam(HDR_COMMENT) final String comment, @javax.ws.rs.core.Context final HttpServletRequest request, @javax.ws.rs.core.Context final UriInfo uriInfo) throws AccountApiException, InvoiceApiException {
    final CallContext callContext = context.createContext(createdBy, reason, comment, request);
    final LocalDate inputDate;
    if (dryRunSubscriptionSpec != null) {
        if (DryRunType.UPCOMING_INVOICE.name().equals(dryRunSubscriptionSpec.getDryRunType())) {
            inputDate = null;
        } else if (DryRunType.SUBSCRIPTION_ACTION.name().equals(dryRunSubscriptionSpec.getDryRunType()) && dryRunSubscriptionSpec.getEffectiveDate() != null) {
            inputDate = dryRunSubscriptionSpec.getEffectiveDate();
        } else {
            inputDate = toLocalDate(targetDate);
        }
    } else {
        inputDate = toLocalDate(targetDate);
    }
    // On the other hand if body is not null, we are attempting a dryRun subscription operation
    if (dryRunSubscriptionSpec != null && dryRunSubscriptionSpec.getDryRunAction() != null) {
        if (SubscriptionEventType.START_BILLING.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductCategory(), "DryRun subscription product category should be specified");
            if (dryRunSubscriptionSpec.getProductCategory().equals(ProductCategory.ADD_ON)) {
                verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBundleId(), "DryRun bundle ID should be specified");
            }
        } else if (SubscriptionEventType.CHANGE.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getProductName(), "DryRun subscription product category should be specified");
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getBillingPeriod(), "DryRun subscription billingPeriod should be specified");
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
        } else if (SubscriptionEventType.STOP_BILLING.toString().equals(dryRunSubscriptionSpec.getDryRunAction())) {
            verifyNonNullOrEmpty(dryRunSubscriptionSpec.getSubscriptionId(), "DryRun subscriptionID should be specified");
        }
    }
    final Account account = accountUserApi.getAccountById(UUID.fromString(accountId), callContext);
    final DryRunArguments dryRunArguments = new DefaultDryRunArguments(dryRunSubscriptionSpec, account);
    try {
        final Invoice generatedInvoice = invoiceApi.triggerInvoiceGeneration(UUID.fromString(accountId), inputDate, dryRunArguments, callContext);
        return Response.status(Status.OK).entity(new InvoiceJson(generatedInvoice, true, null, null)).build();
    } catch (InvoiceApiException e) {
        if (e.getCode() == ErrorCode.INVOICE_NOTHING_TO_DO.getCode()) {
            return Response.status(Status.NOT_FOUND).build();
        }
        throw e;
    }
}
Also used : Account(org.killbill.billing.account.api.Account) InvoiceApiException(org.killbill.billing.invoice.api.InvoiceApiException) Invoice(org.killbill.billing.invoice.api.Invoice) DryRunArguments(org.killbill.billing.invoice.api.DryRunArguments) InvoiceJson(org.killbill.billing.jaxrs.json.InvoiceJson) CallContext(org.killbill.billing.util.callcontext.CallContext) LocalDate(org.joda.time.LocalDate) Path(javax.ws.rs.Path) TimedResource(org.killbill.commons.metrics.TimedResource) POST(javax.ws.rs.POST) Consumes(javax.ws.rs.Consumes) Produces(javax.ws.rs.Produces) ApiOperation(io.swagger.annotations.ApiOperation) ApiResponses(io.swagger.annotations.ApiResponses)

Aggregations

InvoiceApiException (org.killbill.billing.invoice.api.InvoiceApiException)56 UUID (java.util.UUID)29 Invoice (org.killbill.billing.invoice.api.Invoice)26 InvoiceItem (org.killbill.billing.invoice.api.InvoiceItem)24 BigDecimal (java.math.BigDecimal)23 LocalDate (org.joda.time.LocalDate)23 DefaultInvoice (org.killbill.billing.invoice.model.DefaultInvoice)19 Test (org.testng.annotations.Test)16 InternalCallContext (org.killbill.billing.callcontext.InternalCallContext)14 FixedPriceInvoiceItem (org.killbill.billing.invoice.model.FixedPriceInvoiceItem)14 RecurringInvoiceItem (org.killbill.billing.invoice.model.RecurringInvoiceItem)14 LinkedList (java.util.LinkedList)13 ItemAdjInvoiceItem (org.killbill.billing.invoice.model.ItemAdjInvoiceItem)12 RepairAdjInvoiceItem (org.killbill.billing.invoice.model.RepairAdjInvoiceItem)12 BillingEventSet (org.killbill.billing.junction.BillingEventSet)9 AccountApiException (org.killbill.billing.account.api.AccountApiException)7 MockPlan (org.killbill.billing.catalog.MockPlan)7 MockPlanPhase (org.killbill.billing.catalog.MockPlanPhase)7 MockBillingEventSet (org.killbill.billing.invoice.MockBillingEventSet)7 SubscriptionFutureNotificationDates (org.killbill.billing.invoice.generator.InvoiceWithMetadata.SubscriptionFutureNotificationDates)7