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
This commit is contained in:
1 parent
bd9ecd5e66
commit
087887fefb
24 files changed
+272
-249
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/payment": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(core-flows): support ad hoc returns
|
||||||
@@ -218,7 +218,7 @@ medusaIntegrationTestRunner({
|
|||||||
const refundReason = (
|
const refundReason = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/refund-reasons`,
|
`/admin/refund-reasons`,
|
||||||
{ label: "test" },
|
{ label: "test", code: "test" },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.refund_reason
|
).data.refund_reason
|
||||||
@@ -254,6 +254,7 @@ medusaIntegrationTestRunner({
|
|||||||
refund_reason_id: refundReason.id,
|
refund_reason_id: refundReason.id,
|
||||||
refund_reason: expect.objectContaining({
|
refund_reason: expect.objectContaining({
|
||||||
label: "test",
|
label: "test",
|
||||||
|
code: "test"
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -274,7 +275,7 @@ medusaIntegrationTestRunner({
|
|||||||
const refundReason = (
|
const refundReason = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/refund-reasons`,
|
`/admin/refund-reasons`,
|
||||||
{ label: "test" },
|
{ label: "test", code: "test" },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.refund_reason
|
).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 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(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/capture`,
|
`/admin/payments/${payment.id}/capture`,
|
||||||
undefined,
|
undefined,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
await createClaim({ order })
|
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{ amount: 25 },
|
{
|
||||||
|
amount: 50,
|
||||||
|
refund_reason_id: refundReason.id,
|
||||||
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
const e = await api
|
const updatedOrder = (
|
||||||
.post(
|
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||||
`/admin/payments/${payment.id}/refund`,
|
).data.order
|
||||||
{ amount: 1000 },
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
.catch((e) => e)
|
|
||||||
|
|
||||||
expect(e.response.data.message).toEqual(
|
expect(updatedOrder.credit_line_total).toEqual(50)
|
||||||
"Cannot refund more than pending difference - 75"
|
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"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
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)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -15,7 +18,7 @@ medusaIntegrationTestRunner({
|
|||||||
refundReason1 = (
|
refundReason1 = (
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/refund-reasons",
|
"/admin/refund-reasons",
|
||||||
{ label: "reason 1 - too big" },
|
{ label: "reason 1 - too big", code: "too_big" },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.refund_reason
|
).data.refund_reason
|
||||||
@@ -23,7 +26,7 @@ medusaIntegrationTestRunner({
|
|||||||
refundReason2 = (
|
refundReason2 = (
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/refund-reasons",
|
"/admin/refund-reasons",
|
||||||
{ label: "reason 2 - too small" },
|
{ label: "reason 2 - too small", code: "too_small" },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
).data.refund_reason
|
).data.refund_reason
|
||||||
@@ -41,11 +44,11 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.count).toEqual(5) // There are 3 default ones
|
expect(response.data.count).toEqual(5) // There are 3 default ones
|
||||||
expect(response.data.refund_reasons).toEqual(
|
expect(response.data.refund_reasons).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ label: "Customer Care Adjustment" }),
|
expect.objectContaining({ label: "Customer Care Adjustment", code: "customer_care_adjustment" }),
|
||||||
expect.objectContaining({ label: "Shipping Issue" }),
|
expect.objectContaining({ label: "Shipping Issue", code: "shipping_issue" }),
|
||||||
expect.objectContaining({ label: "Pricing Error" }),
|
expect.objectContaining({ label: "Pricing Error", code: "pricing_error" }),
|
||||||
expect.objectContaining({ label: "reason 1 - too big" }),
|
expect.objectContaining({ label: "reason 1 - too big", code: "too_big" }),
|
||||||
expect.objectContaining({ label: "reason 2 - too small" }),
|
expect.objectContaining({ label: "reason 2 - too small", code: "too_small" }),
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -74,6 +77,7 @@ medusaIntegrationTestRunner({
|
|||||||
"/admin/refund-reasons",
|
"/admin/refund-reasons",
|
||||||
{
|
{
|
||||||
label: "reason test",
|
label: "reason test",
|
||||||
|
code: "reason_test",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
@@ -83,6 +87,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.refund_reason).toEqual(
|
expect(response.data.refund_reason).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
label: "reason test",
|
label: "reason test",
|
||||||
|
code: "reason_test",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -95,6 +100,7 @@ medusaIntegrationTestRunner({
|
|||||||
`/admin/refund-reasons/${refundReason1.id}`,
|
`/admin/refund-reasons/${refundReason1.id}`,
|
||||||
{
|
{
|
||||||
label: "reason test",
|
label: "reason test",
|
||||||
|
code: "reason_test",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
@@ -104,6 +110,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.data.refund_reason).toEqual(
|
expect(response.data.refund_reason).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
label: "reason test",
|
label: "reason test",
|
||||||
|
code: "reason_test",
|
||||||
description: "test description",
|
description: "test description",
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ export const useRefundReasonTableColumns = () => {
|
|||||||
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
|
sortAscLabel: t("filters.sorting.alphabeticallyAsc"),
|
||||||
sortDescLabel: t("filters.sorting.alphabeticallyDesc"),
|
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", {
|
columnHelper.accessor("description", {
|
||||||
header: () => t("fields.description"),
|
header: () => t("fields.description"),
|
||||||
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
|
cell: ({ getValue }) => <DescriptionCell description={getValue()} />,
|
||||||
|
|||||||
@@ -10306,6 +10306,19 @@
|
|||||||
"required": ["label", "placeholder"],
|
"required": ["label", "placeholder"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["label", "placeholder"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -10320,7 +10333,7 @@
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["label", "description"],
|
"required": ["label", "code", "description"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2771,6 +2771,10 @@
|
|||||||
"label": "Label",
|
"label": "Label",
|
||||||
"placeholder": "Gesture of goodwill"
|
"placeholder": "Gesture of goodwill"
|
||||||
},
|
},
|
||||||
|
"code": {
|
||||||
|
"label": "Code",
|
||||||
|
"placeholder": "gesture_of_goodwill"
|
||||||
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"placeholder": "Customer had a bad shopping experience"
|
"placeholder": "Customer had a bad shopping experience"
|
||||||
|
|||||||
+6
-70
@@ -1,14 +1,7 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import { Button, CurrencyInput, Select, Textarea, toast } from "@medusajs/ui"
|
||||||
Button,
|
import { useMemo, useState } from "react"
|
||||||
CurrencyInput,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
Textarea,
|
|
||||||
toast,
|
|
||||||
} from "@medusajs/ui"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
|
||||||
import { formatValue } from "react-currency-input-field"
|
import { formatValue } from "react-currency-input-field"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -20,7 +13,6 @@ import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
|||||||
import { useRefundPayment, useRefundReasons } from "../../../../../hooks/api"
|
import { useRefundPayment, useRefundReasons } from "../../../../../hooks/api"
|
||||||
import { currencies } from "../../../../../lib/data/currencies"
|
import { currencies } from "../../../../../lib/data/currencies"
|
||||||
import { formatCurrency } from "../../../../../lib/format-currency"
|
import { formatCurrency } from "../../../../../lib/format-currency"
|
||||||
import { formatProvider } from "../../../../../lib/format-provider"
|
|
||||||
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
import { getLocaleAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
import { getPaymentsFromOrder } from "../../../../../lib/orders"
|
import { getPaymentsFromOrder } from "../../../../../lib/orders"
|
||||||
import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
|
import { useDocumentDirection } from "../../../../../hooks/use-document-direction"
|
||||||
@@ -63,29 +55,10 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
|
|||||||
value: paymentAmount.toFixed(currency.decimal_digits),
|
value: paymentAmount.toFixed(currency.decimal_digits),
|
||||||
float: paymentAmount,
|
float: paymentAmount,
|
||||||
},
|
},
|
||||||
note: "",
|
|
||||||
refund_reason_id: "",
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(CreateRefundSchema),
|
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 { mutateAsync, isPending } = useRefundPayment(order.id, payment?.id!)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
@@ -123,53 +96,16 @@ export const CreateRefundForm = ({ order }: CreateRefundFormProps) => {
|
|||||||
>
|
>
|
||||||
<RouteDrawer.Body className="flex-1 overflow-auto">
|
<RouteDrawer.Body className="flex-1 overflow-auto">
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<Select
|
<div className="flex items-center">
|
||||||
dir={direction}
|
|
||||||
value={paymentId}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setPaymentId(value)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Label className="txt-compact-small mb-[-6px] font-sans font-medium">
|
|
||||||
{t("orders.payment.selectPaymentToRefund")}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Select.Trigger>
|
|
||||||
<Select.Value
|
|
||||||
placeholder={t("orders.payment.selectPaymentToRefund")}
|
|
||||||
/>
|
|
||||||
</Select.Trigger>
|
|
||||||
|
|
||||||
<Select.Content>
|
|
||||||
{payments.map((payment) => {
|
|
||||||
const totalRefunded = payment.refunds.reduce(
|
|
||||||
(acc, next) => next.amount + acc,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select.Item
|
|
||||||
value={payment!.id}
|
|
||||||
key={payment.id}
|
|
||||||
disabled={
|
|
||||||
!!payment.canceled_at || totalRefunded >= payment.amount
|
|
||||||
}
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<span>
|
<span>
|
||||||
{getLocaleAmount(
|
{getLocaleAmount(
|
||||||
payment.amount as number,
|
payment.amount as number,
|
||||||
payment.currency_code
|
payment.currency_code
|
||||||
)}
|
)}
|
||||||
{" - "}
|
|
||||||
</span>
|
</span>
|
||||||
<span>{formatProvider(payment.provider_id)}</span>
|
<span> - </span>
|
||||||
<span> - (#{payment.id.substring(23)})</span>
|
<span>(#{payment.id.substring(23)})</span>
|
||||||
</Select.Item>
|
</div>
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Select.Content>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
+1
-5
@@ -853,10 +853,6 @@ const DiscountAndTotalBreakdown = ({
|
|||||||
.split("-")
|
.split("-")
|
||||||
.join(" ")
|
.join(" ")
|
||||||
|
|
||||||
const prettyReferenceId = creditLine.reference_id ? (
|
|
||||||
<DisplayId id={creditLine.reference_id} />
|
|
||||||
) : null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={creditLine.id}
|
key={creditLine.id}
|
||||||
@@ -899,7 +895,7 @@ const DiscountAndTotalBreakdown = ({
|
|||||||
leading="compact"
|
leading="compact"
|
||||||
className="txt-small text-ui-fg-subtle capitalize"
|
className="txt-small text-ui-fg-subtle capitalize"
|
||||||
>
|
>
|
||||||
({prettyReference} {prettyReferenceId})
|
({prettyReference})
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
|
|||||||
+46
@@ -13,6 +13,7 @@ import { useCreateRefundReason } from "../../../../../hooks/api"
|
|||||||
|
|
||||||
const RefundReasonCreateSchema = z.object({
|
const RefundReasonCreateSchema = z.object({
|
||||||
label: z.string().min(1),
|
label: z.string().min(1),
|
||||||
|
code: z.string().min(1),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -23,11 +24,20 @@ export const RefundReasonCreateForm = () => {
|
|||||||
const form = useForm<z.infer<typeof RefundReasonCreateSchema>>({
|
const form = useForm<z.infer<typeof RefundReasonCreateSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
label: "",
|
label: "",
|
||||||
|
code: "",
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RefundReasonCreateSchema),
|
resolver: zodResolver(RefundReasonCreateSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const generateCodeFromLabel = (label: string) => {
|
||||||
|
return label
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]/g, "_")
|
||||||
|
.replace(/_+/g, "_")
|
||||||
|
.replace(/^_|_$/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
const { mutateAsync, isPending } = useCreateRefundReason()
|
const { mutateAsync, isPending } = useCreateRefundReason()
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
@@ -81,6 +91,42 @@ export const RefundReasonCreateForm = () => {
|
|||||||
placeholder={t(
|
placeholder={t(
|
||||||
"refundReasons.fields.label.placeholder"
|
"refundReasons.fields.label.placeholder"
|
||||||
)}
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (
|
||||||
|
!form.getFieldState("code").isTouched ||
|
||||||
|
!form.getValues("code")
|
||||||
|
) {
|
||||||
|
form.setValue(
|
||||||
|
"code",
|
||||||
|
generateCodeFromLabel(e.target.value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
field.onChange(e)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("refundReasons.fields.code.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t(
|
||||||
|
"refundReasons.fields.code.placeholder"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.ErrorMessage />
|
<Form.ErrorMessage />
|
||||||
|
|||||||
+23
-1
@@ -8,7 +8,7 @@ import { z } from "zod"
|
|||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||||
import { useUpdateRefundReason } from "../../../../../hooks/api/refund-reasons"
|
import { useUpdateRefundReason } from "../../../../../hooks/api"
|
||||||
|
|
||||||
type RefundReasonEditFormProps = {
|
type RefundReasonEditFormProps = {
|
||||||
refundReason: HttpTypes.AdminRefundReason
|
refundReason: HttpTypes.AdminRefundReason
|
||||||
@@ -16,6 +16,7 @@ type RefundReasonEditFormProps = {
|
|||||||
|
|
||||||
const RefundReasonEditSchema = z.object({
|
const RefundReasonEditSchema = z.object({
|
||||||
label: z.string().min(1),
|
label: z.string().min(1),
|
||||||
|
code: z.string().min(1),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export const RefundReasonEditForm = ({
|
|||||||
const form = useForm<z.infer<typeof RefundReasonEditSchema>>({
|
const form = useForm<z.infer<typeof RefundReasonEditSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
label: refundReason.label,
|
label: refundReason.label,
|
||||||
|
code: refundReason.code,
|
||||||
description: refundReason.description ?? undefined,
|
description: refundReason.description ?? undefined,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RefundReasonEditSchema),
|
resolver: zodResolver(RefundReasonEditSchema),
|
||||||
@@ -78,6 +80,26 @@ export const RefundReasonEditForm = ({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Form.Field
|
||||||
|
control={form.control}
|
||||||
|
name="code"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<Form.Item>
|
||||||
|
<Form.Label>
|
||||||
|
{t("refundReasons.fields.code.label")}
|
||||||
|
</Form.Label>
|
||||||
|
<Form.Control>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder={t("refundReasons.fields.code.placeholder")}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
<Form.ErrorMessage />
|
||||||
|
</Form.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
+7
-14
@@ -1,15 +1,6 @@
|
|||||||
import type { BigNumberInput, OrderDTO } from "@medusajs/framework/types"
|
import type { BigNumberInput, OrderDTO } from "@medusajs/framework/types"
|
||||||
import {
|
import { ChangeActionType, OrderChangeStatus, OrderChangeType, } from "@medusajs/framework/utils"
|
||||||
ChangeActionType,
|
import { createStep, createWorkflow, transform, WorkflowData, } from "@medusajs/framework/workflows-sdk"
|
||||||
OrderChangeStatus,
|
|
||||||
OrderChangeType,
|
|
||||||
} from "@medusajs/framework/utils"
|
|
||||||
import {
|
|
||||||
WorkflowData,
|
|
||||||
createStep,
|
|
||||||
createWorkflow,
|
|
||||||
transform,
|
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
|
||||||
import { useQueryGraphStep } from "../../../common"
|
import { useQueryGraphStep } from "../../../common"
|
||||||
import { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
import { confirmOrderChanges } from "../../steps/confirm-order-changes"
|
||||||
import { createOrderChangeStep } from "../../steps/create-order-change"
|
import { createOrderChangeStep } from "../../steps/create-order-change"
|
||||||
@@ -36,8 +27,10 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow(
|
|||||||
function (
|
function (
|
||||||
input: WorkflowData<{
|
input: WorkflowData<{
|
||||||
order_id: string
|
order_id: string
|
||||||
|
amount: BigNumberInput,
|
||||||
|
reference?: string,
|
||||||
|
referenceId?: string
|
||||||
created_by?: string
|
created_by?: string
|
||||||
amount: BigNumberInput
|
|
||||||
}>
|
}>
|
||||||
) {
|
) {
|
||||||
const orderQuery = useQueryGraphStep({
|
const orderQuery = useQueryGraphStep({
|
||||||
@@ -69,8 +62,8 @@ export const createOrderRefundCreditLinesWorkflow = createWorkflow(
|
|||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
version: orderChange.version,
|
version: orderChange.version,
|
||||||
action: ChangeActionType.CREDIT_LINE_ADD,
|
action: ChangeActionType.CREDIT_LINE_ADD,
|
||||||
reference: "payment_collection",
|
reference: input.reference ?? "payment_collection",
|
||||||
reference_id: order.payment_collections[0]?.id,
|
reference_id: input.referenceId ?? order.payment_collections[0]?.id,
|
||||||
amount: input.amount,
|
amount: input.amount,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import {
|
import { CreateRefundReasonDTO, RefundReasonDTO, } from "@medusajs/framework/types"
|
||||||
CreateRefundReasonDTO,
|
import { createWorkflow, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk"
|
||||||
RefundReasonDTO,
|
|
||||||
} from "@medusajs/framework/types"
|
|
||||||
import {
|
|
||||||
WorkflowData,
|
|
||||||
WorkflowResponse,
|
|
||||||
createWorkflow,
|
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
|
||||||
import { createRefundReasonStep } from "../steps/create-refund-reasons"
|
import { createRefundReasonStep } from "../steps/create-refund-reasons"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +26,8 @@ export const createRefundReasonsWorkflowId = "create-refund-reasons-workflow"
|
|||||||
* input: {
|
* input: {
|
||||||
* data: [
|
* data: [
|
||||||
* {
|
* {
|
||||||
* label: "damaged",
|
* label: "Damaged",
|
||||||
|
* code: "damaged"
|
||||||
* }
|
* }
|
||||||
* ]
|
* ]
|
||||||
* }
|
* }
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import { BigNumberInput, IPaymentModuleService } from "@medusajs/framework/types"
|
||||||
BigNumberInput,
|
|
||||||
IPaymentModuleService,
|
|
||||||
} from "@medusajs/framework/types"
|
|
||||||
import { Modules } from "@medusajs/framework/utils"
|
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.
|
* The data to refund a payment.
|
||||||
|
|||||||
@@ -1,89 +1,10 @@
|
|||||||
import type {
|
import { BigNumberInput } from "@medusajs/framework/types"
|
||||||
BigNumberInput,
|
import { MathBN, PaymentEvents } from "@medusajs/framework/utils"
|
||||||
OrderDTO,
|
import { createWorkflow, transform, when, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk"
|
||||||
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 { emitEventStep, useRemoteQueryStep } from "../../common"
|
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||||
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
||||||
import { refundPaymentStep } from "../steps/refund-payment"
|
import { refundPaymentStep } from "../steps/refund-payment"
|
||||||
|
import { createOrderRefundCreditLinesWorkflow } from "../../order/workflows/payments/create-order-refund-credit-lines"
|
||||||
/**
|
|
||||||
* 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}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The data to refund a payment.
|
* 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.
|
* The amount to refund. If not provided, the full payment amount will be refunded.
|
||||||
*/
|
*/
|
||||||
amount?: BigNumberInput
|
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"
|
export const refundPaymentWorkflowId = "refund-payment-workflow"
|
||||||
@@ -156,7 +85,46 @@ export const refundPaymentWorkflow = createWorkflow(
|
|||||||
list: false,
|
list: false,
|
||||||
}).config({ name: "order" })
|
}).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)
|
const refundPayment = refundPaymentStep(input)
|
||||||
|
|
||||||
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
|
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ type AdminBaseRefundReasonPayload = {
|
|||||||
* "Refund"
|
* "Refund"
|
||||||
*/
|
*/
|
||||||
label: string
|
label: string
|
||||||
|
/**
|
||||||
|
* The refund reason's code.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "refund"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
/**
|
/**
|
||||||
* The refund reason's description.
|
* The refund reason's description.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ export interface BaseRefundReason {
|
|||||||
* "Refund"
|
* "Refund"
|
||||||
*/
|
*/
|
||||||
label: string
|
label: string
|
||||||
|
/**
|
||||||
|
* The refund reason's code.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* "refund"
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
/**
|
/**
|
||||||
* The refund reason's description.
|
* The refund reason's description.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { BigNumberInput } from "../totals"
|
import { BigNumberInput } from "../totals"
|
||||||
import { PaymentCollectionStatus, PaymentSessionStatus } from "./common"
|
import { PaymentCollectionStatus, PaymentSessionStatus } from "./common"
|
||||||
import {
|
import { PaymentAccountHolderDTO, PaymentCustomerDTO, PaymentProviderContext, } from "./provider"
|
||||||
PaymentAccountHolderDTO,
|
|
||||||
PaymentCustomerDTO,
|
|
||||||
PaymentProviderContext,
|
|
||||||
} from "./provider"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The payment collection to be created.
|
* The payment collection to be created.
|
||||||
@@ -356,6 +352,10 @@ export interface CreateRefundReasonDTO {
|
|||||||
* The label of the refund reason
|
* The label of the refund reason
|
||||||
*/
|
*/
|
||||||
label: string
|
label: string
|
||||||
|
/**
|
||||||
|
* The code of the refund reason
|
||||||
|
*/
|
||||||
|
code: string
|
||||||
/**
|
/**
|
||||||
* The description of the refund reason
|
* The description of the refund reason
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,20 +22,20 @@ import {
|
|||||||
RefundReasonDTO,
|
RefundReasonDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
|
CreateAccountHolderDTO,
|
||||||
CreateCaptureDTO,
|
CreateCaptureDTO,
|
||||||
CreatePaymentCollectionDTO,
|
CreatePaymentCollectionDTO,
|
||||||
|
CreatePaymentMethodDTO,
|
||||||
CreatePaymentSessionDTO,
|
CreatePaymentSessionDTO,
|
||||||
CreateRefundDTO,
|
CreateRefundDTO,
|
||||||
CreateRefundReasonDTO,
|
CreateRefundReasonDTO,
|
||||||
PaymentCollectionUpdatableFields,
|
PaymentCollectionUpdatableFields,
|
||||||
ProviderWebhookPayload,
|
ProviderWebhookPayload,
|
||||||
|
UpdateAccountHolderDTO,
|
||||||
UpdatePaymentDTO,
|
UpdatePaymentDTO,
|
||||||
UpdatePaymentSessionDTO,
|
UpdatePaymentSessionDTO,
|
||||||
UpdateRefundReasonDTO,
|
UpdateRefundReasonDTO,
|
||||||
CreateAccountHolderDTO,
|
|
||||||
UpsertPaymentCollectionDTO,
|
UpsertPaymentCollectionDTO,
|
||||||
CreatePaymentMethodDTO,
|
|
||||||
UpdateAccountHolderDTO,
|
|
||||||
} from "./mutations"
|
} from "./mutations"
|
||||||
import { WebhookActionResult } from "./provider"
|
import { WebhookActionResult } from "./provider"
|
||||||
|
|
||||||
@@ -1242,9 +1242,11 @@ export interface IPaymentModuleService extends IModuleService {
|
|||||||
* await paymentModuleService.createRefundReasons([
|
* await paymentModuleService.createRefundReasons([
|
||||||
* {
|
* {
|
||||||
* label: "Too big",
|
* 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 =
|
* const refundReason =
|
||||||
* await paymentModuleService.createRefundReasons({
|
* await paymentModuleService.createRefundReasons({
|
||||||
* label: "Too big",
|
* label: "Too big",
|
||||||
|
* code: "too_big"
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
createRefundReasons(
|
createRefundReasons(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const defaultAdminPaymentFields = [
|
|||||||
"refunds.note",
|
"refunds.note",
|
||||||
"refunds.payment_id",
|
"refunds.payment_id",
|
||||||
"refunds.refund_reason.label",
|
"refunds.refund_reason.label",
|
||||||
|
"refunds.refund_reason.code",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const listTransformQueryConfig = {
|
export const listTransformQueryConfig = {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const defaultAdminRefundReasonFields = [
|
export const defaultAdminRefundReasonFields = [
|
||||||
"id",
|
"id",
|
||||||
"label",
|
"label",
|
||||||
|
"code",
|
||||||
"description",
|
"description",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type AdminCreatePaymentRefundReasonType = z.infer<
|
|||||||
export const AdminCreatePaymentRefundReason = z
|
export const AdminCreatePaymentRefundReason = z
|
||||||
.object({
|
.object({
|
||||||
label: z.string(),
|
label: z.string(),
|
||||||
|
code: z.string(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
@@ -17,6 +18,7 @@ export type AdminUpdatePaymentRefundReasonType = z.infer<
|
|||||||
export const AdminUpdatePaymentRefundReason = z
|
export const AdminUpdatePaymentRefundReason = z
|
||||||
.object({
|
.object({
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
|
code: z.string().optional(),
|
||||||
description: z.string().nullish(),
|
description: z.string().nullish(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|||||||
@@ -30,10 +30,10 @@
|
|||||||
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
|
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
|
||||||
"test": "jest --bail --passWithNoTests --forceExit -- src",
|
"test": "jest --bail --passWithNoTests --forceExit -- src",
|
||||||
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts",
|
"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: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-orm migration:create",
|
"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-orm migration:up",
|
"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-orm cache:clear"
|
"orm:cache:clear": "MIKRO_ORM_CLI_CONFIG=./mikro-orm.config.dev.ts MIKRO_ORM_ALLOW_GLOBAL_CLI=true medusa-mikro-orm cache:clear"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/framework": "2.10.3",
|
"@medusajs/framework": "2.10.3",
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20250929124701 extends Migration {
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
// 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<void> {
|
||||||
|
// Remove the code column
|
||||||
|
this.addSql(`alter table "refund_reason" drop column "code";`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import Refund from "./refund"
|
|||||||
const RefundReason = model.define("RefundReason", {
|
const RefundReason = model.define("RefundReason", {
|
||||||
id: model.id({ prefix: "refr" }).primaryKey(),
|
id: model.id({ prefix: "refr" }).primaryKey(),
|
||||||
label: model.text().searchable(),
|
label: model.text().searchable(),
|
||||||
|
code: model.text().searchable(),
|
||||||
description: model.text().searchable().nullable(),
|
description: model.text().searchable().nullable(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
refunds: model.hasMany(() => Refund, {
|
refunds: model.hasMany(() => Refund, {
|
||||||
|
|||||||
Reference in new issue
Block a user