Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

473 - Refactor and change out the donation flow to use Stripe.SetupIntents #479

Conversation

dimitur2204
Copy link
Contributor

@dimitur2204 dimitur2204 commented Mar 23, 2023

Closes #428 #466

Motivation and context

Changed out the donation flow to now use the Stripe.SetupIntent in order to solve a problem that occured with the PaymentIntent and recurring donations, now when we do not use the CheckoutSession.

This is an overview of the new flow in a simplified version (recurring donations will be part of a future PR):
image

Changes

Added a new stripe.module that is concerned with all functionality regarding communication with stripe and using the stripe client. Provides better separation of concerns.

Changed out how the donations are created. With the new implementation they are created directly in the Donation.succeeded state after the PaymentIntent was confirmed successfully and a charge was made, but this is subject to change if needed. The creation of the Donations is now reliant on the metadata provided on the Setup/Payment Intent.

Changed out some methods into services that made more sense for them to be in.

Removed old checkout session implementation.

Testing

I have only adjusted current tests and removed old ones, that were testing parts that do not exist now.

New endpoints

POST /setup-intent - Creates a new SetupIntent
POST /setup-intent/:id - Updates SetupIntent
POST /setup-intent/:id/finalize - Finalizes SetupIntent by creating a Customer, attaching a PaymentMethod and returning a confirmed PaymentIntent

All endpoints concerning communication with Stripe have been moved to the stripe.controller

Potential problems

There might be some confusion between the StripeModule that comes from golevelup and our new Stripe module.

The donations being instantiated in status Donation.succeeded means that failed and possibly donations that might take a long time to process for some reason might be problematic. However, all the tests I did with many different Stripe test cards are passing!

dimitur2204 and others added 25 commits March 23, 2023 15:10
* added error logging to stripe client

* added type for stripe config

* removed unneded import

---------

Co-authored-by: quantum-grit <quantum-grit@gmail.com>
* added: explicit version for stripe api client

* added: maxNetworkRetries 2 to stripe client

* added: full stripe error log

---------

Co-authored-by: quantum-grit <quantum-grit@gmail.com>
Co-authored-by: quantum-grit <quantum-grit@gmail.com>
@github-actions
Copy link

github-actions bot commented Mar 23, 2023

✅ Tests will run for this PR. Once they succeed it can be merged.

@dimitur2204 dimitur2204 self-assigned this Mar 24, 2023
@dimitur2204 dimitur2204 marked this pull request as ready for review March 24, 2023 10:06
@dimitur2204 dimitur2204 added the run tests Allows running the tests workflows for forked repos label Mar 24, 2023
@github-actions github-actions bot removed the run tests Allows running the tests workflows for forked repos label Mar 24, 2023
@dimitur2204 dimitur2204 changed the base branch from master to 480-epic-new-donation-flow-recurring-donations-support March 24, 2023 10:19
@dimitur2204 dimitur2204 added the run tests Allows running the tests workflows for forked repos label Mar 24, 2023
@github-actions github-actions bot removed the run tests Allows running the tests workflows for forked repos label Mar 24, 2023
@dimitur2204 dimitur2204 requested a review from kachar March 24, 2023 10:33
@kachar
Copy link
Member

kachar commented Apr 3, 2023

@dimitur2204 @igoychev The PR looks awesome to me. Can't wait to test it on staging

@igoychev
Copy link
Contributor

igoychev commented Apr 3, 2023

@dimitur2204 @igoychev The PR looks awesome to me. Can't wait to test it on staging

Yep, we are just pushing out the Expense Reports and Bank Imports and this one will come next!

Copy link
Contributor

@igoychev igoychev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great improvement and also puts things in good order!

Please see a few comments that need adjustments and we are good to go.

Comment on lines +13 to +24
imports: [ConfigModule, HttpModule],
providers: [
PaypalService,
{
provide: CampaignService,
useValue: {},
},
{
provide: DonationsService,
useValue: {},
},
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DonationsService depends on StripeClient and including it here makes Paypal depend on Stripe too, which is not a good practice.
It looks like if we move createSubscriptionDonation from DonationsService to the StripeService will resolve the situation.

Also wen resolving the dependencies, it will be better to add DonationsModule in the imports[] section, so that there is no need to include its services in the providers section.

Comment on lines +9 to +13
imports: [
StripeClientModule.forRootAsync(StripeClientModule, {
inject: [ConfigService],
useFactory: StripeConfigFactory.useFactory,
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you might need to add the ConfigModule in imports[] section, to indicate who is providing the ConfigService

customer: customer.id,
})
const paymentIntent = await this.stripeClient.paymentIntents.create({
amount: Math.round(Number(setupIntent.metadata.amount)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you share what is the need of the round here? asking if it could create rounding errors with the amount

import { DeepMockProxy } from 'jest-mock-extended/lib/mjs/Mock'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice side improvement!

Comment on lines -63 to -70
@StripeWebhookHandler('payment_intent.canceled')
async handlePaymentIntentCancelled(event: Stripe.Event) {
const paymentIntent: Stripe.PaymentIntent = event.data.object as Stripe.PaymentIntent
Logger.log(
'[ handlePaymentIntentCancelled ]',
paymentIntent,
paymentIntent.metadata as DonationMetadata,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need of handling payment_intent.canceled was for recording if the person didn't complete the payment after his initial desire to donate.
Could you share how we are recording that information after this handler is removed?

Comment on lines 304 to +305
const mockedupdateDonationPayment = jest
.spyOn(campaignService, 'updateDonationPayment')
.spyOn(donationService, 'createDonation')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[consistency] mockedupdateDonationPayment needs to become mockedcreateDonation

Comment on lines 349 to +352
const mockedupdateDonationPayment = jest
.spyOn(campaignService, 'updateDonationPayment')
.spyOn(donationService, 'createDonation')
.mockImplementation(() => Promise.resolve(''))
.mockName('updateDonationPayment')
.mockName('createDonation')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is for cancelling the subscription, yet it seems to expect a createDonation function to be called?

Comment on lines +140 to +143
const product = await this.stripeClient.products.create({
name: `Donation of ${subscriptionPaymentDto.amount}`,
description: `Donation of ${subscriptionPaymentDto.amount} to campaign ${subscriptionPaymentDto.campaignId} by person ${person.email}`,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per Stripe documentation the product should be the name of the campaign - see 3rd bullet point here: https://stripe.com/docs/products-prices/how-products-and-prices-work#what-is-a-product

In that regard the product name and description should contain only campaign related information and should not include the price or person details, so that we don't create too many products for each client and each price. Also if easy, we could also pass the campaign url as parameter to the create product function.

Comment on lines +163 to +168
const invoice = await this.stripeClient.invoices.retrieve(
subscription.latest_invoice as string,
{
expand: ['payment_intent'],
},
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just noticed that we can skip the invoice.retrieve request if we pass pass the "expand: ['latest_invoice.payment_intent']," to the subscription.create function so that it is returned there

@igoychev
Copy link
Contributor

igoychev commented Mar 4, 2024

closing without merge, as the code changed quite a lot after this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve data integrity on the extCustomerId
4 participants