mirror of
https://github.com/google/nomulus.git
synced 2025-05-28 13:51:15 +02:00
Delete all Braintree code
We never launched this, don't planning on launching it now anyway, and it's rotted over the past two years anyway. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=202993577
This commit is contained in:
parent
7023b818b7
commit
32b3563126
67 changed files with 28 additions and 3352 deletions
|
@ -12,7 +12,6 @@ java_library(
|
|||
"//java/google/registry/ui/css:registrar_dbg.css.js",
|
||||
],
|
||||
deps = [
|
||||
"//java/google/registry/braintree",
|
||||
"//java/google/registry/config",
|
||||
"//java/google/registry/export/sheet",
|
||||
"//java/google/registry/flows",
|
||||
|
|
|
@ -137,7 +137,6 @@ public final class ConsoleUiAction implements Runnable {
|
|||
Registrar.loadByClientIdCached(clientId), "Registrar %s does not exist", clientId);
|
||||
data.put("xsrfToken", xsrfTokenManager.generateToken(userAuthInfo.user().getEmail()));
|
||||
data.put("clientId", clientId);
|
||||
data.put("showPaymentLink", registrar.getBillingMethod() == Registrar.BillingMethod.BRAINTREE);
|
||||
data.put("requireFeeExtension", registrar.getPremiumPriceAckRequired());
|
||||
|
||||
String payload = TOFU_SUPPLIER.get()
|
||||
|
|
|
@ -1,350 +0,0 @@
|
|||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.ui.server.registrar;
|
||||
|
||||
import static com.google.common.base.MoreObjects.firstNonNull;
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.security.JsonResponseHelper.Status.ERROR;
|
||||
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import com.braintreegateway.BraintreeGateway;
|
||||
import com.braintreegateway.Result;
|
||||
import com.braintreegateway.Transaction;
|
||||
import com.braintreegateway.TransactionRequest;
|
||||
import com.braintreegateway.ValidationError;
|
||||
import com.braintreegateway.ValidationErrors;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.re2j.Pattern;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.JsonActionRunner;
|
||||
import google.registry.request.JsonActionRunner.JsonAction;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.security.JsonResponseHelper;
|
||||
import google.registry.ui.forms.FormField;
|
||||
import google.registry.ui.forms.FormFieldException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
import org.joda.money.IllegalCurrencyException;
|
||||
import org.joda.money.Money;
|
||||
|
||||
/**
|
||||
* Action handling submission of customer payment form.
|
||||
*
|
||||
* <h3>Request Object</h3>
|
||||
*
|
||||
* <p>The request payload is a JSON object with the following fields:
|
||||
*
|
||||
* <dl>
|
||||
* <dt>amount
|
||||
* <dd>String containing a fixed point value representing the amount of money the registrar
|
||||
* customer wishes to send the registry. This amount is arbitrary and entered manually by the
|
||||
* customer in the payment form, as there is currently no integration with the billing system.
|
||||
* <dt>currency
|
||||
* <dd>String containing a three letter ISO currency code, which is used to look up the Braintree
|
||||
* merchant account ID to which payment should be posted.
|
||||
* <dt>paymentMethodNonce
|
||||
* <dd>UUID nonce string supplied by the Braintree JS SDK representing the selected payment
|
||||
* method.
|
||||
* </dl>
|
||||
*
|
||||
* <h3>Response Object</h3>
|
||||
*
|
||||
* <p>The response payload will be a JSON response object (as defined by {@link JsonResponseHelper})
|
||||
* which, if successful, will contain a single result object with the following fields:
|
||||
*
|
||||
* <dl>
|
||||
* <dt>id
|
||||
* <dd>String containing transaction ID returned by Braintree gateway.
|
||||
* <dt>formattedAmount
|
||||
* <dd>String containing amount paid, which can be displayed to the customer on a success page.
|
||||
* </dl>
|
||||
*
|
||||
* <p><b>Note:</b> These definitions corresponds to Closure Compiler extern {@code
|
||||
* registry.rpc.Payment} which must be updated should these definitions change.
|
||||
*
|
||||
* <h3>PCI Compliance</h3>
|
||||
*
|
||||
* <p>The request object will not contain credit card information, but rather a {@code
|
||||
* payment_method_nonce} field that's populated by the Braintree JS SDK iframe.
|
||||
*
|
||||
* @see RegistrarPaymentSetupAction
|
||||
*/
|
||||
@Action(
|
||||
path = "/registrar-payment",
|
||||
method = Action.Method.POST,
|
||||
auth = Auth.AUTH_PUBLIC_LOGGED_IN
|
||||
)
|
||||
public final class RegistrarPaymentAction implements Runnable, JsonAction {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final FormField<String, BigDecimal> AMOUNT_FIELD =
|
||||
FormField.named("amount")
|
||||
.trimmed()
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.matches(Pattern.compile("-?\\d+(?:\\.\\d+)?"), "Invalid number.")
|
||||
.transform(
|
||||
BigDecimal.class,
|
||||
value -> {
|
||||
BigDecimal result = new BigDecimal(value);
|
||||
if (result.signum() != 1) {
|
||||
throw new FormFieldException("Must be a positive number.");
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.build();
|
||||
|
||||
private static final FormField<String, CurrencyUnit> CURRENCY_FIELD =
|
||||
FormField.named("currency")
|
||||
.trimmed()
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.matches(Pattern.compile("[A-Z]{3}"), "Invalid currency code.")
|
||||
.transform(
|
||||
CurrencyUnit.class,
|
||||
value -> {
|
||||
try {
|
||||
return CurrencyUnit.of(value);
|
||||
} catch (IllegalCurrencyException ignored) {
|
||||
throw new FormFieldException("Unknown ISO currency code.");
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
private static final FormField<String, String> PAYMENT_METHOD_NONCE_FIELD =
|
||||
FormField.named("paymentMethodNonce")
|
||||
.trimmed()
|
||||
.emptyToNull()
|
||||
.required()
|
||||
.build();
|
||||
|
||||
@Inject HttpServletRequest request;
|
||||
@Inject BraintreeGateway braintreeGateway;
|
||||
@Inject JsonActionRunner jsonActionRunner;
|
||||
@Inject AuthResult authResult;
|
||||
@Inject SessionUtils sessionUtils;
|
||||
@Inject @Config("braintreeMerchantAccountIds") ImmutableMap<CurrencyUnit, String> accountIds;
|
||||
@Inject RegistrarPaymentAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
jsonActionRunner.run(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
|
||||
Registrar registrar = sessionUtils.getRegistrarForAuthResult(request, authResult);
|
||||
logger.atInfo().log("Processing payment: %s", json);
|
||||
String paymentMethodNonce;
|
||||
Money amount;
|
||||
String merchantAccountId;
|
||||
try {
|
||||
paymentMethodNonce = PAYMENT_METHOD_NONCE_FIELD.extractUntyped(json).get();
|
||||
try {
|
||||
amount = Money.of(
|
||||
CURRENCY_FIELD.extractUntyped(json).get(),
|
||||
AMOUNT_FIELD.extractUntyped(json).get());
|
||||
} catch (ArithmeticException e) {
|
||||
// This happens when amount has more precision than the currency allows, e.g. $3.141.
|
||||
throw new FormFieldException(AMOUNT_FIELD.name(), e.getMessage());
|
||||
}
|
||||
merchantAccountId = accountIds.get(amount.getCurrencyUnit());
|
||||
if (merchantAccountId == null) {
|
||||
throw new FormFieldException(CURRENCY_FIELD.name(), "Unsupported currency.");
|
||||
}
|
||||
} catch (FormFieldException e) {
|
||||
logger.atWarning().withCause(e).log("Form field error in RegistrarPaymentAction.");
|
||||
return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
|
||||
}
|
||||
Result<Transaction> result =
|
||||
braintreeGateway.transaction().sale(
|
||||
new TransactionRequest()
|
||||
.amount(amount.getAmount())
|
||||
.paymentMethodNonce(paymentMethodNonce)
|
||||
.merchantAccountId(merchantAccountId)
|
||||
.customerId(registrar.getClientId())
|
||||
.options()
|
||||
.submitForSettlement(true)
|
||||
.done());
|
||||
if (result.isSuccess()) {
|
||||
return handleSuccessResponse(result.getTarget());
|
||||
} else if (result.getTransaction() != null) {
|
||||
Transaction transaction = result.getTransaction();
|
||||
switch (transaction.getStatus()) {
|
||||
case PROCESSOR_DECLINED:
|
||||
return handleProcessorDeclined(transaction);
|
||||
case SETTLEMENT_DECLINED:
|
||||
return handleSettlementDecline(transaction);
|
||||
case GATEWAY_REJECTED:
|
||||
return handleRejection(transaction);
|
||||
default:
|
||||
return handleMiscProcessorError(transaction);
|
||||
}
|
||||
} else {
|
||||
return handleValidationErrorResponse(result.getErrors());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a transaction success response.
|
||||
*
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/response/transaction/java#success">
|
||||
* Braintree - Transaction - Success</a>
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/general/statuses#transaction">
|
||||
* Braintree - Statuses - Transaction</a>
|
||||
*/
|
||||
private Map<String, Object> handleSuccessResponse(Transaction transaction) {
|
||||
// XXX: Currency scaling: https://github.com/braintree/braintree_java/issues/33
|
||||
Money amount =
|
||||
Money.of(CurrencyUnit.of(transaction.getCurrencyIsoCode()),
|
||||
transaction.getAmount().stripTrailingZeros());
|
||||
logger.atInfo().log(
|
||||
"Transaction for %s via %s %s with ID: %s",
|
||||
amount,
|
||||
transaction.getPaymentInstrumentType(), // e.g. credit_card, paypal_account
|
||||
transaction.getStatus(), // e.g. SUBMITTED_FOR_SETTLEMENT
|
||||
transaction.getId());
|
||||
return JsonResponseHelper
|
||||
.create(SUCCESS, "Payment processed successfully", asList(
|
||||
ImmutableMap.of(
|
||||
"id", transaction.getId(),
|
||||
"formattedAmount", formatMoney(amount))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a processor declined response.
|
||||
*
|
||||
* <p>This happens when the customer's bank blocks the transaction.
|
||||
*
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/response/transaction/java#processor-declined">
|
||||
* Braintree - Transaction - Processor declined</a>
|
||||
* @see <a href="https://articles.braintreepayments.com/control-panel/transactions/declines">
|
||||
* Braintree - Transactions/Declines</a>
|
||||
*/
|
||||
private Map<String, Object> handleProcessorDeclined(Transaction transaction) {
|
||||
logger.atWarning().log(
|
||||
"Processor declined: %s %s",
|
||||
transaction.getProcessorResponseCode(), transaction.getProcessorResponseText());
|
||||
return JsonResponseHelper.create(ERROR,
|
||||
"Payment declined: " + transaction.getProcessorResponseText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a settlement declined response.
|
||||
*
|
||||
* <p>This is a very rare condition that, for all intents and purposes, means the same thing as a
|
||||
* processor declined response.
|
||||
*
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/response/transaction/java#processor-settlement-declined">
|
||||
* Braintree - Transaction - Processor settlement declined</a>
|
||||
* @see <a href="https://articles.braintreepayments.com/control-panel/transactions/declines">
|
||||
* Braintree - Transactions/Declines</a>
|
||||
*/
|
||||
private Map<String, Object> handleSettlementDecline(Transaction transaction) {
|
||||
logger.atWarning().log(
|
||||
"Settlement declined: %s %s",
|
||||
transaction.getProcessorSettlementResponseCode(),
|
||||
transaction.getProcessorSettlementResponseText());
|
||||
return JsonResponseHelper.create(ERROR,
|
||||
"Payment declined: " + transaction.getProcessorSettlementResponseText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a gateway rejection response.
|
||||
*
|
||||
* <p>This happens when a transaction is blocked due to settings we configured ourselves in the
|
||||
* Braintree control panel.
|
||||
*
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/response/transaction/java#gateway-rejection">
|
||||
* Braintree - Transaction - Gateway rejection</a>
|
||||
* @see <a href="https://articles.braintreepayments.com/control-panel/transactions/gateway-rejections">
|
||||
* Braintree - Transactions/Gateway Rejections</a>
|
||||
* @see <a href="https://articles.braintreepayments.com/guides/fraud-tools/avs-cvv">
|
||||
* Braintree - Fruad Tools/Basic Fraud Tools - AVS and CVV rules</a>
|
||||
* @see <a href="https://articles.braintreepayments.com/guides/fraud-tools/overview">
|
||||
* Braintree - Fraud Tools/Overview</a>
|
||||
*/
|
||||
private Map<String, Object> handleRejection(Transaction transaction) {
|
||||
logger.atWarning().log("Gateway rejection: %s", transaction.getGatewayRejectionReason());
|
||||
switch (transaction.getGatewayRejectionReason()) {
|
||||
case DUPLICATE:
|
||||
return JsonResponseHelper.create(ERROR, "Payment rejected: Possible duplicate.");
|
||||
case AVS:
|
||||
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid address.");
|
||||
case CVV:
|
||||
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid CVV code.");
|
||||
case AVS_AND_CVV:
|
||||
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid address and CVV code.");
|
||||
case FRAUD:
|
||||
return JsonResponseHelper.create(ERROR,
|
||||
"Our merchant gateway suspects this payment of fraud. Please contact support.");
|
||||
default:
|
||||
return JsonResponseHelper.create(ERROR,
|
||||
"Payment rejected: " + transaction.getGatewayRejectionReason());
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles a miscellaneous transaction processing error response. */
|
||||
private Map<String, Object> handleMiscProcessorError(Transaction transaction) {
|
||||
logger.atWarning().log(
|
||||
"Error processing transaction: %s %s %s",
|
||||
transaction.getStatus(),
|
||||
transaction.getProcessorResponseCode(),
|
||||
transaction.getProcessorResponseText());
|
||||
return JsonResponseHelper.create(ERROR,
|
||||
"Payment failure: "
|
||||
+ firstNonNull(
|
||||
emptyToNull(transaction.getProcessorResponseText()),
|
||||
transaction.getStatus().toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a validation error response from Braintree.
|
||||
*
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/response/transaction/java#validation-errors">
|
||||
* Braintree - Transaction - Validation errors</a>
|
||||
* @see <a href="https://developers.braintreepayments.com/reference/general/validation-errors/all/java">
|
||||
* Braintree - Validation Errors/All</a>
|
||||
*/
|
||||
private Map<String, Object> handleValidationErrorResponse(ValidationErrors validationErrors) {
|
||||
List<ValidationError> errors = validationErrors.getAllDeepValidationErrors();
|
||||
verify(!errors.isEmpty(), "Payment failed but validation error list was empty");
|
||||
for (ValidationError error : errors) {
|
||||
logger.atWarning().log(
|
||||
"Payment validation failed on field: %s\nCode: %s\nMessage: %s",
|
||||
error.getAttribute(), error.getCode(), error.getMessage());
|
||||
}
|
||||
return JsonResponseHelper
|
||||
.createFormFieldError(errors.get(0).getMessage(), errors.get(0).getAttribute());
|
||||
}
|
||||
|
||||
private static String formatMoney(Money amount) {
|
||||
String symbol = amount.getCurrencyUnit().getSymbol(Locale.US);
|
||||
BigDecimal number = amount.getAmount().setScale(amount.getCurrencyUnit().getDecimalPlaces());
|
||||
return symbol.length() == 1 ? symbol + number : amount.toString();
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// 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 google.registry.ui.server.registrar;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static google.registry.security.JsonResponseHelper.Status.ERROR;
|
||||
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import com.braintreegateway.BraintreeGateway;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.braintree.BraintreeRegistrarSyncer;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.JsonActionRunner;
|
||||
import google.registry.request.JsonActionRunner.JsonAction;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.auth.AuthResult;
|
||||
import google.registry.security.JsonResponseHelper;
|
||||
import java.util.Map;
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import org.joda.money.CurrencyUnit;
|
||||
|
||||
/**
|
||||
* Action returning information needed to render payment form in browser.
|
||||
*
|
||||
* <h3>Request Object</h3>
|
||||
*
|
||||
* <p>The request payload must be an empty JSON object.
|
||||
*
|
||||
* <h3>Response Object</h3>
|
||||
*
|
||||
* <p>The response payload will be a JSON response object (as defined by {@link JsonResponseHelper})
|
||||
* containing a single result object with the following fields:
|
||||
*
|
||||
* <dl>
|
||||
* <dt>brainframe
|
||||
* <dd>URL for iframe that loads Braintree payment method selector.
|
||||
* <dt>token
|
||||
* <dd>Nonce string obtained from the Braintree API which is needed by the Braintree JS SDK.
|
||||
* <dt>currencies
|
||||
* <dd>Array of strings, each containing a three letter currency code, which should be displayed
|
||||
* to the customer in a drop-down field. This will be all currencies for which a Braintree
|
||||
* merchant account exists. A currency will even be displayed if no TLD is enabled on the
|
||||
* customer account that bills in that currency.
|
||||
* </dl>
|
||||
*
|
||||
* <p><b>Note:</b> These definitions corresponds to Closure Compiler extern {@code
|
||||
* registry.rpc.PaymentSetup} which must be updated should these definitions change.
|
||||
*
|
||||
* @see RegistrarPaymentAction
|
||||
* @see <a
|
||||
* href="https://developers.braintreepayments.com/start/hello-server/java#generate-a-client-token">Generate
|
||||
* a client token</a>
|
||||
*/
|
||||
@Action(
|
||||
path = "/registrar-payment-setup",
|
||||
method = Action.Method.POST,
|
||||
auth = Auth.AUTH_PUBLIC_LOGGED_IN
|
||||
)
|
||||
public final class RegistrarPaymentSetupAction implements Runnable, JsonAction {
|
||||
|
||||
@Inject HttpServletRequest request;
|
||||
@Inject BraintreeGateway braintreeGateway;
|
||||
@Inject BraintreeRegistrarSyncer customerSyncer;
|
||||
@Inject JsonActionRunner jsonActionRunner;
|
||||
@Inject AuthResult authResult;
|
||||
@Inject SessionUtils sessionUtils;
|
||||
@Inject @Config("brainframe") String brainframe;
|
||||
@Inject @Config("braintreeMerchantAccountIds") ImmutableMap<CurrencyUnit, String> accountIds;
|
||||
@Inject RegistrarPaymentSetupAction() {}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
jsonActionRunner.run(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
|
||||
Registrar registrar = sessionUtils.getRegistrarForAuthResult(request, authResult);
|
||||
|
||||
if (!json.isEmpty()) {
|
||||
return JsonResponseHelper.create(ERROR, "JSON request object must be empty");
|
||||
}
|
||||
|
||||
// payment.js is hard-coded to display a specific SOY error template for certain error messages.
|
||||
if (registrar.getBillingMethod() != Registrar.BillingMethod.BRAINTREE) {
|
||||
// Registrar needs to contact support to have their billing bit flipped.
|
||||
return JsonResponseHelper.create(ERROR, "not-using-cc-billing");
|
||||
}
|
||||
|
||||
// In order to set the customerId field on the payment, the customer must exist.
|
||||
customerSyncer.sync(registrar);
|
||||
|
||||
return JsonResponseHelper.create(
|
||||
SUCCESS,
|
||||
"Success",
|
||||
asList(
|
||||
ImmutableMap.of(
|
||||
"brainframe", brainframe,
|
||||
"token", braintreeGateway.clientToken().generate(),
|
||||
"currencies",
|
||||
accountIds
|
||||
.keySet()
|
||||
.stream()
|
||||
.map(Object::toString)
|
||||
.collect(toImmutableList()))));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue