use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.
the class PluginControlPaymentProcessor method retryPaymentTransaction.
public void retryPaymentTransaction(final UUID attemptId, final List<String> paymentControlPluginNames, final InternalCallContext internalCallContext) {
final PaymentAttemptModelDao attempt = paymentDao.getPaymentAttempt(attemptId, internalCallContext);
log.info("Retrying attemptId='{}', paymentExternalKey='{}', transactionExternalKey='{}'. paymentControlPluginNames='{}'", attemptId, attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), paymentControlPluginNames);
final PaymentModelDao paymentModelDao = paymentDao.getPaymentByExternalKey(attempt.getPaymentExternalKey(), internalCallContext);
final UUID paymentId = paymentModelDao != null ? paymentModelDao.getId() : null;
final CallContext callContext = buildCallContext(internalCallContext);
final String transactionType = TransactionType.PURCHASE.name();
Account account = null;
Payment payment = null;
PaymentTransaction paymentTransaction = null;
try {
account = accountInternalApi.getAccountById(attempt.getAccountId(), internalCallContext);
final State state = paymentControlStateMachineHelper.getState(attempt.getStateName());
final Iterable<PluginProperty> pluginProperties = PluginPropertySerializer.deserialize(attempt.getPluginProperties());
logEnterAPICall(log, transactionType, account, attempt.getPaymentMethodId(), paymentId, null, attempt.getAmount(), attempt.getCurrency(), attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), null, paymentControlPluginNames);
payment = pluginControlledPaymentAutomatonRunner.run(state, false, attempt.getTransactionType(), ControlOperation.valueOf(attempt.getTransactionType().toString()), account, attempt.getPaymentMethodId(), paymentId, attempt.getPaymentExternalKey(), attempt.getTransactionExternalKey(), // The amount on the payment attempt drives the amount of the new payment transaction
attempt.getAmount(), attempt.getCurrency(), null, pluginProperties, paymentControlPluginNames, callContext, internalCallContext);
log.debug("retryPaymentTransaction result: payment='{}'", payment);
paymentTransaction = Iterables.<PaymentTransaction>find(Lists.<PaymentTransaction>reverse(payment.getTransactions()), new Predicate<PaymentTransaction>() {
@Override
public boolean apply(final PaymentTransaction input) {
return attempt.getTransactionExternalKey().equals(input.getExternalKey());
}
});
} catch (final AccountApiException e) {
log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
} catch (final PaymentApiException e) {
// Log exception unless nothing left to be paid
if (e.getCode() == ErrorCode.PAYMENT_PLUGIN_API_ABORTED.getCode() && paymentControlPluginNames != null && paymentControlPluginNames.size() == 1 && InvoicePaymentControlPluginApi.PLUGIN_NAME.equals(paymentControlPluginNames.get(0))) {
log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames));
} else {
log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
}
} catch (final PluginPropertySerializerException e) {
log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
} catch (final MissingEntryException e) {
log.warn("Failed to retry attemptId='{}', paymentControlPlugins='{}'", attemptId, toPluginNamesOnError(paymentControlPluginNames), e);
} finally {
logExitAPICall(log, transactionType, account, payment != null ? payment.getPaymentMethodId() : null, payment != null ? payment.getId() : null, paymentTransaction != null ? paymentTransaction.getId() : null, paymentTransaction != null ? paymentTransaction.getProcessedAmount() : null, paymentTransaction != null ? paymentTransaction.getProcessedCurrency() : null, payment != null ? payment.getExternalKey() : null, paymentTransaction != null ? paymentTransaction.getExternalKey() : null, paymentTransaction != null ? paymentTransaction.getTransactionStatus() : null, paymentControlPluginNames, null);
}
}
use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.
the class IncompletePaymentAttemptTask method doIteration.
// Since the code is a bit tedious to follow, I'm adding some notes here on where isApiPayment is used (valid as of 09/19/2019 - might become stale!):
// * Passed through to plugins as PaymentControlContext#isApiPayment (e.g. InvoicePaymentControlPluginApi)
// * Used in PaymentEnteringStateCallback to decide whether to send a PaymentErrorInternalEvent
// To ensure that payments are retried, isApiPayment must be true (see https://github.com/killbill/killbill/issues/880).
@VisibleForTesting
public boolean doIteration(final PaymentAttemptModelDao attempt, final boolean isApiPayment) {
// We don't grab account lock here as the lock will be taken when calling the completeRun API.
final InternalTenantContext tenantContext = internalCallContextFactory.createInternalTenantContext(attempt.getTenantRecordId(), attempt.getAccountRecordId());
final InternalCallContext internalCallContext = internalCallContextFactory.createInternalCallContext(tenantContext.getTenantRecordId(), tenantContext.getAccountRecordId(), "AttemptCompletionJanitorTask", CallOrigin.INTERNAL, UserType.SYSTEM, UUIDs.randomUUID());
final List<PaymentTransactionModelDao> transactions = paymentDao.getPaymentTransactionsByExternalKey(attempt.getTransactionExternalKey(), tenantContext);
final List<PaymentTransactionModelDao> filteredTransactions = ImmutableList.copyOf(Iterables.filter(transactions, new Predicate<PaymentTransactionModelDao>() {
@Override
public boolean apply(final PaymentTransactionModelDao input) {
return input.getAttemptId().equals(attempt.getId());
}
}));
// We only expect at most one transaction for a given attempt, but as a precaution we check for more; if this is the case we log a warn and continue processing the first one.
if (filteredTransactions.size() > 1) {
log.warn("Found {} transactions for paymentAttempt {}", filteredTransactions.size(), attempt.getId());
}
final PaymentTransactionModelDao transaction = filteredTransactions.isEmpty() ? null : filteredTransactions.get(0);
if (transaction == null) {
log.info("Moving attemptId='{}' to ABORTED", attempt.getId());
paymentDao.updatePaymentAttemptWithProperties(attempt.getId(), attempt.getPaymentMethodId(), attempt.getTransactionId(), "ABORTED", // Keep the initial amount, as this will drive the retries
attempt.getAmount(), attempt.getCurrency(), attempt.getPluginProperties(), internalCallContext);
return true;
}
// at which point the attempt can also be transition to a different state.
if (transaction.getTransactionStatus() == TransactionStatus.UNKNOWN) {
return false;
}
try {
log.info("Completing attemptId='{}', stateName='{}'", attempt.getId(), attempt.getStateName());
final Account account = accountInternalApi.getAccountById(attempt.getAccountId(), tenantContext);
final PaymentStateControlContext paymentStateContext = new PaymentStateControlContext(attempt.toPaymentControlPluginNames(), isApiPayment, null, transaction.getPaymentId(), attempt.getPaymentExternalKey(), transaction.getId(), transaction.getTransactionExternalKey(), transaction.getTransactionType(), account, attempt.getPaymentMethodId(), transaction.getAmount(), transaction.getCurrency(), null, PluginPropertySerializer.deserialize(attempt.getPluginProperties()), internalCallContext, internalCallContextFactory.createCallContext(internalCallContext));
// Normally set by leavingState Callback
paymentStateContext.setAttemptId(attempt.getId());
// Normally set by raw state machine
paymentStateContext.setPaymentTransactionModelDao(transaction);
//
// Will rerun the state machine with special callbacks to only make the executePluginOnSuccessCalls / executePluginOnFailureCalls calls
// to the PaymentControlPluginApi plugin and transition the state.
//
pluginControlledPaymentAutomatonRunner.completeRun(paymentStateContext);
return true;
} catch (final AccountApiException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
} catch (final PluginPropertySerializerException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
} catch (final PaymentApiException e) {
log.warn("Error completing paymentAttemptId='{}'", attempt.getId(), e);
}
return false;
}
use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.
the class PaymentMethodProcessor method refreshPaymentMethods.
/**
* This refreshed the payment methods from the plugin for cases when adding payment method does not flow through KB because of PCI compliance
* issues. The logic below is not optimal because there is no atomicity in the step but the good news is that this is idempotent so can always be
* replayed if necessary-- partial failure scenario.
*
* @param pluginName
* @param account
* @param context
* @return the list of payment methods -- should be identical between KB, the plugin view-- if it keeps a state-- and the gateway.
* @throws PaymentApiException
*/
public List<PaymentMethod> refreshPaymentMethods(final String pluginName, final Account account, final Iterable<PluginProperty> properties, final CallContext callContext, final InternalCallContext context) throws PaymentApiException {
// Don't hold the account lock while fetching the payment methods from the gateway as those could change anyway
final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
final List<PaymentMethodInfoPlugin> pluginPms;
try {
pluginPms = pluginApi.getPaymentMethods(account.getId(), true, properties, callContext);
// The method should never return null by convention, but let's not trust the plugin...
if (pluginPms == null) {
log.debug("No payment methods defined on the account {} for plugin {}", account.getId(), pluginName);
return ImmutableList.<PaymentMethod>of();
}
} catch (final PaymentPluginApiException e) {
throw new PaymentApiException(e, ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
}
try {
final PluginDispatcherReturnType<List<PaymentMethod>> result = new WithAccountLock<List<PaymentMethod>, PaymentApiException>(paymentConfig).processAccountWithLock(locker, account.getId(), new DispatcherCallback<PluginDispatcherReturnType<List<PaymentMethod>>, PaymentApiException>() {
@Override
public PluginDispatcherReturnType<List<PaymentMethod>> doOperation() throws PaymentApiException {
UUID defaultPaymentMethodId = null;
final List<PaymentMethodInfoPlugin> pluginPmsWithId = new ArrayList<PaymentMethodInfoPlugin>();
final List<PaymentMethodModelDao> finalPaymentMethods = new ArrayList<PaymentMethodModelDao>();
for (final PaymentMethodInfoPlugin cur : pluginPms) {
// If the kbPaymentId is NULL, the plugin does not know about it, so we create a new UUID
final UUID paymentMethodId = cur.getPaymentMethodId() != null ? cur.getPaymentMethodId() : UUIDs.randomUUID();
final String externalKey = cur.getExternalPaymentMethodId() != null ? cur.getExternalPaymentMethodId() : paymentMethodId.toString();
final PaymentMethod input = new DefaultPaymentMethod(paymentMethodId, externalKey, account.getId(), pluginName);
final PaymentMethodModelDao pmModel = new PaymentMethodModelDao(input.getId(), input.getExternalKey(), input.getCreatedDate(), input.getUpdatedDate(), input.getAccountId(), input.getPluginName(), input.isActive());
finalPaymentMethods.add(pmModel);
pluginPmsWithId.add(new DefaultPaymentMethodInfoPlugin(cur, paymentMethodId));
// will always return false - it's Kill Bill in that case which is responsible to manage default payment methods
if (cur.isDefault()) {
defaultPaymentMethodId = paymentMethodId;
}
}
final List<PaymentMethodModelDao> refreshedPaymentMethods = paymentDao.refreshPaymentMethods(pluginName, finalPaymentMethods, context);
try {
pluginApi.resetPaymentMethods(account.getId(), pluginPmsWithId, properties, callContext);
} catch (final PaymentPluginApiException e) {
throw new PaymentApiException(e, ErrorCode.PAYMENT_REFRESH_PAYMENT_METHOD, account.getId(), e.getErrorMessage());
}
try {
updateDefaultPaymentMethodIfNeeded(pluginName, account, defaultPaymentMethodId, context);
} catch (final AccountApiException e) {
throw new PaymentApiException(e);
}
final List<PaymentMethod> result = ImmutableList.<PaymentMethod>copyOf(Collections2.transform(refreshedPaymentMethods, new Function<PaymentMethodModelDao, PaymentMethod>() {
@Override
public PaymentMethod apply(final PaymentMethodModelDao input) {
return new DefaultPaymentMethod(input, null);
}
}));
return PluginDispatcher.createPluginDispatcherReturnType(result);
}
});
return result.getReturnType();
} catch (final Exception e) {
throw new PaymentApiException(e, ErrorCode.PAYMENT_INTERNAL_ERROR, MoreObjects.firstNonNull(e.getMessage(), ""));
}
}
use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.
the class PaymentMethodProcessor method buildDefaultPaymentMethod.
private PaymentMethod buildDefaultPaymentMethod(final PaymentMethodModelDao paymentMethodModelDao, final boolean withPluginInfo, final Iterable<PluginProperty> properties, final TenantContext tenantContext, final InternalTenantContext context) throws PaymentApiException {
final PaymentMethodPlugin paymentMethodPlugin;
if (withPluginInfo) {
try {
final PaymentPluginApi pluginApi = getPaymentPluginApi(paymentMethodModelDao.getPluginName());
paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), properties, tenantContext);
} catch (final PaymentPluginApiException e) {
throw new PaymentApiException(e, ErrorCode.PAYMENT_GET_PAYMENT_METHODS, paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId());
}
} else {
paymentMethodPlugin = null;
}
return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
}
use of org.killbill.billing.payment.api.PaymentApiException in project killbill by killbill.
the class PaymentProcessor method findTransactionToCompleteAndRunSanityChecks.
private PaymentTransactionModelDao findTransactionToCompleteAndRunSanityChecks(final PaymentModelDao paymentModelDao, final Iterable<PaymentTransactionModelDao> paymentTransactionsForCurrentPayment, final PaymentStateContext paymentStateContext, final InternalCallContext internalCallContext) throws PaymentApiException {
final Collection<PaymentTransactionModelDao> completionCandidates = new LinkedList<PaymentTransactionModelDao>();
for (final PaymentTransactionModelDao paymentTransactionModelDao : paymentTransactionsForCurrentPayment) {
// Check if we already have a transaction for that id or key
if (!(paymentStateContext.getTransactionId() != null && paymentTransactionModelDao.getId().equals(paymentStateContext.getTransactionId())) && !(paymentStateContext.getPaymentTransactionExternalKey() != null && paymentTransactionModelDao.getTransactionExternalKey().equals(paymentStateContext.getPaymentTransactionExternalKey()))) {
// Sanity: if not, prevent multiple PENDING transactions for initial calls (cannot be enforced by the state machine unfortunately)
if ((paymentTransactionModelDao.getTransactionType() == TransactionType.AUTHORIZE || paymentTransactionModelDao.getTransactionType() == TransactionType.PURCHASE || paymentTransactionModelDao.getTransactionType() == TransactionType.CREDIT) && paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING) {
throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_OPERATION, paymentTransactionModelDao.getTransactionType(), paymentModelDao.getStateName());
} else {
continue;
}
}
// Sanity: if we already have a transaction for that id or key, the transaction type must match
if (paymentTransactionModelDao.getTransactionType() != paymentStateContext.getTransactionType()) {
throw new PaymentApiException(ErrorCode.PAYMENT_INVALID_PARAMETER, "transactionType", String.format("%s doesn't match existing transaction type %s", paymentStateContext.getTransactionType(), paymentTransactionModelDao.getTransactionType()));
}
// UNKNOWN transactions are potential candidates, we'll invoke the Janitor first though
if (paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.PENDING || paymentTransactionModelDao.getTransactionStatus() == TransactionStatus.UNKNOWN) {
completionCandidates.add(paymentTransactionModelDao);
}
}
Preconditions.checkState(Iterables.size(completionCandidates) <= 1, "There should be at most one completion candidate");
return Iterables.<PaymentTransactionModelDao>getLast(completionCandidates, null);
}
Aggregations