feat: Refund payment (#6610)
Essentially the same as #6601 but for refunds
This commit is contained in:
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: [],
|
},
|
||||||
// },
|
|
||||||
]
|
]
|
||||||
+17
-17
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user