From a58acabfd8de56fe214d6215964c4d1862a92178 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:28:19 +0530 Subject: [PATCH 01/14] Additional payment method allowed * Additional payment method allowed * Integration test --- .../plugin/stripe/StripePaymentPluginApi.java | 35 ++++++++++++---- .../stripe/TestStripePaymentPluginApi.java | 40 +++++++++++++++++++ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index c453f05..2d20868 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -335,14 +335,20 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI final Map additionalDataMap; final String stripeId; + final String customerId; 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 params = ImmutableMap.of("payment_method", stripePaymentMethod.getId()); - createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripePaymentMethod); + if (existingCustomerId == null) { + ImmutableMap params = ImmutableMap.of("payment_method", stripePaymentMethod.getId()); + createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + } else { + ImmutableMap attachParams = ImmutableMap.of("customer", existingCustomerId); + stripePaymentMethod.attach(attachParams, requestOptions); + } stripeId = stripePaymentMethod.getId(); } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); @@ -350,9 +356,16 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI } else if ("token".equals(objectType)) { try { final Token stripeToken = Token.retrieve(paymentMethodIdInStripe, requestOptions); - additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); - ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); - String customerId = createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); + if (existingCustomerId == null) { + ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); + customerId = createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + } else { + Customer customer = Customer.retrieve(existingCustomerId, requestOptions); + ImmutableMap attachParams = ImmutableMap.of("source", stripeToken.getId()); + customer.getSources().create(attachParams, requestOptions); + customerId = existingCustomerId; + } stripeId = retrievePaymentMethod(customerId, existingCustomerId, getTokenInnerId(stripeToken), requestOptions); } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); @@ -362,8 +375,14 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI // The Stripe sourceId must be passed as the PaymentMethodPlugin#getExternalPaymentMethodId final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions); additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeSource); - ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); - createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + if (existingCustomerId == null) { + ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); + createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + } else { + Customer customer = Customer.retrieve(existingCustomerId, requestOptions); + ImmutableMap attachParams = ImmutableMap.of("source", stripeSource.getId()); + customer.getSources().create(attachParams, requestOptions); + } stripeId = stripeSource.getId(); } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); diff --git a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java index 308bb47..e106444 100644 --- a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java +++ b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java @@ -317,6 +317,46 @@ 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 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 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 testDeletePaymentMethod() throws PaymentPluginApiException, StripeException { From 7ba5bd73017c954113704c728f6e9720b39cde00 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 06:43:18 +0530 Subject: [PATCH 02/14] Update StripePaymentPluginApi.java --- .../billing/plugin/stripe/StripePaymentPluginApi.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 2d20868..3b44a44 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -344,7 +344,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripePaymentMethod); if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("payment_method", stripePaymentMethod.getId()); - createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); } else { ImmutableMap attachParams = ImmutableMap.of("customer", existingCustomerId); stripePaymentMethod.attach(attachParams, requestOptions); @@ -359,7 +359,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); - customerId = createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); } else { Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeToken.getId()); @@ -377,7 +377,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeSource); if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); - createStripeCustomer(kbAccountId, existingCustomerId, params, requestOptions, allProperties, context); + createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); } else { Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeSource.getId()); From dae00b1228dcd416df2e368533c2640faeb74626 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:18:36 +0530 Subject: [PATCH 03/14] Incorporated Copilot fixes Incorporated Copilot fixes --- .../plugin/stripe/StripePaymentPluginApi.java | 32 +++++--- .../stripe/TestStripePaymentPluginApi.java | 79 +++++++++++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 3b44a44..7681848 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -341,32 +341,39 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI if ("payment_method".equals(objectType)) { try { final PaymentMethod stripePaymentMethod = PaymentMethod.retrieve(paymentMethodIdInStripe, requestOptions); + final PaymentMethod paymentMethodForAdditionalData; additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripePaymentMethod); if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("payment_method", stripePaymentMethod.getId()); createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + paymentMethodForAdditionalData = stripePaymentMethod; } else { ImmutableMap attachParams = ImmutableMap.of("customer", existingCustomerId); - stripePaymentMethod.attach(attachParams, requestOptions); - } - stripeId = stripePaymentMethod.getId(); + 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); + additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); + + String customerId; if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); } else { Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeToken.getId()); - customer.getSources().create(attachParams, requestOptions); + // Capture the newly created source directly from Stripe + PaymentSource attachedSource = customer.getSources().create(attachParams, requestOptions); customerId = existingCustomerId; + stripeId = attachedSource.getId(); } - stripeId = retrievePaymentMethod(customerId, existingCustomerId, getTokenInnerId(stripeToken), requestOptions); } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } @@ -374,16 +381,23 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI try { // The Stripe sourceId must be passed as the PaymentMethodPlugin#getExternalPaymentMethodId final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions); - additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeSource); + final PaymentSource sourceForAdditionalData; + if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + sourceForAdditionalData = stripeSource; } else { Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeSource.getId()); - customer.getSources().create(attachParams, requestOptions); + // Capture the returned, attached PaymentSource + sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions); } - stripeId = stripeSource.getId(); + + // Build the map AFTER attachment so the local DB gets the customer_id + additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); + stripeId = sourceForAdditionalData.getId(); + } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } diff --git a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java index e106444..8e55618 100644 --- a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java +++ b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java @@ -357,6 +357,85 @@ public void testAddSecondPaymentMethodToExistingCustomer() throws PaymentPluginA 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 paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context); + assertEquals(paymentMethods.size(), 1); + + // 2. Create a raw Token in Stripe (simulating a legacy stripe.js token generation) + final RequestOptions options = stripePaymentPluginApi.buildRequestOptions(context); + final Map cardParams = new HashMap<>(); + cardParams.put("number", "4242424242424242"); + cardParams.put("exp_month", 12); + cardParams.put("exp_year", 2030); + cardParams.put("cvc", "123"); + final Map 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 + 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 + Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, options); + assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); + } + + @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 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 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 + Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, options); + assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); + } @Test(groups = "integration") public void testDeletePaymentMethod() throws PaymentPluginApiException, StripeException { From db54c30ba48aa150133e589e197a0589d2a7f997 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:22:19 +0530 Subject: [PATCH 04/14] Update StripePaymentPluginApi.java --- .../killbill/billing/plugin/stripe/StripePaymentPluginApi.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 7681848..aa1adab 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -359,9 +359,8 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI } else if ("token".equals(objectType)) { try { final Token stripeToken = Token.retrieve(paymentMethodIdInStripe, requestOptions); - additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); + additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); - String customerId; if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); From ba2c377eca849de7c069a0abab9a0668b0a66523 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:29:10 +0530 Subject: [PATCH 05/14] Update StripePaymentPluginApi.java --- .../killbill/billing/plugin/stripe/StripePaymentPluginApi.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index aa1adab..337729a 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -342,7 +342,6 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI try { final PaymentMethod stripePaymentMethod = PaymentMethod.retrieve(paymentMethodIdInStripe, requestOptions); final PaymentMethod paymentMethodForAdditionalData; - additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripePaymentMethod); if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("payment_method", stripePaymentMethod.getId()); createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); From 759d84466093aa75b13353f1c89e87daa845eda3 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:40:13 +0530 Subject: [PATCH 06/14] Update StripePaymentPluginApi.java --- .../billing/plugin/stripe/StripePaymentPluginApi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 337729a..6174cc6 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -365,7 +365,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); } else { - Customer customer = Customer.retrieve(existingCustomerId, requestOptions); + Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeToken.getId()); // Capture the newly created source directly from Stripe PaymentSource attachedSource = customer.getSources().create(attachParams, requestOptions); @@ -386,7 +386,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); sourceForAdditionalData = stripeSource; } else { - Customer customer = Customer.retrieve(existingCustomerId, requestOptions); + Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); ImmutableMap attachParams = ImmutableMap.of("source", stripeSource.getId()); // Capture the returned, attached PaymentSource sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions); From ef2cbcbb390ef658da6e10a337e2e71aae74b556 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:54:05 +0530 Subject: [PATCH 07/14] Update StripePaymentPluginApi.java --- .../plugin/stripe/StripePaymentPluginApi.java | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 6174cc6..e9ef980 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -358,48 +358,45 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI } else if ("token".equals(objectType)) { try { final Token stripeToken = Token.retrieve(paymentMethodIdInStripe, requestOptions); - additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); + additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); if (existingCustomerId == null) { - ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); - customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); + String customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); - } else { - Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); - ImmutableMap attachParams = ImmutableMap.of("source", stripeToken.getId()); - // Capture the newly created source directly from Stripe - PaymentSource attachedSource = customer.getSources().create(attachParams, requestOptions); - customerId = existingCustomerId; - stripeId = attachedSource.getId(); + } else { + ImmutableMap updateParams = ImmutableMap.of("source", stripeToken.getId()); + Customer.update(existingCustomerId, updateParams, requestOptions); + + stripeId = retrievePaymentMethod(existingCustomerId, null, getTokenInnerId(stripeToken), 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); - final PaymentSource sourceForAdditionalData; - - if (existingCustomerId == null) { - ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); - createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); - sourceForAdditionalData = stripeSource; - } else { - Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); - ImmutableMap attachParams = ImmutableMap.of("source", stripeSource.getId()); - // Capture the returned, attached PaymentSource - sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions); - } - - // Build the map AFTER attachment so the local DB gets the customer_id - additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); - stripeId = sourceForAdditionalData.getId(); - - } catch (final StripeException e) { - throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); + try { + final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions); + final PaymentSource sourceForAdditionalData; + + if (existingCustomerId == null) { + ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); + createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + sourceForAdditionalData = stripeSource; + } else { + // FIX: Use Customer.update() to bypass the getSources() NPE entirely + ImmutableMap updateParams = ImmutableMap.of("source", stripeSource.getId()); + Customer.update(existingCustomerId, updateParams, requestOptions); + // Re-retrieve to capture the updated customer_id for the local database + sourceForAdditionalData = Source.retrieve(stripeSource.getId(), requestOptions); } - } else if ("bank_account".equals(objectType)) { + + additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); + stripeId = sourceForAdditionalData.getId(); + + } catch (final StripeException e) { + throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); + } + } else if ("bank_account".equals(objectType)) { try { // The Stripe bankAccountId must be passed as the PaymentMethodPlugin#getExternalPaymentMethodId final PaymentSource paymentSource = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions) From 7983068e8317f6725515ee3214466457f658d31b Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Tue, 14 Apr 2026 12:56:42 +0530 Subject: [PATCH 08/14] Update StripePaymentPluginApi.java --- .../plugin/stripe/StripePaymentPluginApi.java | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index e9ef980..fff42c7 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -361,42 +361,47 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI additionalDataMap = StripePluginProperties.toAdditionalDataMap(stripeToken); if (existingCustomerId == null) { - ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); - String customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); - stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); - } else { + ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); + customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + } else { + // Retrieve the instance first, then call the instance .update() method + Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap updateParams = ImmutableMap.of("source", stripeToken.getId()); - Customer.update(existingCustomerId, updateParams, requestOptions); + customer.update(updateParams, requestOptions); - stripeId = retrievePaymentMethod(existingCustomerId, null, getTokenInnerId(stripeToken), requestOptions); + customerId = existingCustomerId; } + + stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } } else if ("source".equals(objectType)) { - try { - final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions); - final PaymentSource sourceForAdditionalData; - - if (existingCustomerId == null) { - ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); - createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); - sourceForAdditionalData = stripeSource; - } else { - // FIX: Use Customer.update() to bypass the getSources() NPE entirely - ImmutableMap updateParams = ImmutableMap.of("source", stripeSource.getId()); - Customer.update(existingCustomerId, updateParams, requestOptions); - // Re-retrieve to capture the updated customer_id for the local database - sourceForAdditionalData = Source.retrieve(stripeSource.getId(), requestOptions); + try { + final Source stripeSource = Source.retrieve(paymentMethodIdInStripe, requestOptions); + final PaymentSource sourceForAdditionalData; + + if (existingCustomerId == null) { + ImmutableMap params = ImmutableMap.of("source", stripeSource.getId()); + createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + sourceForAdditionalData = stripeSource; + } else { + // Retrieve the instance first, then call the instance .update() method + Customer customer = Customer.retrieve(existingCustomerId, requestOptions); + ImmutableMap updateParams = ImmutableMap.of("source", stripeSource.getId()); + customer.update(updateParams, requestOptions); + + // Re-retrieve to capture the updated customer_id for the local database + sourceForAdditionalData = Source.retrieve(stripeSource.getId(), requestOptions); + } + + additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); + stripeId = sourceForAdditionalData.getId(); + + } catch (final StripeException e) { + throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } - - additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); - stripeId = sourceForAdditionalData.getId(); - - } catch (final StripeException e) { - throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); - } - } else if ("bank_account".equals(objectType)) { + } else if ("bank_account".equals(objectType)) { try { // The Stripe bankAccountId must be passed as the PaymentMethodPlugin#getExternalPaymentMethodId final PaymentSource paymentSource = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions) From 66224273d3b870c195e948a1dbb6e8fb7c98c1e8 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:00:48 +0530 Subject: [PATCH 09/14] Addressing test failures Addressing test failures --- .../billing/plugin/stripe/StripePaymentPluginApi.java | 11 +++++++---- .../plugin/stripe/TestStripePaymentPluginApi.java | 8 ++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index fff42c7..8bb3266 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -386,10 +386,13 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); sourceForAdditionalData = stripeSource; } else { - // Retrieve the instance first, then call the instance .update() method - Customer customer = Customer.retrieve(existingCustomerId, requestOptions); - ImmutableMap updateParams = ImmutableMap.of("source", stripeSource.getId()); - customer.update(updateParams, requestOptions); + // Retrieve the instance first, then call the instance .update() method + final Map expandParams = new HashMap<>(); + expandParams.put("expand", ImmutableList.of("sources")); + final Customer customer = Customer.retrieve(existingCustomerId, expandParams, requestOptions); + final Map attachParams = new HashMap<>(); + attachParams.put("source", stripeSource.getId()); + customer.getSources().create(attachParams, requestOptions); // Re-retrieve to capture the updated customer_id for the local database sourceForAdditionalData = Source.retrieve(stripeSource.getId(), requestOptions); diff --git a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java index 8e55618..ac57432 100644 --- a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java +++ b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java @@ -395,7 +395,9 @@ public void testAddSecondTokenToExistingCustomer() throws PaymentPluginApiExcept 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 - Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, options); + final Map expandParams = new HashMap<>(); + expandParams.put("expand", ImmutableList.of("sources")); + Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options); assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); } @@ -433,7 +435,9 @@ public void testAddSecondSourceToExistingCustomer() throws PaymentPluginApiExcep 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 - Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, options); + final Map expandParams = new HashMap<>(); + expandParams.put("expand", ImmutableList.of("sources")); + Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options); assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); } From 4d64ceebd217724ba78c3380a6e2fe8e4e12f427 Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:24:46 +0530 Subject: [PATCH 10/14] Addressing test issues Addressing test issues --- .../stripe/TestStripePaymentPluginApi.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java index ac57432..807ca97 100644 --- a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java +++ b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java @@ -367,8 +367,8 @@ public void testAddSecondTokenToExistingCustomer() throws PaymentPluginApiExcept final String existingStripeCustomerId = customer.getId(); // Verify we start with exactly 1 payment method - List paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context); - assertEquals(paymentMethods.size(), 1); + List 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); @@ -391,14 +391,20 @@ public void testAddSecondTokenToExistingCustomer() throws PaymentPluginApiExcept context); // 4. Verify local Kill Bill database now correctly reflects 2 payment methods - paymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context); + List 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 expandParams = new HashMap<>(); expandParams.put("expand", ImmutableList.of("sources")); Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options); - assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); + 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 finalPaymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context); + final String secondMethodStripeId = finalPaymentMethods.stream() + .filter(pm -> !pm.getPaymentMethodId().equals(initialPaymentMethods.get(0).getPaymentMethodId())) + .findFirst().get().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") @@ -438,7 +444,9 @@ public void testAddSecondSourceToExistingCustomer() throws PaymentPluginApiExcep final Map expandParams = new HashMap<>(); expandParams.put("expand", ImmutableList.of("sources")); Customer retrievedCustomer = Customer.retrieve(existingStripeCustomerId, expandParams, options); - assertEquals(retrievedCustomer.getSources().getData().size(), 2, "The Stripe Customer should have exactly 2 sources attached on the backend!"); + 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 + assertEquals(retrievedCustomer.getSources().getData().get(0).getId(), stripeSource.getId(), "The attached source ID should match the one we passed in."); } @Test(groups = "integration") From 1d1319e2ec41b14532ce31018f9f7622a7110e6f Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:54:46 +0530 Subject: [PATCH 11/14] Incorporated Copilot's suggestions Incorporated Copilot's suggestions --- .../plugin/stripe/StripePaymentPluginApi.java | 24 ++++++++++--------- .../stripe/TestStripePaymentPluginApi.java | 4 +++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 8bb3266..8c24185 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -364,16 +364,21 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); } else { - // Retrieve the instance first, then call the instance .update() method Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap updateParams = ImmutableMap.of("source", stripeToken.getId()); - customer.update(updateParams, requestOptions); - + final Customer updatedCustomer = customer.update(updateParams, requestOptions); + // Use the update response directly — defaultSource is already populated, + // no need for an extra Customer.retrieve() round-trip + stripeId = updatedCustomer.getDefaultSource() != null + ? updatedCustomer.getDefaultSource() + : getTokenInnerId(stripeToken); customerId = existingCustomerId; } - - stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); - } catch (final StripeException e) { + // stripeId is now assigned inside the else branch above, so only call this for the new-customer path + if (existingCustomerId == null) { + stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); + } + } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } } else if ("source".equals(objectType)) { @@ -391,11 +396,8 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI expandParams.put("expand", ImmutableList.of("sources")); final Customer customer = Customer.retrieve(existingCustomerId, expandParams, requestOptions); final Map attachParams = new HashMap<>(); - attachParams.put("source", stripeSource.getId()); - customer.getSources().create(attachParams, requestOptions); - - // Re-retrieve to capture the updated customer_id for the local database - sourceForAdditionalData = Source.retrieve(stripeSource.getId(), requestOptions); + attachParams.put("source", stripeSource.getId()); + sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions); } additionalDataMap = StripePluginProperties.toAdditionalDataMap(sourceForAdditionalData); diff --git a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java index 807ca97..f1c5369 100644 --- a/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java +++ b/src/test/java/org/killbill/billing/plugin/stripe/TestStripePaymentPluginApi.java @@ -403,7 +403,9 @@ public void testAddSecondTokenToExistingCustomer() throws PaymentPluginApiExcept final List finalPaymentMethods = stripePaymentPluginApi.getPaymentMethods(kbAccountId, false, ImmutableList.of(), context); final String secondMethodStripeId = finalPaymentMethods.stream() .filter(pm -> !pm.getPaymentMethodId().equals(initialPaymentMethods.get(0).getPaymentMethodId())) - .findFirst().get().getExternalPaymentMethodId(); + .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."); } From 32080fd635a735fedffb9562b3a2503a8ebf9fee Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:02:05 +0530 Subject: [PATCH 12/14] Update StripePaymentPluginApi.java --- .../billing/plugin/stripe/StripePaymentPluginApi.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 8c24185..8896aa8 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -363,6 +363,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI if (existingCustomerId == null) { ImmutableMap params = ImmutableMap.of("source", stripeToken.getId()); customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); + stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); } else { Customer customer = Customer.retrieve(existingCustomerId, requestOptions); ImmutableMap updateParams = ImmutableMap.of("source", stripeToken.getId()); @@ -373,11 +374,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI ? updatedCustomer.getDefaultSource() : getTokenInnerId(stripeToken); customerId = existingCustomerId; - } - // stripeId is now assigned inside the else branch above, so only call this for the new-customer path - if (existingCustomerId == null) { - stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); - } + } } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); } From 37dd2e2b559038d974e0c4002aa74c5643d711ab Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:16:52 +0530 Subject: [PATCH 13/14] Update StripePaymentPluginApi.java --- .../killbill/billing/plugin/stripe/StripePaymentPluginApi.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index 8896aa8..a768e17 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -372,8 +372,7 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI // no need for an extra Customer.retrieve() round-trip stripeId = updatedCustomer.getDefaultSource() != null ? updatedCustomer.getDefaultSource() - : getTokenInnerId(stripeToken); - customerId = existingCustomerId; + : getTokenInnerId(stripeToken); } } catch (final StripeException e) { throw new PaymentPluginApiException("Error calling Stripe while adding payment method", e); From b3dc3aff35750f668c2225ff4e5e9b62affd41eb Mon Sep 17 00:00:00 2001 From: KBbitsP <75751774+KBbitsP@users.noreply.github.com> Date: Thu, 16 Apr 2026 08:44:26 +0530 Subject: [PATCH 14/14] addressed copilot comments addressed copilot comments --- .../plugin/stripe/StripePaymentPluginApi.java | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java index a768e17..d6c7777 100644 --- a/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java +++ b/src/main/java/org/killbill/billing/plugin/stripe/StripePaymentPluginApi.java @@ -365,16 +365,19 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI customerId = createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); stripeId = retrievePaymentMethod(customerId, null, getTokenInnerId(stripeToken), requestOptions); } else { - Customer customer = Customer.retrieve(existingCustomerId, requestOptions); - ImmutableMap updateParams = ImmutableMap.of("source", stripeToken.getId()); - final Customer updatedCustomer = customer.update(updateParams, requestOptions); - // Use the update response directly — defaultSource is already populated, - // no need for an extra Customer.retrieve() round-trip - stripeId = updatedCustomer.getDefaultSource() != null - ? updatedCustomer.getDefaultSource() - : getTokenInnerId(stripeToken); - } - } catch (final StripeException e) { + final Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); + final Map attachParams = new HashMap<>(); + attachParams.put("source", stripeToken.getId()); + final PaymentSource attachedSource = customer.getSources().create(attachParams, requestOptions); + stripeId = attachedSource.getId(); + + if (setDefault) { + final Map 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)) { @@ -387,12 +390,9 @@ public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodI createStripeCustomer(kbAccountId, null, params, requestOptions, allProperties, context); sourceForAdditionalData = stripeSource; } else { - // Retrieve the instance first, then call the instance .update() method - final Map expandParams = new HashMap<>(); - expandParams.put("expand", ImmutableList.of("sources")); - final Customer customer = Customer.retrieve(existingCustomerId, expandParams, requestOptions); + final Customer customer = Customer.retrieve(existingCustomerId, expandSourcesParams, requestOptions); final Map attachParams = new HashMap<>(); - attachParams.put("source", stripeSource.getId()); + attachParams.put("source", stripeSource.getId()); sourceForAdditionalData = customer.getSources().create(attachParams, requestOptions); }