Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -335,36 +335,70 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI

final Map<String, Object> additionalDataMap;
final String stripeId;
final String customerId;
Comment thread
KBbitsP marked this conversation as resolved.
final String existingCustomerId = getCustomerIdNoException(kbAccountId, context);
if (paymentMethodIdInStripe != null) {
if ("payment_method".equals(objectType)) {
try {
final PaymentMethod stripePaymentMethod = PaymentMethod.retrieve(paymentMethodIdInStripe, requestOptions);
additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripePaymentMethod);
ImmutableMap<String, Object> params = ImmutableMap.of("payment_method", stripePaymentMethod.getId());
createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context);
stripeId = stripePaymentMethod.getId();
final PaymentMethod paymentMethodForAdditionalData;
if (existingCustomerId == null) {
ImmutableMap<String, Object> params = ImmutableMap.of("payment_method", stripePaymentMethod.getId());
createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context);
paymentMethodForAdditionalData = stripePaymentMethod;
} else {
ImmutableMap<String, Object> attachParams = ImmutableMap.of("customer", existingCustomerId);
paymentMethodForAdditionalData = stripePaymentMethod.attach(attachParams, requestOptions);
}
additionalDataMap = StripePluginProperties.toAdditionalDataMap(paymentMethodForAdditionalData);
stripeId = paymentMethodForAdditionalData.getId();
} catch (final StripeException e) {
throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e);
}
} else if ("token".equals(objectType)) {
try {
final Token stripeToken = Token.retrieve(paymentMethodIdInStripe, requestOptions);
additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken);
ImmutableMap<String, Object> params = ImmutableMap.of("source", stripeToken.getId());
String customerId = createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context);
stripeId = retrievePaymentMethod(customerId, existingCustomerId, getTokenInnerId(stripeToken), requestOptions);
} catch (final StripeException e) {

if (existingCustomerId == null) {
ImmutableMap<String, Object> params = ImmutableMap.of("source", stripeToken.getId());
customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context);
stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions);
} else {
final Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions);
final Map<String, Object> attachParams = new HashMap<>();
attachParams.put("source", stripeToken.getId());
final PaymentSource attachedSource = customer.getSources().create(attachParams, requestOptions);
stripeId = attachedSource.getId();

if (setDefault) {
final Map<String, Object> defaultParams = new HashMap<>();
defaultParams.put("default_source", stripeId);
customer.update(defaultParams, requestOptions);
}
}
} catch (final StripeException e) {
throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e);
}
} else if ("source".equals(objectType)) {
try {
// The Stripe sourceId must be passed as the PaymentMethodPlugin#getExternalPaymentMethodId
final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions);
additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeSource);
ImmutableMap<String, Object> params = ImmutableMap.of("source", stripeSource.getId());
createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context);
stripeId = stripeSource.getId();
final PaymentSource sourceForAdditionalData;

if (existingCustomerId == null) {
ImmutableMap<String, Object> params = ImmutableMap.of("source", stripeSource.getId());
createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context);
sourceForAdditionalData = stripeSource;
} else {
final Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions);
final Map<String, Object> attachParams = new HashMap<>();
attachParams.put("source", stripeSource.getId());
sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions);
}

additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData);
stripeId = sourceForAdditionalData.getId();

} catch (final StripeException e) {
throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,139 @@ public void testVerifySyncOfPaymentMethods() throws PaymentPluginApiException, S
context);
assertEquals(((Map) PluginProperties.toMap(paymentMethodDetail.getProperties()).get("metadata")).get("testing"), metadata.get("testing"));
}

@Test(groups = "integration")
public void testAddSecondPaymentMethodToExistingCustomer() throws PaymentPluginApiException, StripeException {
final UUID kbAccountId = account.getId();

// 1. Create the customer and the first payment method (This sets existingCustomerId)
createStripeCustomerWithCreditCardAndSyncPaymentMethod();
final String existingStripeCustomerId = customer.getId();

// Verify we start with exactly 1 payment method
List<PaymentMethodInfoPlugin> paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(paymentMethods.size(), 1);

// 2. Create a SECOND payment method in Stripe (e.g., a Mastercard)
// At this point, it is just a floating payment method not attached to any customer
final RequestOptions options = stripePaymentPluginApi.buildRequestOptions(context);
Map<String, Object> pmParams = new HashMap<>();
pmParams.put("type", "card");
pmParams.put("card", ImmutableMap.of("token", "tok_mastercard")); // Stripe testing token
PaymentMethod secondStripePaymentMethod = PaymentMethod.create(pmParams, options);

// 3. Add the second payment method to the existing Kill Bill account
// It should detect the existing customer and call .attach()
final UUID secondKbPaymentMethodId = UUID.randomUUID();
stripePaymentPluginApi.addPaymentMethod(kbAccountId,
secondKbPaymentMethodId,
new PluginPaymentMethodPlugin(secondKbPaymentMethodId, secondStripePaymentMethod.getId(), false, ImmutableList.of()),
false,
ImmutableList.of(),
context);

// 4. Reach out to Stripe and ensure the attachment actually happened
PaymentMethod retrievedSecondPm = PaymentMethod.retrieve(secondStripePaymentMethod.getId(), options);
assertNotNull(retrievedSecondPm.getCustomer(), "The Stripe PaymentMethod was not attached to a customer!");
assertEquals(retrievedSecondPm.getCustomer(), existingStripeCustomerId, "The second payment method should be attached to the ORIGINAL Stripe customer ID!");

// 5. Verify local Kill Bill database now correctly reflects 2 payment methods
paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(paymentMethods.size(), 2, "Kill Bill should have exactly 2 payment methods saved locally.");
}

