-
+
+
+ {getLocaleAmount(
+ payment.amount as number,
+ payment.currency_code
+ )}
+
+ -
+ (#{payment.id.substring(23)})
+
- ) : null
-
return (
- ({prettyReference} {prettyReferenceId})
+ ({prettyReference})
diff --git a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx
index f1852fefb7..294c4eb641 100644
--- a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx
+++ b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-create/components/refund-reason-create-form/refund-reason-create-form.tsx
@@ -13,6 +13,7 @@ import { useCreateRefundReason } from "../../../../../hooks/api"
const RefundReasonCreateSchema = z.object({
label: z.string().min(1),
+ code: z.string().min(1),
description: z.string().optional(),
})
@@ -23,11 +24,20 @@ export const RefundReasonCreateForm = () => {
const form = useForm>({
defaultValues: {
label: "",
+ code: "",
description: "",
},
resolver: zodResolver(RefundReasonCreateSchema),
})
+ const generateCodeFromLabel = (label: string) => {
+ return label
+ .toLowerCase()
+ .replace(/[^a-z0-9]/g, "_")
+ .replace(/_+/g, "_")
+ .replace(/^_|_$/g, "")
+ }
+
const { mutateAsync, isPending } = useCreateRefundReason()
const handleSubmit = form.handleSubmit(async (data) => {
@@ -81,6 +91,42 @@ export const RefundReasonCreateForm = () => {
placeholder={t(
"refundReasons.fields.label.placeholder"
)}
+ onChange={(e) => {
+ if (
+ !form.getFieldState("code").isTouched ||
+ !form.getValues("code")
+ ) {
+ form.setValue(
+ "code",
+ generateCodeFromLabel(e.target.value)
+ )
+ }
+ field.onChange(e)
+ }}
+ />
+
+
+
+ )
+ }}
+ />
+
+
+
{
+ return (
+
+
+ {t("refundReasons.fields.code.label")}
+
+
+
diff --git a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-edit/components/refund-reason-edit-form/refund-reason-edit-form.tsx b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-edit/components/refund-reason-edit-form/refund-reason-edit-form.tsx
index e70ac17f84..c6ae141659 100644
--- a/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-edit/components/refund-reason-edit-form/refund-reason-edit-form.tsx
+++ b/packages/admin/dashboard/src/routes/refund-reasons/refund-reason-edit/components/refund-reason-edit-form/refund-reason-edit-form.tsx
@@ -8,7 +8,7 @@ import { z } from "zod"
import { Form } from "../../../../../components/common/form"
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
-import { useUpdateRefundReason } from "../../../../../hooks/api/refund-reasons"
+import { useUpdateRefundReason } from "../../../../../hooks/api"
type RefundReasonEditFormProps = {
refundReason: HttpTypes.AdminRefundReason
@@ -16,6 +16,7 @@ type RefundReasonEditFormProps = {
const RefundReasonEditSchema = z.object({
label: z.string().min(1),
+ code: z.string().min(1),
description: z.string().optional(),
})
@@ -28,6 +29,7 @@ export const RefundReasonEditForm = ({
const form = useForm>({
defaultValues: {
label: refundReason.label,
+ code: refundReason.code,
description: refundReason.description ?? undefined,
},
resolver: zodResolver(RefundReasonEditSchema),
@@ -78,6 +80,26 @@ export const RefundReasonEditForm = ({
)
}}
/>
+ {
+ return (
+
+
+ {t("refundReasons.fields.code.label")}
+
+
+
+
+
+
+ )
+ }}
+ />
) {
const orderQuery = useQueryGraphStep({
@@ -69,8 +62,8 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow(
order_id: order.id,
version: orderChange.version,
action: ChangeActionType.CREDIT_LINE_ADD,
- reference: "payment_collection",
- reference_id: order.payment_collections[0]?.id,
+ reference: input.reference ?? "payment_collection",
+ reference_id: input.referenceId ?? order.payment_collections[0]?.id,
amount: input.amount,
})
)
diff --git a/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts
index 14f7e927d5..cc4cb2d17c 100644
--- a/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts
+++ b/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts
@@ -1,12 +1,5 @@
-import {
- CreateRefundReasonDTO,
- RefundReasonDTO,
-} from "@medusajs/framework/types"
-import {
- WorkflowData,
- WorkflowResponse,
- createWorkflow,
-} from "@medusajs/framework/workflows-sdk"
+import { CreateRefundReasonDTO, RefundReasonDTO, } from "@medusajs/framework/types"
+import { createWorkflow, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk"
import { createRefundReasonStep } from "../steps/create-refund-reasons"
/**
@@ -33,7 +26,8 @@ export const createRefundReasonsWorkflowId = "create-refund-reasons-workflow"
* input: {
* data: [
* {
- * label: "damaged",
+ * label: "Damaged",
+ * code: "damaged"
* }
* ]
* }
diff --git a/packages/core/core-flows/src/payment/steps/refund-payment.ts b/packages/core/core-flows/src/payment/steps/refund-payment.ts
index 0e5c596bc9..867551e56e 100644
--- a/packages/core/core-flows/src/payment/steps/refund-payment.ts
+++ b/packages/core/core-flows/src/payment/steps/refund-payment.ts
@@ -1,9 +1,6 @@
-import {
- BigNumberInput,
- IPaymentModuleService,
-} from "@medusajs/framework/types"
+import { BigNumberInput, IPaymentModuleService } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils"
-import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk"
+import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
/**
* The data to refund a payment.
diff --git a/packages/core/core-flows/src/payment/workflows/refund-payment.ts b/packages/core/core-flows/src/payment/workflows/refund-payment.ts
index 6b30c219f5..676cc533f7 100644
--- a/packages/core/core-flows/src/payment/workflows/refund-payment.ts
+++ b/packages/core/core-flows/src/payment/workflows/refund-payment.ts
@@ -1,89 +1,10 @@
-import type {
- BigNumberInput,
- OrderDTO,
- PaymentDTO,
-} from "@medusajs/framework/types"
-import { MathBN, MedusaError, PaymentEvents } from "@medusajs/framework/utils"
-import {
- WorkflowData,
- WorkflowResponse,
- createStep,
- createWorkflow,
- transform,
- when,
-} from "@medusajs/framework/workflows-sdk"
+import { BigNumberInput } from "@medusajs/framework/types"
+import { MathBN, PaymentEvents } from "@medusajs/framework/utils"
+import { createWorkflow, transform, when, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk"
import { emitEventStep, useRemoteQueryStep } from "../../common"
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
import { refundPaymentStep } from "../steps/refund-payment"
-
-/**
- * The data to validate whether the refund is valid for the order.
- */
-export type ValidateRefundStepInput = {
- /**
- * The order's details.
- */
- order: OrderDTO
- /**
- * The order's payment details.
- */
- payment: PaymentDTO
- /**
- * The amound to refund.
- */
- amount?: BigNumberInput
-}
-
-/**
- * This step validates that the refund is valid for the order.
- * If the order does not have an outstanding balance to refund, the step throws an error.
- *
- * :::note
- *
- * You can retrieve an order or payment's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query),
- * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep).
- *
- * :::
- *
- * @example
- * const data = validateRefundStep({
- * order: {
- * id: "order_123",
- * // other order details...
- * },
- * payment: {
- * id: "payment_123",
- * // other payment details...
- * },
- * amount: 10
- * })
- */
-export const validateRefundStep = createStep(
- "validate-refund-step",
- async function ({ order, payment, amount }: ValidateRefundStepInput) {
- const pendingDifference =
- order.summary?.raw_pending_difference! ??
- order.summary?.pending_difference! ??
- 0
-
- if (MathBN.gte(pendingDifference, 0)) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Order does not have an outstanding balance to refund`
- )
- }
-
- const amountPending = MathBN.mult(pendingDifference, -1)
- const amountToRefund = amount ?? payment.raw_amount ?? payment.amount
-
- if (MathBN.gt(amountToRefund, amountPending)) {
- throw new MedusaError(
- MedusaError.Types.INVALID_DATA,
- `Cannot refund more than pending difference - ${amountPending}`
- )
- }
- }
-)
+import { createOrderRefundCreditLinesWorkflow } from "../../order/workflows/payments/create-order-refund-credit-lines"
/**
* The data to refund a payment.
@@ -101,6 +22,14 @@ export type RefundPaymentWorkflowInput = {
* The amount to refund. If not provided, the full payment amount will be refunded.
*/
amount?: BigNumberInput
+ /**
+ * The note to attach to the refund.
+ */
+ note?: string
+ /**
+ * The ID of the refund reason to attach to the refund.
+ */
+ refund_reason_id?: string
}
export const refundPaymentWorkflowId = "refund-payment-workflow"
@@ -156,7 +85,46 @@ export const refundPaymentWorkflow = createWorkflow(
list: false,
}).config({ name: "order" })
- validateRefundStep({ order, payment, amount: input.amount })
+ const refundReason = when(
+ "fetch-refund-reason",
+ { input }, ({ input }) =>
+ !!input.refund_reason_id
+ ).then(() => {
+ return useRemoteQueryStep({
+ entry_point: "refund_reason",
+ fields: ["id", "label", "code"],
+ variables: { id: input.refund_reason_id },
+ list: false,
+ throw_if_key_not_found: true,
+ }).config({ name: "refund-reason" })
+ })
+
+ const creditLineAmount = transform({ order, payment, input }, ({ order, payment, input }) => {
+ const pendingDifference = order.summary?.raw_pending_difference! ?? order.summary?.pending_difference! ?? 0
+ const amountToRefund = input.amount ?? payment.raw_amount ?? payment.amount
+
+ if (MathBN.lt(pendingDifference, 0)) {
+ const amountOwed = MathBN.mult(pendingDifference, -1)
+
+ return MathBN.gt(amountToRefund, amountOwed) ? MathBN.sub(amountToRefund, amountOwed) : 0
+ }
+
+ return amountToRefund
+ })
+
+ when(
+ { creditLineAmount, refundReason }, ({ creditLineAmount, refundReason }) => MathBN.gt(creditLineAmount, 0)
+ ).then(() => {
+ createOrderRefundCreditLinesWorkflow.runAsStep({
+ input: {
+ order_id: order.id,
+ amount: creditLineAmount,
+ reference: refundReason?.label,
+ referenceId: refundReason?.code
+ },
+ })
+ })
+
const refundPayment = refundPaymentStep(input)
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
diff --git a/packages/core/types/src/http/refund-reason/admin/payloads.ts b/packages/core/types/src/http/refund-reason/admin/payloads.ts
index aff7890886..88f2ecca57 100644
--- a/packages/core/types/src/http/refund-reason/admin/payloads.ts
+++ b/packages/core/types/src/http/refund-reason/admin/payloads.ts
@@ -6,6 +6,13 @@ type AdminBaseRefundReasonPayload = {
* "Refund"
*/
label: string
+ /**
+ * The refund reason's code.
+ *
+ * @example
+ * "refund"
+ */
+ code: string
/**
* The refund reason's description.
*/
diff --git a/packages/core/types/src/http/refund-reason/common.ts b/packages/core/types/src/http/refund-reason/common.ts
index f137a9696a..f3ffa00aae 100644
--- a/packages/core/types/src/http/refund-reason/common.ts
+++ b/packages/core/types/src/http/refund-reason/common.ts
@@ -13,6 +13,13 @@ export interface BaseRefundReason {
* "Refund"
*/
label: string
+ /**
+ * The refund reason's code.
+ *
+ * @example
+ * "refund"
+ */
+ code: string
/**
* The refund reason's description.
*/
diff --git a/packages/core/types/src/payment/mutations.ts b/packages/core/types/src/payment/mutations.ts
index 7a0369dcc6..7fdbf68df7 100644
--- a/packages/core/types/src/payment/mutations.ts
+++ b/packages/core/types/src/payment/mutations.ts
@@ -1,10 +1,6 @@
import { BigNumberInput } from "../totals"
import { PaymentCollectionStatus, PaymentSessionStatus } from "./common"
-import {
- PaymentAccountHolderDTO,
- PaymentCustomerDTO,
- PaymentProviderContext,
-} from "./provider"
+import { PaymentAccountHolderDTO, PaymentCustomerDTO, PaymentProviderContext, } from "./provider"
/**
* The payment collection to be created.
@@ -356,6 +352,10 @@ export interface CreateRefundReasonDTO {
* The label of the refund reason
*/
label: string
+ /**
+ * The code of the refund reason
+ */
+ code: string
/**
* The description of the refund reason
*/
diff --git a/packages/core/types/src/payment/service.ts b/packages/core/types/src/payment/service.ts
index e609052a33..9f072bbe62 100644
--- a/packages/core/types/src/payment/service.ts
+++ b/packages/core/types/src/payment/service.ts
@@ -22,20 +22,20 @@ import {
RefundReasonDTO,
} from "./common"
import {
+ CreateAccountHolderDTO,
CreateCaptureDTO,
CreatePaymentCollectionDTO,
+ CreatePaymentMethodDTO,
CreatePaymentSessionDTO,
CreateRefundDTO,
CreateRefundReasonDTO,
PaymentCollectionUpdatableFields,
ProviderWebhookPayload,
+ UpdateAccountHolderDTO,
UpdatePaymentDTO,
UpdatePaymentSessionDTO,
UpdateRefundReasonDTO,
- CreateAccountHolderDTO,
UpsertPaymentCollectionDTO,
- CreatePaymentMethodDTO,
- UpdateAccountHolderDTO,
} from "./mutations"
import { WebhookActionResult } from "./provider"
@@ -1242,9 +1242,11 @@ export interface IPaymentModuleService extends IModuleService {
* await paymentModuleService.createRefundReasons([
* {
* label: "Too big",
+ * code: "too_big
* },
* {
- * label: "Too big",
+ * label: "Too small",
+ * code: "too_small
* },
* ])
*/
@@ -1264,6 +1266,7 @@ export interface IPaymentModuleService extends IModuleService {
* const refundReason =
* await paymentModuleService.createRefundReasons({
* label: "Too big",
+ * code: "too_big"
* })
*/
createRefundReasons(
diff --git a/packages/medusa/src/api/admin/payments/query-config.ts b/packages/medusa/src/api/admin/payments/query-config.ts
index 36fbe40f73..63989993df 100644
--- a/packages/medusa/src/api/admin/payments/query-config.ts
+++ b/packages/medusa/src/api/admin/payments/query-config.ts
@@ -12,6 +12,7 @@ export const defaultAdminPaymentFields = [
"refunds.note",
"refunds.payment_id",
"refunds.refund_reason.label",
+ "refunds.refund_reason.code",
]
export const listTransformQueryConfig = {
diff --git a/packages/medusa/src/api/admin/refund-reasons/query-config.ts b/packages/medusa/src/api/admin/refund-reasons/query-config.ts
index 7a76a79b4c..954b9dbf53 100644
--- a/packages/medusa/src/api/admin/refund-reasons/query-config.ts
+++ b/packages/medusa/src/api/admin/refund-reasons/query-config.ts
@@ -1,6 +1,7 @@
export const defaultAdminRefundReasonFields = [
"id",
"label",
+ "code",
"description",
"created_at",
"updated_at",
diff --git a/packages/medusa/src/api/admin/refund-reasons/validators.ts b/packages/medusa/src/api/admin/refund-reasons/validators.ts
index 1d978ce436..f549f9ec4f 100644
--- a/packages/medusa/src/api/admin/refund-reasons/validators.ts
+++ b/packages/medusa/src/api/admin/refund-reasons/validators.ts
@@ -7,6 +7,7 @@ export type AdminCreatePaymentRefundReasonType = z.infer<
export const AdminCreatePaymentRefundReason = z
.object({
label: z.string(),
+ code: z.string(),
description: z.string().nullish(),
})
.strict()
@@ -17,6 +18,7 @@ export type AdminUpdatePaymentRefundReasonType = z.infer<
export const AdminUpdatePaymentRefundReason = z
.object({
label: z.string().optional(),
+ code: z.string().optional(),
description: z.string().nullish(),
})
.strict()
diff --git a/packages/modules/payment/package.json b/packages/modules/payment/package.json
index 29ef89d4f2..e0b8464821 100644
--- a/packages/modules/payment/package.json
+++ b/packages/modules/payment/package.json
@@ -30,10 +30,10 @@
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
"test": "jest --bail --passWithNoTests --forceExit -- src",
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
- "migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:create --initial",
- "migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm-orm migration:create",
- "migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm-orm migration:up",
- "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm-orm cache:clear"
+ "migration:initial": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro migration:create --initial",
+ "migration:create": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:create",
+ "migration:up": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm migration:up",
+ "orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm cache:clear"
},
"devDependencies": {
"@medusajs/framework": "2.10.3",
diff --git a/packages/modules/payment/src/migrations/Migration20250929124701.ts b/packages/modules/payment/src/migrations/Migration20250929124701.ts
new file mode 100644
index 0000000000..114a01451e
--- /dev/null
+++ b/packages/modules/payment/src/migrations/Migration20250929124701.ts
@@ -0,0 +1,24 @@
+import { Migration } from "@mikro-orm/migrations"
+
+export class Migration20250929124701 extends Migration {
+ override async up(): Promise {
+ // Step 1: Add the column as nullable
+ this.addSql(
+ `alter table "refund_reason" add column "code" text;`
+ )
+
+ // Step 2: Populate the code column from label (convert to snake_case)
+ this.addSql(`
+ update "refund_reason"
+ set "code" = lower(replace("label", ' ', '_'));
+ `)
+
+ // Step 3: Set the column to not nullable
+ this.addSql(`alter table "refund_reason" alter column "code" set not null;`)
+ }
+
+ override async down(): Promise {
+ // Remove the code column
+ this.addSql(`alter table "refund_reason" drop column "code";`)
+ }
+}
diff --git a/packages/modules/payment/src/models/refund-reason.ts b/packages/modules/payment/src/models/refund-reason.ts
index 90c9da40f0..b6236961a3 100644
--- a/packages/modules/payment/src/models/refund-reason.ts
+++ b/packages/modules/payment/src/models/refund-reason.ts
@@ -4,6 +4,7 @@ import Refund from "./refund"
const RefundReason = model.define("RefundReason", {
id: model.id({ prefix: "refr" }).primaryKey(),
label: model.text().searchable(),
+ code: model.text().searchable(),
description: model.text().searchable().nullable(),
metadata: model.json().nullable(),
refunds: model.hasMany(() => Refund, {