feat: Refund payment (#6610)

Essentially the same as #6601 but for refunds
This commit is contained in:
Oli Juhl authored and GitHub committed 2024-03-07 12:34:36 +00:00
1 parent 7dee1a3fd3
commit e5cbe28d54
10 files changed
+245 -36

No files matched your search

@@ -1,4 +1,7 @@
import { capturePaymentWorkflow } from "@medusajs/core-flows" import {
capturePaymentWorkflow,
refundPaymentWorkflow,
} from "@medusajs/core-flows"
import { import {
LinkModuleUtils, LinkModuleUtils,
ModuleRegistrationName, ModuleRegistrationName,
@@ -120,7 +123,7 @@ medusaIntegrationTestRunner({
) )
}) })
it("should capture a payment with custom amount", async () => { it("should partially capture a payment", async () => {
const paymentCollection = await paymentService.createPaymentCollections( const paymentCollection = await paymentService.createPaymentCollections(
{ {
region_id: "test-region", region_id: "test-region",
@@ -176,6 +179,109 @@ medusaIntegrationTestRunner({
}) })
) )
}) })
it("should refund a payment", async () => {
const paymentCollection = await paymentService.createPaymentCollections(
{
region_id: "test-region",
amount: 1000,
currency_code: "usd",
}
)
const paymentSession = await paymentService.createPaymentSession(
paymentCollection.id,
{
provider_id: "pp_system_default",
amount: 1000,
currency_code: "usd",
data: {},
}
)
const payment = await paymentService.authorizePaymentSession(
paymentSession.id,
{}
)
await capturePaymentWorkflow(appContainer).run({
input: {
payment_id: payment.id,
},
throwOnError: false,
})
await refundPaymentWorkflow(appContainer).run({
input: {
payment_id: payment.id,
},
throwOnError: false,
})
const [refund] = await paymentService.listRefunds({
payment_id: payment.id,
})
expect(refund).toEqual(
expect.objectContaining({
id: expect.any(String),
payment: expect.objectContaining({ id: payment.id }),
amount: 1000,
})
)
})
it("should partially refund a payment", async () => {
const paymentCollection = await paymentService.createPaymentCollections(
{
region_id: "test-region",
amount: 1000,
currency_code: "usd",
}
)
const paymentSession = await paymentService.createPaymentSession(
paymentCollection.id,
{
provider_id: "pp_system_default",
amount: 1000,
currency_code: "usd",
data: {},
}
)
const payment = await paymentService.authorizePaymentSession(
paymentSession.id,
{}
)
await capturePaymentWorkflow(appContainer).run({
input: {
payment_id: payment.id,
},
throwOnError: false,
})
await refundPaymentWorkflow(appContainer).run({
input: {
payment_id: payment.id,
amount: 500,
},
throwOnError: false,
})
const [refund] = await paymentService.listRefunds({
payment_id: payment.id,
})
expect(refund).toEqual(
expect.objectContaining({
id: expect.any(String),
payment: expect.objectContaining({ id: payment.id }),
amount: 500,
})
)
})
}) })
}, },
}) })
@@ -1 +1,3 @@
export * from "./capture-payment" export * from "./capture-payment"
export * from "./refund-payment"
@@ -0,0 +1,23 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { IPaymentModuleService } from "@medusajs/types"
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
type StepInput = {
payment_id: string
created_by?: string
amount?: number
}
export const refundPaymentStepId = "refund-payment-step"
export const refundPaymentStep = createStep(
refundPaymentStepId,
async (input: StepInput, { container }) => {
const paymentModule = container.resolve<IPaymentModuleService>(
ModuleRegistrationName.PAYMENT
)
const payment = await paymentModule.refundPayment(input)
return new StepResponse(payment)
}
)
@@ -1 +1,3 @@
export * from "./capture-payment" export * from "./capture-payment"
export * from "./refund-payment"
@@ -0,0 +1,17 @@
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
import { refundPaymentStep } from "../steps/refund-payment"
export const refundPaymentWorkflowId = "refund-payment-workflow"
export const refundPaymentWorkflow = createWorkflow(
refundPaymentWorkflowId,
(
input: WorkflowData<{
payment_id: string
created_by?: string
amount?: number
}>
) => {
const payment = refundPaymentStep(input)
return payment
}
)
@@ -0,0 +1,42 @@
import { refundPaymentWorkflow } from "@medusajs/core-flows"
import { Modules } from "@medusajs/modules-sdk"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../../types/routing"
import { defaultAdminPaymentFields } from "../../query-config"
export const POST = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const { id } = req.params
const { errors } = await refundPaymentWorkflow(req.scope).run({
input: {
payment_id: id,
created_by: req.auth?.actor_id,
},
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const query = remoteQueryObjectFromString({
entryPoint: Modules.PAYMENT,
variables: { id },
fields: defaultAdminPaymentFields,
})
const [payment] = await remoteQuery(query)
res.status(200).json({ payment })
}
@@ -35,10 +35,9 @@ export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
matcher: "/admin/payments/:id/capture", matcher: "/admin/payments/:id/capture",
middlewares: [], middlewares: [],
}, },
// TODO: Add in follow-up PR {
// { method: ["POST"],
// method: ["POST"], matcher: "/admin/payments/:id/refund",
// matcher: "/admin/payments/:id/refund", middlewares: [],
// middlewares: [], },
// },
] ]
@@ -642,23 +642,23 @@ moduleIntegrationTestRunner({
) )
}) })
// it("should throw if refund is greater than captured amount", async () => { it("should throw if refund is greater than captured amount", async () => {
// await service.capturePayment({ await service.capturePayment({
// amount: 50, amount: 50,
// payment_id: "pay-id-1", payment_id: "pay-id-1",
// }) })
//
// const error = await service const error = await service
// .refundPayment({ .refundPayment({
// amount: 100, amount: 100,
// payment_id: "pay-id-1", payment_id: "pay-id-1",
// }) })
// .catch((e) => e) .catch((e) => e)
//
// expect(error.message).toEqual( expect(error.message).toEqual(
// "Refund amount for payment: pay-id-1 cannot be greater than the amount captured on the payment." "You cannot refund more than what is captured on the payment."
// ) )
// }) })
}) })
describe("cancel", () => { describe("cancel", () => {
@@ -39,6 +39,12 @@ export class Migration20240225134525 extends Migration {
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL; ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL; ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "created_by" TEXT NULL;
${generatePostgresAlterColummnIfExistStatement(
"refund",
["reason"],
"DROP NOT NULL"
)}
CREATE TABLE IF NOT EXISTS "capture" ( CREATE TABLE IF NOT EXISTS "capture" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
+23 -11
View File
@@ -450,9 +450,9 @@ export default class PaymentModuleService<
) )
} }
const capturedAmount = payment.captures.reduce((acc, next) => { const capturedAmount = payment.captures.reduce((captureAmount, next) => {
const bn = new BigNumber(next.raw_amount.value) const amountAsBigNumber = new BigNumber(next.raw_amount.value)
return acc.plus(bn) return captureAmount.plus(amountAsBigNumber)
}, BigNumber(0)) }, BigNumber(0))
const authorizedAmount = BigNumber(payment.raw_amount.value) const authorizedAmount = BigNumber(payment.raw_amount.value)
@@ -507,17 +507,29 @@ export default class PaymentModuleService<
): Promise<PaymentDTO> { ): Promise<PaymentDTO> {
const payment = await this.paymentService_.retrieve( const payment = await this.paymentService_.retrieve(
data.payment_id, data.payment_id,
{ select: ["id", "data", "provider_id"] }, {
select: ["id", "data", "provider_id", "amount", "raw_amount"],
relations: ["captures.raw_amount"],
},
sharedContext sharedContext
) )
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged if (!data.amount) {
// if (payment.captured_amount < input.amount) { data.amount = payment.amount as number
// throw new MedusaError( }
// MedusaError.Types.INVALID_DATA,
// `Refund amount for payment: ${payment.id} cannot be greater than the amount captured on the payment.` const capturedAmount = payment.captures.reduce((captureAmount, next) => {
// ) const amountAsBigNumber = new BigNumber(next.raw_amount.value)
// } return captureAmount.plus(amountAsBigNumber)
}, BigNumber(0))
const refundAmount = BigNumber(data.amount)
if (capturedAmount.lt(refundAmount)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`You cannot refund more than what is captured on the payment.`
)
}
const paymentData = await this.paymentProviderService_.refundPayment( const paymentData = await this.paymentProviderService_.refundPayment(
{ {