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();
}
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);
}
}
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);
}
}
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());
}
}
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;
}
}
Aggregations