@Test(groups = "integration")
public void testAddSecondTokenToExistingCustomer() throws PaymentPluginApiException, StripeException {
final UUID kbAccountId = account.getId();

// 1. Create the customer and the first payment method (This sets existingCustomerId)
createStripeCustomerWithCreditCardAndSyncPaymentMethod();
final String existingStripeCustomerId = customer.getId();

// Verify we start with exactly 1 payment method
List<PaymentMethodInfoPlugin> initialPaymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(initialPaymentMethods.size(), 1);

// 2. Create a raw Token in Stripe (simulating a legacy stripe.js token generation)
final RequestOptions options = stripePaymentPluginApi.buildRequestOptions(context);
final Map<String, Object> cardParams = new HashMap<>();
cardParams.put("number", "4242424242424242");
cardParams.put("exp_month", 12);
cardParams.put("exp_year", 2030);
cardParams.put("cvc", "123");
final Map<String, Object> tokenParams = new HashMap<>();
tokenParams.put("card", cardParams);
final Token stripeToken = Token.create(tokenParams, options);

// 3. Add the token to the existing Kill Bill account
final UUID secondKbPaymentMethodId = UUID.randomUUID();
stripePaymentPluginApi.addPaymentMethod(kbAccountId,
secondKbPaymentMethodId,
new PluginPaymentMethodPlugin(secondKbPaymentMethodId, null, false, ImmutableList.of()),
false,
ImmutableList.of(new PluginProperty("token", stripeToken.getId(), false)),
context);

// 4. Verify local Kill Bill database now correctly reflects 2 payment methods
List<PaymentMethodInfoPlugin> paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(paymentMethods.size(), 2, "Kill Bill should have exactly 2 payment methods saved locally.");

// 5. VERIFY THE FIX IN STRIPE: Ensure the token was consumed and attached as a source
final Map<String, Object> expandParams = new HashMap<>();
expandParams.put("expand", ImmutableList.of("sources"));
Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options);
assertEquals(retrievedCustomer.getSources().getData().size(), 1, "The new card from the token should be present in the Stripe Customer's sources list.");
// Verify the card ID saved in Kill Bill matches what's in Stripe's sources
final List<PaymentMethodInfoPlugin> finalPaymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
final String secondMethodStripeId = finalPaymentMethods.stream()
.filter(pm -> !pm.getPaymentMethodId().equals(initialPaymentMethods.get(0).getPaymentMethodId()))
.findFirst()
.orElseThrow(() -> new AssertionError("Expected a second payment method in Kill Bill but none was found"))
.getExternalPaymentMethodId();
assertEquals(retrievedCustomer.getSources().getData().get(0).getId(), secondMethodStripeId, "The card in Stripe sources should match the ID saved in Kill Bill.");
}

@Test(groups = "integration")
public void testAddSecondSourceToExistingCustomer() throws PaymentPluginApiException, StripeException {
final UUID kbAccountId = account.getId();

// 1. Create the customer and the first payment method (This sets existingCustomerId)
createStripeCustomerWithCreditCardAndSyncPaymentMethod();
final String existingStripeCustomerId = customer.getId();

// Verify we start with exactly 1 payment method
List<PaymentMethodInfoPlugin> paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(paymentMethods.size(), 1);

// 2. Create a raw Source object directly in Stripe (e.g., using an Amex test token)
final RequestOptions options = stripePaymentPluginApi.buildRequestOptions(context);
Map<String, Object> sourceParams = new HashMap<>();
sourceParams.put("type", "card");
sourceParams.put("token", "tok_amex");
Source stripeSource = Source.create(sourceParams, options);

// 3. Add the Source to the existing Kill Bill account
// THIS EXERCISES OUR FIX IN THE "source" BLOCK!
final UUID secondKbPaymentMethodId = UUID.randomUUID();
stripePaymentPluginApi.addPaymentMethod(kbAccountId,
secondKbPaymentMethodId,
new PluginPaymentMethodPlugin(secondKbPaymentMethodId, null, false, ImmutableList.of()),
false,
ImmutableList.of(new PluginProperty("source", stripeSource.getId(), false)),
context);

// 4. Verify local Kill Bill database now correctly reflects 2 payment methods
paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context);
assertEquals(paymentMethods.size(), 2, "Kill Bill should have exactly 2 payment methods saved locally.");

// 5. VERIFY THE FIX IN STRIPE: Ensure the source was physically attached to the customer
final Map<String, Object> expandParams = new HashMap<>();
expandParams.put("expand", ImmutableList.of("sources"));
Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options);
assertEquals(retrievedCustomer.getSources().getData().size(), 1, "The new source should be present in the Stripe Customer's sources list.");
// Also verify it's specifically the source we attached, not just any source
Comment thread
KBbitsP marked this conversation as resolved.
assertEquals(retrievedCustomer.getSources().getData().get(0).getId(), stripeSource.getId(), "The attached source ID should match the one we passed in.");
}

@Test(groups = "integration")
public void testDeletePaymentMethod() throws PaymentPluginApiException, StripeException {
Expand Down
Loading