Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions apps/api/src/donations/donations.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,7 @@ export class DonationsService {
campaign: Campaign,
paymentData: PaymentData,
newDonationStatus: PaymentStatus,
): Promise<string | undefined> {
): Promise<{ id: string; status: PaymentStatus } | undefined> {
const campaignId = campaign.id
Logger.debug('Update donation to status: ' + newDonationStatus, {
campaignId,
Expand All @@ -806,7 +806,6 @@ export class DonationsService {
//also increments the vault amount and marks campaign as completed
//if target amount is reached
return await this.prisma.$transaction(async (tx) => {
let donationId
// Find donation by extPaymentIntentId
const existingDonation = await this.findExistingDonation(tx, paymentData)

Expand All @@ -818,7 +817,7 @@ export class DonationsService {
newDonationStatus,
campaign,
)
donationId = newDonation.id
return { id: newDonation.id, status: newDonationStatus }
}
//donation exists, so check if it is safe to update it
else {
Expand All @@ -828,10 +827,10 @@ export class DonationsService {
newDonationStatus,
paymentData,
)
donationId = updatedDonation?.id
if (updatedDonation) {
return { id: updatedDonation.id, status: newDonationStatus }
}
}

return donationId
}) //end of the transaction scope
}

Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/stripe/events/stripe-payment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export class StripePaymentService {

const billingData = getPaymentDataFromCharge(charge)

const donationId = await this.donationService.updateDonationPayment(
const result = await this.donationService.updateDonationPayment(
campaign,
billingData,
PaymentStatus.succeeded,
Expand All @@ -147,8 +147,8 @@ export class StripePaymentService {
await this.cancelSubscriptionsIfCompletedCampaign(metadata.campaignId)

//and finally save the donation wish
if (donationId && metadata?.wish) {
await this.campaignService.createDonationWish(metadata.wish, donationId, campaign.id)
if (result?.id && metadata?.wish) {
await this.campaignService.createDonationWish(metadata.wish, result.id, campaign.id)
}
}

Expand Down
15 changes: 14 additions & 1 deletion apps/api/src/stripe/stripe.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
ConvertSingleSubscriptionCurrencyDto,
} from './dto/currency-conversion.dto'
import { CreateSessionDto } from '../donations/dto/create-session.dto'
import { forwardRef, NotAcceptableException } from '@nestjs/common'
import { BadRequestException, forwardRef, NotAcceptableException } from '@nestjs/common'
import { PersonService } from '../person/person.service'
import { CampaignService } from '../campaign/campaign.service'
import { MarketingNotificationsService } from '../notifications/notifications.service'
Expand Down Expand Up @@ -215,6 +215,8 @@ describe('StripeController', () => {
})

it('should request refund for donation', async () => {
apiMock.createRefund.mockResolvedValue({ status: 'succeeded' } as any)

await controller.refundStripePaymet('unique-intent')

expect(apiMock.retrievePaymentIntent).toHaveBeenCalledWith('unique-intent')
Expand All @@ -223,6 +225,17 @@ describe('StripeController', () => {
reason: 'requested_by_customer',
})
})

it('should throw error when refund fails', async () => {
apiMock.createRefund.mockResolvedValue({
status: 'failed',
failure_reason: 'expired_or_canceled_card',
} as any)

await expect(controller.refundStripePaymet('unique-intent')).rejects.toThrow(
BadRequestException,
)
})
it(`should not call setupintents.update if no campaignId is provided`, async () => {
prismaMock.campaign.findFirst.mockResolvedValue({
id: 'complete-campaign',
Expand Down
25 changes: 21 additions & 4 deletions apps/api/src/stripe/stripe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import {
Currency,
DonationMetadata,
Payment,
PaymentStatus,
RecurringDonationStatus,
} from '@prisma/client'
import { ConfigService } from '@nestjs/config'
import { InvoiceWithPayments, StripeMetadata } from './stripe-metadata.interface'
import { getPaymentDataFromCharge } from '../donations/helpers/payment-intent-helpers'
import { CreateStripePaymentDto } from '../donations/dto/create-stripe-payment.dto'
import { RecurringDonationService } from '../recurring-donation/recurring-donation.service'
import * as crypto from 'crypto'
Expand Down Expand Up @@ -475,19 +477,34 @@ export class StripeService {
/**
* Refund a stipe payment donation
* https://stripe.com/docs/api/refunds/create
* @param inputDto Refund-stripe params
* @returns {Promise<Stripe.Response<Stripe.Refund>>}
* @param paymentIntentId Stripe payment intent id
*/
async refundStripePayment(paymentIntentId: string): Promise<Stripe.Response<Stripe.Refund>> {
async refundStripePayment(paymentIntentId: string): Promise<{ id: string; status: PaymentStatus } | undefined> {
const intent = await this.api.retrievePaymentIntent(paymentIntentId)
if (!intent) {
throw new BadRequestException('Payment Intent is missing from stripe')
}

return await this.api.createRefund({
const refund = await this.api.createRefund({
payment_intent: paymentIntentId,
reason: 'requested_by_customer',
})

if (refund.status !== 'succeeded') {
throw new BadRequestException(`Refund failed with status: ${refund.status}. Reason: ${refund.failure_reason}`)
}

const donation = await this.donationService.getDonationByPaymentIntent(paymentIntentId)
const campaign = donation?.targetVault?.campaign

if (campaign) {
const charge = intent.latest_charge as Stripe.Charge | string
const chargeObj =
typeof charge === 'string' ? await this.api.retrieveCharge(charge) : charge

const billingData = getPaymentDataFromCharge(chargeObj)
return await this.donationService.updateDonationPayment(campaign, billingData, PaymentStatus.refund)
}
}

async cancelSubscription(stripeSubscriptionId: string): Promise<Stripe.Subscription> {
Expand Down
Loading