feat(medusa, stripe, paypal): Add update payment session support to the abstract payment processor API (#4442)

* feat(medusa, stripe, paypal): Add update payment session support to the abstract payment processor API

* continue to add updatePaymentData

* feat: Add update payment data to paypal as well and add stripe tests

* fix stripe updatePaymentData returned object type

* fix tests

* Create three-ravens-raise.md

* fix paypal updatePayment data impl

---------

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2023-07-10 15:59:19 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl
parent 1903217705
commit 4264302f21
9 changed files with 345 additions and 138 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/medusa": patch
"medusa-payment-paypal": patch
"medusa-payment-stripe": patch
---
feat(medusa, stripe, paypal): Add update payment session support to the abstract payment processor API
@@ -17,6 +17,7 @@ import { humanizeAmount } from "medusa-core-utils"
import { roundToTwo } from "./utils/utils" import { roundToTwo } from "./utils/utils"
import { CreateOrder, PaypalSdk } from "../core" import { CreateOrder, PaypalSdk } from "../core"
import { Logger } from "@medusajs/types" import { Logger } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
class PayPalProviderService extends AbstractPaymentProcessor { class PayPalProviderService extends AbstractPaymentProcessor {
static identifier = "paypal" static identifier = "paypal"
@@ -280,6 +281,23 @@ class PayPalProviderService extends AbstractPaymentProcessor {
} }
} }
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (data.amount) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return data
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", e)
}
}
async retrieveOrderFromAuth(authorization) { async retrieveOrderFromAuth(authorization) {
const link = authorization.links.find((l) => l.rel === "up") const link = authorization.links.find((l) => l.rel === "up")
const parts = link.href.split("/") const parts = link.href.split("/")
@@ -2,6 +2,7 @@ import {
EXISTING_CUSTOMER_EMAIL, EXISTING_CUSTOMER_EMAIL,
FAIL_INTENT_ID, FAIL_INTENT_ID,
PARTIALLY_FAIL_INTENT_ID, PARTIALLY_FAIL_INTENT_ID,
STRIPE_ID,
WRONG_CUSTOMER_EMAIL, WRONG_CUSTOMER_EMAIL,
} from "../../__mocks__/stripe" } from "../../__mocks__/stripe"
import { PaymentIntentDataByStatus } from "../../__fixtures__/data" import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
@@ -212,3 +213,13 @@ export const updatePaymentContextFailWithDifferentAmount = {
amount: 1000, amount: 1000,
}, },
} }
export const updatePaymentDataWithAmountData = {
sessionId: STRIPE_ID,
amount: 2000,
}
export const updatePaymentDataWithoutAmountData = {
sessionId: STRIPE_ID,
customProp: "test",
}
@@ -26,6 +26,8 @@ import {
updatePaymentContextWithExistingCustomer, updatePaymentContextWithExistingCustomer,
updatePaymentContextWithExistingCustomerStripeId, updatePaymentContextWithExistingCustomerStripeId,
updatePaymentContextWithWrongEmail, updatePaymentContextWithWrongEmail,
updatePaymentDataWithAmountData,
updatePaymentDataWithoutAmountData,
} from "../__fixtures__/data" } from "../__fixtures__/data"
import { import {
PARTIALLY_FAIL_INTENT_ID, PARTIALLY_FAIL_INTENT_ID,
@@ -571,4 +573,53 @@ describe("StripeTest", () => {
}) })
}) })
}) })
describe("updatePaymentData", function () {
let stripeTest
beforeAll(async () => {
const scopedContainer = { ...container }
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed to update the payment data", async () => {
const result = await stripeTest.updatePaymentData(
updatePaymentDataWithoutAmountData.sessionId,
{ ...updatePaymentDataWithoutAmountData, sessionId: undefined }
)
expect(StripeMock.paymentIntents.update).toHaveBeenCalled()
expect(StripeMock.paymentIntents.update).toHaveBeenCalledWith(
updatePaymentDataWithoutAmountData.sessionId,
{
customProp: updatePaymentDataWithoutAmountData.customProp,
}
)
expect(result).toEqual(
expect.objectContaining({
customProp: updatePaymentDataWithoutAmountData.customProp,
})
)
})
it("should fail to update the payment data if the amount is present", async () => {
const result = await stripeTest.updatePaymentData(
updatePaymentDataWithAmountData.sessionId,
{ ...updatePaymentDataWithAmountData, sessionId: undefined }
)
expect(StripeMock.paymentIntents.update).not.toHaveBeenCalled()
expect(result).toEqual({
error: "An error occurred in updatePaymentData",
code: undefined,
detail: "Cannot update amount, use updatePayment instead",
})
})
})
}) })
@@ -14,6 +14,7 @@ import {
PaymentIntentOptions, PaymentIntentOptions,
StripeOptions, StripeOptions,
} from "../types" } from "../types"
import { MedusaError } from "@medusajs/utils"
abstract class StripeBase extends AbstractPaymentProcessor { abstract class StripeBase extends AbstractPaymentProcessor {
static identifier = "" static identifier = ""
@@ -281,6 +282,25 @@ abstract class StripeBase extends AbstractPaymentProcessor {
} }
} }
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (data.amount) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return (await this.stripe_.paymentIntents.update(sessionId, {
...data,
})) as unknown as PaymentProcessorSessionResponse["session_data"]
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", e)
}
}
/** /**
* Constructs Stripe Webhook event * Constructs Stripe Webhook event
* @param {object} data - the data of the webhook request: req.body * @param {object} data - the data of the webhook request: req.body
@@ -119,6 +119,16 @@ export interface PaymentProcessor {
getPaymentStatus( getPaymentStatus(
paymentSessionData: Record<string, unknown> paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> ): Promise<PaymentSessionStatus>
/**
* Update the session data for a payment session
*/
updatePaymentData(
sessionId: string,
data: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
>
} }
/** /**
@@ -195,6 +205,13 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
abstract updatePayment( abstract updatePayment(
context: PaymentProcessorContext context: PaymentProcessorContext
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void> ): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void>
abstract updatePaymentData(
sessionId: string,
data: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
>
} }
/** /**
@@ -1,35 +1,44 @@
import { asClass, asFunction, asValue, createContainer } from "awilix" import { asClass, asFunction, asValue, createContainer } from "awilix"
import { MockManager, MockRepository } from "medusa-test-utils" import { MockManager, MockRepository } from "medusa-test-utils"
import PaymentProviderService from "../payment-provider"; import PaymentProviderService from "../payment-provider"
import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"; import { PaymentProviderServiceMock } from "../__mocks__/payment-provider"
import { CustomerServiceMock } from "../__mocks__/customer"; import { CustomerServiceMock } from "../__mocks__/customer"
import { FlagRouter } from "../../utils/flag-router"; import { FlagRouter } from "../../utils/flag-router"
import Logger from "../../loaders/logger"; import Logger from "../../loaders/logger"
import { import {
AbstractPaymentProcessor, AbstractPaymentProcessor,
PaymentProcessorContext, PaymentProcessorContext,
PaymentProcessorError, PaymentProcessorError,
PaymentProcessorSessionResponse PaymentProcessorSessionResponse,
} from "../../interfaces"; } from "../../interfaces"
import { PaymentSessionStatus } from "../../models"; import { PaymentSessionStatus } from "../../models"
import { PaymentServiceMock } from "../__mocks__/payment"; import { PaymentServiceMock } from "../__mocks__/payment"
export const defaultContainer = createContainer() export const defaultContainer = createContainer()
defaultContainer.register("paymentProviderService", asClass(PaymentProviderService)) defaultContainer.register(
"paymentProviderService",
asClass(PaymentProviderService)
)
defaultContainer.register("paymentService", asValue(PaymentServiceMock)) defaultContainer.register("paymentService", asValue(PaymentServiceMock))
defaultContainer.register("manager", asValue(MockManager)) defaultContainer.register("manager", asValue(MockManager))
defaultContainer.register("paymentSessionRepository", asValue(MockRepository())) defaultContainer.register("paymentSessionRepository", asValue(MockRepository()))
defaultContainer.register("paymentProviderRepository", asValue(PaymentProviderServiceMock)) defaultContainer.register(
"paymentProviderRepository",
asValue(PaymentProviderServiceMock)
)
defaultContainer.register("paymentRepository", asValue(MockRepository())) defaultContainer.register("paymentRepository", asValue(MockRepository()))
defaultContainer.register("refundRepository", asValue(MockRepository())) defaultContainer.register("refundRepository", asValue(MockRepository()))
defaultContainer.register("customerService", asValue(CustomerServiceMock)) defaultContainer.register("customerService", asValue(CustomerServiceMock))
defaultContainer.register("featureFlagRouter", asValue(new FlagRouter({}))) defaultContainer.register("featureFlagRouter", asValue(new FlagRouter({})))
defaultContainer.register("logger", asValue(Logger)) defaultContainer.register("logger", asValue(Logger))
defaultContainer.register("pp_payment_processor", asFunction((cradle) => new PaymentProcessor(cradle))) defaultContainer.register(
"pp_payment_processor",
asFunction((cradle) => new PaymentProcessor(cradle))
)
export class PaymentProcessor extends AbstractPaymentProcessor { export class PaymentProcessor extends AbstractPaymentProcessor {
constructor(container) { constructor(container) {
super(container); super(container)
} }
authorizePayment(context: PaymentProcessorContext): Promise< authorizePayment(context: PaymentProcessorContext): Promise<
| PaymentProcessorError | PaymentProcessorError
@@ -38,43 +47,78 @@ export class PaymentProcessor extends AbstractPaymentProcessor {
data: PaymentProcessorSessionResponse["session_data"] data: PaymentProcessorSessionResponse["session_data"]
} }
> { > {
return Promise.resolve({ } as any); return Promise.resolve({} as any)
} }
getPaymentStatus(paymentSessionData: Record<string, unknown>): Promise<PaymentSessionStatus> { getPaymentStatus(
return Promise.resolve(PaymentSessionStatus.PENDING); paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
return Promise.resolve(PaymentSessionStatus.PENDING)
} }
init(): Promise<void> { init(): Promise<void> {
return Promise.resolve(undefined); return Promise.resolve(undefined)
} }
initiatePayment(context: PaymentProcessorContext): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> { initiatePayment(
return Promise.resolve({ } as PaymentProcessorSessionResponse); context: PaymentProcessorContext
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
return Promise.resolve({} as PaymentProcessorSessionResponse)
} }
retrievePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> { retrievePayment(
return Promise.resolve({ }); paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
} }
updatePayment(context: PaymentProcessorContext): Promise<PaymentProcessorError | void> { updatePayment(
return Promise.resolve(undefined); context: PaymentProcessorContext
): Promise<PaymentProcessorError | void> {
return Promise.resolve(undefined)
} }
capturePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> { capturePayment(
return Promise.resolve({ }); paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
} }
refundPayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> { refundPayment(
return Promise.resolve({}); paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
} }
cancelPayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> { cancelPayment(
return Promise.resolve({}); paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
} }
deletePayment(paymentSessionData: Record<string, unknown>): Promise<PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]> { deletePayment(
return Promise.resolve({}); paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
}
updatePaymentData(
sessionId: string,
data: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return Promise.resolve({})
} }
} }
@@ -1,6 +1,5 @@
import { asValue, createContainer } from "awilix" import { asValue, createContainer } from "awilix"
import { MockRepository } from "medusa-test-utils" import { MockRepository } from "medusa-test-utils"
import PaymentProviderService from "../payment-provider"
import { import {
defaultContainer, defaultContainer,
defaultPaymentSessionInputData, defaultPaymentSessionInputData,
@@ -8,7 +7,7 @@ import {
} from "../__fixtures__/payment-provider" } from "../__fixtures__/payment-provider"
import { testPayServiceMock } from "../__mocks__/test-pay" import { testPayServiceMock } from "../__mocks__/test-pay"
import { EOL } from "os" import { EOL } from "os"
import { PaymentSessionStatus, RefundReason } from "../../models"; import { PaymentSessionStatus, RefundReason } from "../../models"
describe(`PaymentProviderService`, () => { describe(`PaymentProviderService`, () => {
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
@@ -144,8 +143,8 @@ describe(`PaymentProviderService`, () => {
amount: 100, amount: 100,
currency_code: "usd", currency_code: "usd",
cart: { cart: {
id: "cart-test" id: "cart-test",
} },
} }
) )
@@ -564,17 +563,21 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
provider_id: defaultPaymentSessionInputData.provider_id, provider_id: defaultPaymentSessionInputData.provider_id,
payment_session: { payment_session: {
id: sessionId, id: sessionId,
data: { id: externalId } data: { id: externalId },
} },
} }
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.retrievePayment = jest.fn().mockReturnValue(Promise.resolve({})) mockPaymentProcessor.retrievePayment = jest
.fn()
.mockReturnValue(Promise.resolve({}))
container container.register(
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) paymentProcessorResolutionKey,
asValue(mockPaymentProcessor)
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
@@ -584,7 +587,9 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const provider = container.resolve(paymentProcessorResolutionKey) const provider = container.resolve(paymentProcessorResolutionKey)
expect(provider.retrievePayment).toBeCalledTimes(1) expect(provider.retrievePayment).toBeCalledTimes(1)
expect(provider.retrievePayment).toBeCalledWith(paymentInput.payment_session.data) expect(provider.retrievePayment).toBeCalledWith(
paymentInput.payment_session.data
)
}) })
it("throw an error using the provider error response", async () => { it("throw an error using the provider error response", async () => {
@@ -621,27 +626,27 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const paymentSession = { const paymentSession = {
id: "test-session", id: "test-session",
data: { id: externalId }, data: { id: externalId },
provider_id: paymentProviderId provider_id: paymentProviderId,
} }
const context = { ip: "0.0.0.0" } const context = { ip: "0.0.0.0" }
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.authorizePayment = jest.fn().mockReturnValue(Promise.resolve({})) mockPaymentProcessor.authorizePayment = jest
.fn()
.mockReturnValue(Promise.resolve({}))
container container
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) .register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor))
.register( .register(
"paymentSessionRepository", "paymentSessionRepository",
asValue( asValue(
MockRepository({ MockRepository({
findOne: jest findOne: jest.fn().mockImplementation(async () => ({ data: {} })),
.fn() })
.mockImplementation(async () => ({ data: {} })),
})
)
) )
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
@@ -651,7 +656,10 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const provider = container.resolve(paymentProcessorResolutionKey) const provider = container.resolve(paymentProcessorResolutionKey)
expect(provider.authorizePayment).toBeCalledTimes(1) expect(provider.authorizePayment).toBeCalledTimes(1)
expect(provider.authorizePayment).toBeCalledWith(paymentSession.data, context) expect(provider.authorizePayment).toBeCalledWith(
paymentSession.data,
context
)
}) })
it("throw an error using the provider error response", async () => { it("throw an error using the provider error response", async () => {
@@ -688,26 +696,26 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const payment = { const payment = {
id: "payment-id", id: "payment-id",
data: { id: externalId }, data: { id: externalId },
provider_id: paymentProviderId provider_id: paymentProviderId,
} }
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.cancelPayment = jest.fn().mockReturnValue(Promise.resolve()) mockPaymentProcessor.cancelPayment = jest
.fn()
.mockReturnValue(Promise.resolve())
container container
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) .register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor))
.register( .register(
"paymentRepository", "paymentRepository",
asValue( asValue(
MockRepository({ MockRepository({
findOne: jest findOne: jest.fn().mockImplementation(async () => payment),
.fn() })
.mockImplementation(async () => payment),
})
)
) )
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
@@ -739,9 +747,7 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
const err = await providerService const err = await providerService.cancelPayment(payment).catch((e) => e)
.cancelPayment(payment)
.catch((e) => e)
expect(err.message).toBe( expect(err.message).toBe(
`${errResponse.error}:${EOL}${errResponse.detail}` `${errResponse.error}:${EOL}${errResponse.detail}`
@@ -752,7 +758,7 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
describe("getStatus", () => { describe("getStatus", () => {
const payment = { const payment = {
data: { id: "id" }, data: { id: "id" },
provider_id: paymentProviderId provider_id: paymentProviderId,
} }
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
@@ -765,15 +771,13 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
container container
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) .register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor))
.register( .register(
"paymentRepository", "paymentRepository",
asValue( asValue(
MockRepository({ MockRepository({
findOne: jest findOne: jest.fn().mockImplementation(async () => payment),
.fn() })
.mockImplementation(async () => payment),
})
)
) )
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
@@ -792,26 +796,26 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const payment = { const payment = {
data: { id: externalId }, data: { id: externalId },
id: "payment-id", id: "payment-id",
provider_id: paymentProviderId provider_id: paymentProviderId,
} }
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.capturePayment = jest.fn().mockReturnValue(Promise.resolve(payment.data)) mockPaymentProcessor.capturePayment = jest
.fn()
.mockReturnValue(Promise.resolve(payment.data))
container container
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) .register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor))
.register( .register(
"paymentRepository", "paymentRepository",
asValue( asValue(
MockRepository({ MockRepository({
findOne: jest findOne: jest.fn().mockImplementation(async () => payment),
.fn() })
.mockImplementation(async () => payment),
})
)
) )
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
@@ -843,9 +847,7 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
const err = await providerService const err = await providerService.capturePayment(payment).catch((e) => e)
.capturePayment(payment)
.catch((e) => e)
expect(err.message).toBe( expect(err.message).toBe(
`${errResponse.error}:${EOL}${errResponse.detail}` `${errResponse.error}:${EOL}${errResponse.detail}`
@@ -858,62 +860,85 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
const payments = [{ const payments = [
id: "p1", {
captured_at: new Date(), id: "p1",
data: { id: "id1" }, captured_at: new Date(),
amount: 1000, data: { id: "id1" },
amount_refunded: 0, amount: 1000,
provider_id: paymentProviderId, amount_refunded: 0,
}, { provider_id: paymentProviderId,
id: "p2", },
captured_at: new Date(), {
data: { id: "id2" }, id: "p2",
amount: 1000, captured_at: new Date(),
amount_refunded: 0, data: { id: "id2" },
provider_id: paymentProviderId, amount: 1000,
}, { amount_refunded: 0,
id: "p3", provider_id: paymentProviderId,
captured_at: new Date(), },
data: { id: "id3" }, {
amount: 1000, id: "p3",
amount_refunded: 1000, // already fully refunded captured_at: new Date(),
provider_id: paymentProviderId, data: { id: "id3" },
}] amount: 1000,
amount_refunded: 1000, // already fully refunded
provider_id: paymentProviderId,
},
]
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.refundPayment = jest.fn().mockImplementation(async (data) => data) mockPaymentProcessor.refundPayment = jest
.fn()
.mockImplementation(async (data) => data)
container container
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) .register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor))
.register( .register(
"paymentRepository", "paymentRepository",
asValue( asValue(
MockRepository({ MockRepository({
find: jest find: jest.fn().mockImplementation(async () => payments),
.fn() })
.mockImplementation(async () => payments),
})
)
) )
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
const paymentRepo = container.resolve("paymentRepository") const paymentRepo = container.resolve("paymentRepository")
it("successfully refund the payments", async () => { it("successfully refund the payments", async () => {
await providerService.refundPayment(payments, 1500, RefundReason.OTHER, "note") await providerService.refundPayment(
payments,
1500,
RefundReason.OTHER,
"note"
)
const provider = container.resolve(paymentProcessorResolutionKey) const provider = container.resolve(paymentProcessorResolutionKey)
expect(provider.refundPayment).toBeCalledTimes(2) expect(provider.refundPayment).toBeCalledTimes(2)
expect(provider.refundPayment).toHaveBeenNthCalledWith(1, payments[0].data, 1000) expect(provider.refundPayment).toHaveBeenNthCalledWith(
expect(provider.refundPayment).toHaveBeenNthCalledWith(2, payments[1].data, 500) 1,
payments[0].data,
1000
)
expect(provider.refundPayment).toHaveBeenNthCalledWith(
2,
payments[1].data,
500
)
expect(paymentRepo.save).toBeCalledTimes(2) expect(paymentRepo.save).toBeCalledTimes(2)
expect(paymentRepo.save).toHaveBeenNthCalledWith(1, expect.objectContaining({ amount_refunded: 1000 })) expect(paymentRepo.save).toHaveBeenNthCalledWith(
expect(paymentRepo.save).toHaveBeenNthCalledWith(2, expect.objectContaining({ amount_refunded: 500 })) 1,
expect.objectContaining({ amount_refunded: 1000 })
)
expect(paymentRepo.save).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ amount_refunded: 500 })
)
}) })
it("throw an error using the provider error response", async () => { it("throw an error using the provider error response", async () => {
@@ -935,9 +960,7 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
const err = await providerService const err = await providerService.refundPayment(payments).catch((e) => e)
.refundPayment(payments)
.catch((e) => e)
expect(err.message).toBe( expect(err.message).toBe(
`${errResponse.error}:${EOL}${errResponse.detail}` `${errResponse.error}:${EOL}${errResponse.detail}`
@@ -958,16 +981,25 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
const container = createContainer({}, defaultContainer) const container = createContainer({}, defaultContainer)
const mockPaymentProcessor = new PaymentProcessor(container) const mockPaymentProcessor = new PaymentProcessor(container)
mockPaymentProcessor.refundPayment = jest.fn().mockImplementation(async (data) => data) mockPaymentProcessor.refundPayment = jest
.fn()
.mockImplementation(async (data) => data)
container container.register(
.register(paymentProcessorResolutionKey, asValue(mockPaymentProcessor)) paymentProcessorResolutionKey,
asValue(mockPaymentProcessor)
)
const providerService = container.resolve(paymentServiceResolutionKey) const providerService = container.resolve(paymentServiceResolutionKey)
const paymentRepo = container.resolve("paymentRepository") const paymentRepo = container.resolve("paymentRepository")
it("successfully refund the payments", async () => { it("successfully refund the payments", async () => {
await providerService.refundFromPayment(payment, 500, RefundReason.OTHER, "note") await providerService.refundFromPayment(
payment,
500,
RefundReason.OTHER,
"note"
)
const provider = container.resolve(paymentProcessorResolutionKey) const provider = container.resolve(paymentProcessorResolutionKey)
@@ -975,7 +1007,9 @@ describe("PaymentProviderService using AbstractPaymentProcessor", () => {
expect(provider.refundPayment).toBeCalledWith(payment.data, 500) expect(provider.refundPayment).toBeCalledWith(payment.data, 500)
expect(paymentRepo.save).toBeCalledTimes(1) expect(paymentRepo.save).toBeCalledTimes(1)
expect(paymentRepo.save).toBeCalledWith(expect.objectContaining({ amount_refunded: 500 })) expect(paymentRepo.save).toBeCalledWith(
expect.objectContaining({ amount_refunded: 500 })
)
}) })
it("throw an error using the provider error response", async () => { it("throw an error using the provider error response", async () => {
@@ -575,18 +575,23 @@ export default class PaymentProviderService extends TransactionBaseService {
const provider = this.retrieveProvider(paymentSession.provider_id) const provider = this.retrieveProvider(paymentSession.provider_id)
let updatedData
if (provider instanceof AbstractPaymentProcessor) { if (provider instanceof AbstractPaymentProcessor) {
throw new MedusaError( const res = await provider.updatePaymentData(paymentSession.id, data)
MedusaError.Types.NOT_ALLOWED, if ("error" in res) {
`The payment provider ${paymentSession.provider_id} is of type PaymentProcessor. PaymentProcessors cannot update payment session data.` this.throwFromPaymentProcessorError(res as PaymentProcessorError)
) } else {
updatedData = res
}
} else { } else {
session.data = await provider updatedData = await provider
.withTransaction(transactionManager) .withTransaction(transactionManager)
.updatePaymentData(paymentSession.data, data) .updatePaymentData(paymentSession.data, data)
session.status = paymentSession.status
} }
Object.assign(session.data, updatedData)
session.status = paymentSession.status
const sessionRepo = transactionManager.withRepository( const sessionRepo = transactionManager.withRepository(
this.paymentSessionRepository_ this.paymentSessionRepository_
) )