From 087887fefb948e6676001ad2dc2cb265ae6f7430 Mon Sep 17 00:00:00 2001 From: William Bouchard <46496014+willbouch@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:38:50 -0400 Subject: [PATCH] feat(core-flows): support ad hoc returns (#13598) * feat(core-flows): support ad hoc returns * fix: missing transform * handle edge case * refactor * replace gte for gt * cleanup * weird bug fix * add test * Create quick-nails-kick.md * stop sending empty strings * add code to refund reason * fix build * fix tests * handle code in dashboard * fix tests * more tests failing * add reference and reference id to credit lieng * rework create refund form --- .changeset/quick-nails-kick.md | 6 + .../__tests__/payment/admin/payment.spec.ts | 64 ++++----- .../refund-reason/refund-reason.spec.ts | 23 +-- .../use-refund-reason-table-columns.tsx | 7 + .../src/i18n/translations/$schema.json | 15 +- .../dashboard/src/i18n/translations/en.json | 4 + .../create-refund-form/create-refund-form.tsx | 88 ++---------- .../order-summary-section.tsx | 6 +- .../refund-reason-create-form.tsx | 46 ++++++ .../refund-reason-edit-form.tsx | 24 +++- .../create-order-refund-credit-lines.ts | 21 +-- .../workflows/create-refund-reasons.ts | 14 +- .../src/payment/steps/refund-payment.ts | 7 +- .../src/payment/workflows/refund-payment.ts | 136 +++++++----------- .../src/http/refund-reason/admin/payloads.ts | 7 + .../types/src/http/refund-reason/common.ts | 7 + packages/core/types/src/payment/mutations.ts | 10 +- packages/core/types/src/payment/service.ts | 11 +- .../src/api/admin/payments/query-config.ts | 1 + .../api/admin/refund-reasons/query-config.ts | 1 + .../api/admin/refund-reasons/validators.ts | 2 + packages/modules/payment/package.json | 8 +- .../src/migrations/Migration20250929124701.ts | 24 ++++ .../payment/src/models/refund-reason.ts | 1 + 24 files changed, 278 insertions(+), 255 deletions(-) create mode 100644 .changeset/quick-nails-kick.md create mode 100644 packages/modules/payment/src/migrations/Migration20250929124701.ts diff --git a/.changeset/quick-nails-kick.md b/.changeset/quick-nails-kick.md new file mode 100644 index 0000000000..3130e802e7 --- /dev/null +++ b/.changeset/quick-nails-kick.md @@ -0,0 +1,6 @@ +--- +"@medusajs/payment": patch +"@medusajs/core-flows": patch +--- + +feat(core-flows): support ad hoc returns diff --git a/integration-tests/http/__tests__/payment/admin/payment.spec.ts b/integration-tests/http/__tests__/payment/admin/payment.spec.ts index b29d9c73d8..af3b9270f0 100644 --- a/integration-tests/http/__tests__/payment/admin/payment.spec.ts +++ b/integration-tests/http/__tests__/payment/admin/payment.spec.ts @@ -218,7 +218,7 @@ medusaIntegrationTestRunner({ const refundReason = ( await api.post( `/admin/refund-reasons`, - { label: "test" }, + { label: "test", code: "test" }, adminHeaders ) ).data.refund_reason @@ -254,6 +254,7 @@ medusaIntegrationTestRunner({ refund_reason_id: refundReason.id, refund_reason: expect.objectContaining({ label: "test", + code: "test" }), }), ], @@ -274,7 +275,7 @@ medusaIntegrationTestRunner({ const refundReason = ( await api.post( `/admin/refund-reasons`, - { label: "test" }, + { label: "test", code: "test" }, adminHeaders ) ).data.refund_reason @@ -330,57 +331,44 @@ medusaIntegrationTestRunner({ ) }) - it("should throw if refund exceeds captured total", async () => { + it("should create credit lines if issuing a refund when outstanding amount if >= 0", async () => { const payment = order.payment_collections[0].payments[0] + const refundReason = ( + await api.post( + `/admin/refund-reasons`, + { label: "Test", code: "test" }, + adminHeaders + ) + ).data.refund_reason + await api.post( `/admin/payments/${payment.id}/capture`, undefined, adminHeaders ) - await createClaim({ order }) - await api.post( `/admin/payments/${payment.id}/refund`, - { amount: 25 }, + { + amount: 50, + refund_reason_id: refundReason.id, + }, adminHeaders ) - const e = await api - .post( - `/admin/payments/${payment.id}/refund`, - { amount: 1000 }, - adminHeaders - ) - .catch((e) => e) + const updatedOrder = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order - expect(e.response.data.message).toEqual( - "Cannot refund more than pending difference - 75" - ) + expect(updatedOrder.credit_line_total).toEqual(50) + expect(updatedOrder.credit_lines).toEqual([ + expect.objectContaining({ + reference: "Test", + reference_id: "test", + }), + ]) }) }) - - it("should throw if outstanding amount is not present", async () => { - const payment = order.payment_collections[0].payments[0] - - await api.post( - `/admin/payments/${payment.id}/capture`, - undefined, - adminHeaders - ) - - const e = await api - .post( - `/admin/payments/${payment.id}/refund`, - { amount: 10 }, - adminHeaders - ) - .catch((e) => e) - - expect(e.response.data.message).toEqual( - "Order does not have an outstanding balance to refund" - ) - }) }, }) diff --git a/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts b/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts index 7427e7f1b2..cd54324e18 100644 --- a/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts +++ b/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts @@ -1,5 +1,8 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { adminHeaders, createAdminUser, } from "../../../helpers/create-admin-user" +import { + adminHeaders, + createAdminUser, +} from "../../../helpers/create-admin-user" jest.setTimeout(30000) @@ -15,7 +18,7 @@ medusaIntegrationTestRunner({ refundReason1 = ( await api.post( "/admin/refund-reasons", - { label: "reason 1 - too big" }, + { label: "reason 1 - too big", code: "too_big" }, adminHeaders ) ).data.refund_reason @@ -23,7 +26,7 @@ medusaIntegrationTestRunner({ refundReason2 = ( await api.post( "/admin/refund-reasons", - { label: "reason 2 - too small" }, + { label: "reason 2 - too small", code: "too_small" }, adminHeaders ) ).data.refund_reason @@ -41,11 +44,11 @@ medusaIntegrationTestRunner({ expect(response.data.count).toEqual(5) // There are 3 default ones expect(response.data.refund_reasons).toEqual( expect.arrayContaining([ - expect.objectContaining({ label: "Customer Care Adjustment" }), - expect.objectContaining({ label: "Shipping Issue" }), - expect.objectContaining({ label: "Pricing Error" }), - expect.objectContaining({ label: "reason 1 - too big" }), - expect.objectContaining({ label: "reason 2 - too small" }), + expect.objectContaining({ label: "Customer Care Adjustment", code: "customer_care_adjustment" }), + expect.objectContaining({ label: "Shipping Issue", code: "shipping_issue" }), + expect.objectContaining({ label: "Pricing Error", code: "pricing_error" }), + expect.objectContaining({ label: "reason 1 - too big", code: "too_big" }), + expect.objectContaining({ label: "reason 2 - too small", code: "too_small" }), ]) ) }) @@ -74,6 +77,7 @@ medusaIntegrationTestRunner({ "/admin/refund-reasons", { label: "reason test", + code: "reason_test", description: "test description", }, adminHeaders @@ -83,6 +87,7 @@ medusaIntegrationTestRunner({ expect(response.data.refund_reason).toEqual( expect.objectContaining({ label: "reason test", + code: "reason_test", description: "test description", }) ) @@ -95,6 +100,7 @@ medusaIntegrationTestRunner({ `/admin/refund-reasons/${refundReason1.id}`, { label: "reason test", + code: "reason_test", description: "test description", }, adminHeaders @@ -104,6 +110,7 @@ medusaIntegrationTestRunner({ expect(response.data.refund_reason).toEqual( expect.objectContaining({ label: "reason test", + code: "reason_test", description: "test description", }) ) diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx index ba9f27691f..e7c8a11fba 100644 --- a/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx +++ b/packages/admin/dashboard/src/hooks/table/columns/use-refund-reason-table-columns.tsx @@ -18,6 +18,13 @@ export const useRefundReasonTableColumns = () => { sortAscLabel: t("filters.sorting.alphabeticallyAsc"), sortDescLabel: t("filters.sorting.alphabeticallyDesc"), }), + columnHelper.accessor("code", { + header: () => t("fields.code"), + enableSorting: true, + sortLabel: t("fields.code"), + sortAscLabel: t("filters.sorting.alphabeticallyAsc"), + sortDescLabel: t("filters.sorting.alphabeticallyDesc"), + }), columnHelper.accessor("description", { header: () => t("fields.description"), cell: ({ getValue }) => , diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index bab6e44430..21f6e6d61e 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -10306,6 +10306,19 @@ "required": ["label", "placeholder"], "additionalProperties": false }, + "code": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "placeholder": { + "type": "string" + } + }, + "required": ["label", "placeholder"], + "additionalProperties": false + }, "description": { "type": "object", "properties": { @@ -10320,7 +10333,7 @@ "additionalProperties": false } }, - "required": ["label", "description"], + "required": ["label", "code", "description"], "additionalProperties": false } }, diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 8f4e7bc1a3..87506a8e03 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -2771,6 +2771,10 @@ "label": "Label", "placeholder": "Gesture of goodwill" }, + "code": { + "label": "Code", + "placeholder": "gesture_of_goodwill" + }, "description": { "label": "Description", "placeholder": "Customer had a bad shopping experience" diff --git a/packages/admin/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx b/packages/admin/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx index 11c2b41f1a..38f19a0331 100644 --- a/packages/admin/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-create-refund/components/create-refund-form/create-refund-form.tsx @@ -1,14 +1,7 @@ import { zodResolver } from "@hookform/resolvers/zod" import { HttpTypes } from "@medusajs/types" -import { - Button, - CurrencyInput, - Label, - Select, - Textarea, - toast, -} from "@medusajs/ui" -import { useEffect, useMemo, useState } from "react" +import { Button, CurrencyInput, Select, Textarea, toast } from "@medusajs/ui" +import { useMemo, useState } from "react" import { formatValue } from "react-currency-input-field" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" @@ -20,7 +13,6 @@ import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { useRefundPayment, useRefundReasons } from "../../../../../hooks/api" import { currencies } from "../../../../../lib/data/currencies" import { formatCurrency } from "../../../../../lib/format-currency" -import { formatProvider } from "../../../../../lib/format-provider" import { getLocaleAmount } from "../../../../../lib/money-amount-helpers" import { getPaymentsFromOrder } from "../../../../../lib/orders" import { useDocumentDirection } from "../../../../../hooks/use-document-direction" @@ -63,29 +55,10 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => { value: paymentAmount.toFixed(currency.decimal_digits), float: paymentAmount, }, - note: "", - refund_reason_id: "", }, resolver: zodResolver(CreateRefundSchema), }) - useEffect(() => { - const pendingDifference = order.summary.pending_difference as number - const paymentAmount = (payment?.amount || 0) as number - const pendingAmount = - pendingDifference < 0 - ? Math.min(Math.abs(pendingDifference), paymentAmount) - : paymentAmount - - const normalizedAmount = - pendingAmount < 0 ? pendingAmount * -1 : pendingAmount - - form.setValue("amount", { - value: normalizedAmount.toFixed(currency.decimal_digits), - float: normalizedAmount, - }) - }, [payment?.id || ""]) - const { mutateAsync, isPending } = useRefundPayment(order.id, payment?.id!) const handleSubmit = form.handleSubmit(async (data) => { @@ -123,53 +96,16 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => { >
- +
+ + {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, {