diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts
index 8d6de95a28..1bb66dd8f5 100644
--- a/integration-tests/http/__tests__/order/admin/order.spec.ts
+++ b/integration-tests/http/__tests__/order/admin/order.spec.ts
@@ -76,18 +76,41 @@ medusaIntegrationTestRunner({
version: 1,
change_type: "update_order",
status: "confirmed",
+ created_by: expect.any(String),
+ confirmed_by: expect.any(String),
confirmed_at: expect.any(String),
actions: expect.arrayContaining([
expect.objectContaining({
version: 1,
applied: true,
- reference_id: addressBefore.id,
- reference: "shipping_address",
action: "UPDATE_ORDER_PROPERTIES",
- details: {
- city: "New New York",
- address_1: "New Main street 123",
- },
+ details: expect.objectContaining({
+ type: "shipping_address",
+ old: expect.objectContaining({
+ address_1: addressBefore.address_1,
+ city: addressBefore.city,
+ country_code: addressBefore.country_code,
+ province: addressBefore.province,
+ postal_code: addressBefore.postal_code,
+ phone: addressBefore.phone,
+ company: addressBefore.company,
+ first_name: addressBefore.first_name,
+ last_name: addressBefore.last_name,
+ address_2: addressBefore.address_2,
+ }),
+ new: expect.objectContaining({
+ address_1: "New Main street 123",
+ city: "New New York",
+ country_code: addressBefore.country_code,
+ province: addressBefore.province,
+ postal_code: addressBefore.postal_code,
+ phone: addressBefore.phone,
+ company: addressBefore.company,
+ first_name: addressBefore.first_name,
+ last_name: addressBefore.last_name,
+ address_2: addressBefore.address_2,
+ }),
+ }),
}),
]),
})
@@ -162,18 +185,33 @@ medusaIntegrationTestRunner({
version: 1,
change_type: "update_order",
status: "confirmed",
+ created_by: expect.any(String),
+ confirmed_by: expect.any(String),
confirmed_at: expect.any(String),
actions: expect.arrayContaining([
expect.objectContaining({
version: 1,
applied: true,
- reference_id: addressBefore.id,
- reference: "billing_address",
action: "UPDATE_ORDER_PROPERTIES",
- details: {
- city: "New New York",
- address_1: "New Main street 123",
- },
+ details: expect.objectContaining({
+ type: "billing_address",
+ old: expect.objectContaining({
+ address_1: addressBefore.address_1,
+ city: addressBefore.city,
+ country_code: addressBefore.country_code,
+ province: addressBefore.province,
+ postal_code: addressBefore.postal_code,
+ phone: addressBefore.phone,
+ }),
+ new: expect.objectContaining({
+ address_1: "New Main street 123",
+ city: "New New York",
+ country_code: addressBefore.country_code,
+ province: addressBefore.province,
+ postal_code: addressBefore.postal_code,
+ phone: addressBefore.phone,
+ }),
+ }),
}),
]),
})
@@ -239,16 +277,23 @@ medusaIntegrationTestRunner({
change_type: "update_order",
status: "confirmed",
confirmed_at: expect.any(String),
+ created_by: expect.any(String),
+ confirmed_by: expect.any(String),
actions: expect.arrayContaining([
expect.objectContaining({
version: 1,
applied: true,
- reference_id: order.shipping_address.id,
- reference: "shipping_address",
action: "UPDATE_ORDER_PROPERTIES",
- details: {
- address_1: "New Main street 123",
- },
+ details: expect.objectContaining({
+ type: "shipping_address",
+ old: expect.objectContaining({
+ address_1: order.shipping_address.address_1,
+ city: order.shipping_address.city,
+ }),
+ new: expect.objectContaining({
+ address_1: "New Main street 123",
+ }),
+ }),
}),
]),
}),
@@ -257,16 +302,18 @@ medusaIntegrationTestRunner({
change_type: "update_order",
status: "confirmed",
confirmed_at: expect.any(String),
+ created_by: expect.any(String),
+ confirmed_by: expect.any(String),
actions: expect.arrayContaining([
expect.objectContaining({
version: 1,
applied: true,
- reference_id: order.email,
- reference: "email",
action: "UPDATE_ORDER_PROPERTIES",
- details: {
- email: "new-email@example.com",
- },
+ details: expect.objectContaining({
+ type: "email",
+ old: order.email,
+ new: "new-email@example.com",
+ }),
}),
]),
}),
diff --git a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx
index 1a23adcf61..239abb8b08 100644
--- a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx
+++ b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx
@@ -1,5 +1,6 @@
import { Avatar, Text } from "@medusajs/ui"
import { Link } from "react-router-dom"
+import { useUser } from "../../../hooks/api/users"
type UserLinkProps = {
id: string
@@ -32,3 +33,13 @@ export const UserLink = ({
)
}
+
+export const By = ({ id }: { id: string }) => {
+ const { user } = useUser(id) // todo: extend to support customers
+
+ if (!user) {
+ return null
+ }
+
+ return
+}
diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx
index 4e092a4b70..4440bb7942 100644
--- a/packages/admin/dashboard/src/hooks/api/orders.tsx
+++ b/packages/admin/dashboard/src/hooks/api/orders.tsx
@@ -51,6 +51,37 @@ export const useOrder = (
return { ...data, ...rest }
}
+export const useUpdateOrder = (
+ id: string,
+ options?: UseMutationOptions<
+ HttpTypes.AdminOrderResponse,
+ FetchError,
+ HttpTypes.AdminUpdateOrder
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload: HttpTypes.AdminUpdateOrder) =>
+ sdk.admin.order.update(id, payload),
+ onSuccess: (data: any, variables: any, context: any) => {
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.detail(id),
+ })
+
+ queryClient.invalidateQueries({
+ queryKey: ordersQueryKeys.changes(id),
+ })
+
+ // TODO: enable when needed
+ // queryClient.invalidateQueries({
+ // queryKey: ordersQueryKeys.lists(),
+ // })
+
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
+
export const useOrderPreview = (
id: string,
query?: HttpTypes.AdminOrderFilters,
diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json
index b4c6e4c51d..ff3a6d5af3 100644
--- a/packages/admin/dashboard/src/i18n/translations/$schema.json
+++ b/packages/admin/dashboard/src/i18n/translations/$schema.json
@@ -4146,6 +4146,65 @@
],
"additionalProperties": false
},
+ "edit": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "requestSuccess": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "title",
+ "requestSuccess"
+ ],
+ "additionalProperties": false
+ },
+ "shippingAddress": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "requestSuccess": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "title",
+ "requestSuccess"
+ ],
+ "additionalProperties": false
+ },
+ "billingAddress": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "requestSuccess": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "title",
+ "requestSuccess"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "required": [
+ "email",
+ "shippingAddress",
+ "billingAddress"
+ ],
+ "additionalProperties": false
+ },
"returns": {
"type": "object",
"properties": {
@@ -5364,6 +5423,26 @@
"declined"
],
"additionalProperties": false
+ },
+ "update_order": {
+ "type": "object",
+ "properties": {
+ "shipping_address": {
+ "type": "string"
+ },
+ "billing_address": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "shipping_address",
+ "billing_address",
+ "email"
+ ],
+ "additionalProperties": false
}
},
"required": [
@@ -5377,7 +5456,8 @@
"claim",
"exchange",
"edit",
- "transfer"
+ "transfer",
+ "update_order"
],
"additionalProperties": false
}
@@ -5426,6 +5506,7 @@
"transfer",
"payment",
"edits",
+ "edit",
"returns",
"claims",
"exchanges",
@@ -10849,6 +10930,12 @@
},
"removed": {
"type": "string"
+ },
+ "from": {
+ "type": "string"
+ },
+ "to": {
+ "type": "string"
}
},
"required": [
@@ -10857,7 +10944,9 @@
"available",
"inStock",
"added",
- "removed"
+ "removed",
+ "from",
+ "to"
],
"additionalProperties": false
},
@@ -10951,6 +11040,9 @@
"subtitle": {
"type": "string"
},
+ "by": {
+ "type": "string"
+ },
"item": {
"type": "string"
},
@@ -11360,6 +11452,7 @@
"discountable",
"handle",
"subtitle",
+ "by",
"item",
"qty",
"limit",
diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json
index feaf8f0381..5677801063 100644
--- a/packages/admin/dashboard/src/i18n/translations/en.json
+++ b/packages/admin/dashboard/src/i18n/translations/en.json
@@ -1002,6 +1002,20 @@
"quantityLowerThanFulfillment": "Cannot set quantity to be less then or equal to fulfilled quantity"
}
},
+ "edit": {
+ "email": {
+ "title": "Edit email",
+ "requestSuccess": "Order email updated to {{email}}."
+ },
+ "shippingAddress": {
+ "title": "Edit shipping address",
+ "requestSuccess": "Order shipping address updated."
+ },
+ "billingAddress": {
+ "title": "Edit billing address",
+ "requestSuccess": "Order billing address updated."
+ }
+ },
"returns": {
"create": "Create Return",
"confirm": "Confirm Return",
@@ -1301,6 +1315,11 @@
"requested": "Order transfer #{{transferId}} requested",
"confirmed": "Order transfer #{{transferId}} confirmed",
"declined": "Order transfer #{{transferId}} declined"
+ },
+ "update_order": {
+ "shipping_address": "Shipping address updated",
+ "billing_address": "Billing address updated",
+ "email": "Email updated"
}
}
},
@@ -2598,7 +2617,9 @@
"available": "Available",
"inStock": "In stock",
"added": "Added",
- "removed": "Removed"
+ "removed": "Removed",
+ "from": "From",
+ "to": "To"
},
"fields": {
"amount": "Amount",
@@ -2630,6 +2651,7 @@
"discountable": "Discountable",
"handle": "Handle",
"subtitle": "Subtitle",
+ "by": "By",
"item": "Item",
"qty": "qty.",
"limit": "Limit",
diff --git a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
index 2ee8c9d14f..e9fc3ca912 100644
--- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
+++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
@@ -329,6 +329,20 @@ export const RouteMap: RouteObject[] = [
lazy: () =>
import("../../routes/orders/order-request-transfer"),
},
+ {
+ path: "email",
+ lazy: () => import("../../routes/orders/order-edit-email"),
+ },
+ {
+ path: "shipping-address",
+ lazy: () =>
+ import("../../routes/orders/order-edit-shipping-address"),
+ },
+ {
+ path: "billing-address",
+ lazy: () =>
+ import("../../routes/orders/order-edit-billing-address"),
+ },
],
},
],
diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx
new file mode 100644
index 0000000000..25ac34b4e8
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx
@@ -0,0 +1,74 @@
+import { Popover, Text } from "@medusajs/ui"
+import { ReactNode, useState } from "react"
+import { useTranslation } from "react-i18next"
+
+type ChangeDetailsTooltipProps = {
+ previous: ReactNode
+ next: ReactNode
+ title: string
+}
+
+function ChangeDetailsTooltip(props: ChangeDetailsTooltipProps) {
+ const { t } = useTranslation()
+ const [open, setOpen] = useState(false)
+ const previous = props.previous
+ const next = props.next
+ const title = props.title
+
+ const handleMouseEnter = () => {
+ setOpen(true)
+ }
+
+ const handleMouseLeave = () => {
+ setOpen(false)
+ }
+
+ if (!previous && !next) {
+ return null
+ }
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+ {!!previous && (
+
+
+ {t("labels.from")}
+
+
+
{previous}
+
+ )}
+
+ {!!next && (
+
+
+ {t("labels.to")}
+
+
+
{next}
+
+ )}
+
+
+
+ )
+}
+
+export default ChangeDetailsTooltip
diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx
index 5542246d25..6563fb6b9b 100644
--- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx
+++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx
@@ -32,6 +32,9 @@ import { useDate } from "../../../../../hooks/use-date"
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
import { getPaymentsFromOrder } from "../order-payment-section"
import ActivityItems from "./activity-items"
+import { By, UserLink } from "../../../../../components/common/user-link"
+import ChangeDetailsTooltip from "./change-details-tooltip"
+import { getFormattedAddress } from "../../../../../lib/addresses"
type OrderTimelineProps = {
order: AdminOrder
@@ -42,6 +45,11 @@ type OrderTimelineProps = {
*/
const NOTE_LIMIT = 9999
+/**
+ * Order Changes that are not related to RMA flows
+ */
+const NON_RMA_CHANGE_TYPES = ["transfer", "update_order"]
+
export const OrderTimeline = ({ order }: OrderTimelineProps) => {
const items = useActivityItems(order)
@@ -118,10 +126,19 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
const { t } = useTranslation()
const { order_changes: orderChanges = [] } = useOrderChanges(order.id, {
- change_type: ["edit", "claim", "exchange", "return", "transfer"],
+ change_type: [
+ "edit",
+ "claim",
+ "exchange",
+ "return",
+ "transfer",
+ "update_order",
+ ],
})
- const rmaChanges = orderChanges.filter((oc) => oc.change_type !== "transfer")
+ const rmaChanges = orderChanges.filter(
+ (oc) => !NON_RMA_CHANGE_TYPES.includes(oc.change_type)
+ )
const missingLineItemIds = getMissingLineItemIds(order, rmaChanges)
const { order_items: removedLineItems = [] } = useOrderLineItems(
@@ -418,6 +435,74 @@ const useActivityItems = (order: AdminOrder): Activity[] => {
}
}
+ for (const update of orderChanges.filter(
+ (oc) => oc.change_type === "update_order"
+ )) {
+ const updateType = update.actions[0]?.details?.type
+
+ if (updateType === "shipping_address") {
+ items.push({
+ title: (
+
+ ),
+ timestamp: update.created_at,
+ children: (
+
+ {t("fields.by")}
+
+ ),
+ })
+ }
+
+ if (updateType === "billing_address") {
+ items.push({
+ title: (
+
+ ),
+ timestamp: update.created_at,
+ children: (
+
+ {t("fields.by")}
+
+ ),
+ })
+ }
+
+ if (updateType === "email") {
+ items.push({
+ title: (
+
+ ),
+ timestamp: update.created_at,
+ children: (
+
+ {t("fields.by")}
+
+ ),
+ })
+ }
+ }
+
// for (const note of notes || []) {
// items.push({
// title: t("orders.activity.events.note.comment"),
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx
new file mode 100644
index 0000000000..b20e97b395
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx
@@ -0,0 +1,216 @@
+import * as zod from "zod"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { HttpTypes } from "@medusajs/types"
+import { Button, Input, toast } from "@medusajs/ui"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+
+import { Form } from "../../../../../components/common/form"
+import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
+import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
+import { useUpdateOrder } from "../../../../../hooks/api/orders"
+import { CountrySelect } from "../../../../../components/inputs/country-select"
+
+type EditOrderBillingAddressFormProps = {
+ order: HttpTypes.AdminOrder
+}
+
+const EditOrderBillingAddressSchema = zod.object({
+ address_1: zod.string().min(1),
+ address_2: zod.string().optional(),
+ country_code: zod.string().min(2).max(2),
+ city: zod.string().optional(),
+ postal_code: zod.string().optional(),
+ province: zod.string().optional(),
+ company: zod.string().optional(),
+ phone: zod.string().optional(),
+})
+
+export function EditOrderBillingAddressForm({
+ order,
+}: EditOrderBillingAddressFormProps) {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ address_1: order.billing_address?.address_1 || "",
+ address_2: order.billing_address?.address_2 || "",
+ city: order.billing_address?.city || "",
+ company: order.billing_address?.company || "",
+ country_code: order.billing_address?.country_code || "",
+ phone: order.billing_address?.phone || "",
+ postal_code: order.billing_address?.postal_code || "",
+ province: order.billing_address?.province || "",
+ },
+ resolver: zodResolver(EditOrderBillingAddressSchema),
+ })
+
+ const { mutateAsync, isPending } = useUpdateOrder(order.id)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ try {
+ await mutateAsync({
+ billing_address: data,
+ })
+ toast.success(t("orders.edit.billingAddress.requestSuccess"))
+ handleSuccess()
+ } catch (error) {
+ toast.error((error as Error).message)
+ }
+ })
+
+ return (
+
+
+
+
+
{
+ return (
+
+ {t("fields.address")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.address2")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.postalCode")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.city")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.country")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.state")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.company")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.phone")}
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts
new file mode 100644
index 0000000000..1c1b9f5a33
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-order-billing-address-form"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts
new file mode 100644
index 0000000000..dab858990a
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts
@@ -0,0 +1 @@
+export { OrderEditBillingAddress as Component } from "./order-edit-billing-address"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx
new file mode 100644
index 0000000000..88327223bf
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx
@@ -0,0 +1,31 @@
+import { Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { useParams } from "react-router-dom"
+
+import { RouteDrawer } from "../../../components/modals"
+import { useOrder } from "../../../hooks/api"
+import { DEFAULT_FIELDS } from "../order-detail/constants"
+import { EditOrderBillingAddressForm } from "./components/edit-order-billing-address-form"
+
+export const OrderEditBillingAddress = () => {
+ const { t } = useTranslation()
+ const params = useParams()
+
+ const { order, isPending, isError, error } = useOrder(params.id!, {
+ fields: DEFAULT_FIELDS,
+ })
+
+ if (!isPending && isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("orders.edit.billingAddress.title")}
+
+
+ {order && }
+
+ )
+}
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx
new file mode 100644
index 0000000000..db86181971
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx
@@ -0,0 +1,95 @@
+import * as zod from "zod"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { HttpTypes } from "@medusajs/types"
+import { Button, Input, toast } from "@medusajs/ui"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+
+import { Form } from "../../../../../components/common/form"
+import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
+import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
+import { useUpdateOrder } from "../../../../../hooks/api/orders"
+
+type EditOrderEmailFormProps = {
+ order: HttpTypes.AdminOrder
+}
+
+const EditOrderEmailSchema = zod.object({
+ email: zod.string().email(),
+})
+
+export function EditOrderEmailForm({ order }: EditOrderEmailFormProps) {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ email: order.email || "",
+ },
+ resolver: zodResolver(EditOrderEmailSchema),
+ })
+
+ const { mutateAsync, isPending } = useUpdateOrder(order.id)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ try {
+ await mutateAsync({
+ email: data.email,
+ })
+ toast.success(
+ t("orders.edit.email.requestSuccess", { email: data.email })
+ )
+ handleSuccess()
+ } catch (error) {
+ toast.error((error as Error).message)
+ }
+ })
+
+ return (
+
+
+
+ {
+ return (
+
+ {t("fields.email")}
+
+
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts
new file mode 100644
index 0000000000..55443133a7
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-order-email-form"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts
new file mode 100644
index 0000000000..014a490fc1
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts
@@ -0,0 +1 @@
+export { OrderEditEmail as Component } from "./order-edit-email"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx
new file mode 100644
index 0000000000..3411a91ac7
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx
@@ -0,0 +1,31 @@
+import { Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { useParams } from "react-router-dom"
+
+import { RouteDrawer } from "../../../components/modals"
+import { useOrder } from "../../../hooks/api"
+import { DEFAULT_FIELDS } from "../order-detail/constants"
+import { EditOrderEmailForm } from "./components/edit-order-email-form"
+
+export const OrderEditEmail = () => {
+ const { t } = useTranslation()
+ const params = useParams()
+
+ const { order, isPending, isError, error } = useOrder(params.id!, {
+ fields: DEFAULT_FIELDS,
+ })
+
+ if (!isPending && isError) {
+ throw error
+ }
+
+ return (
+
+
+ {t("orders.edit.email.title")}
+
+
+ {order && }
+
+ )
+}
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx
new file mode 100644
index 0000000000..f6be220cb4
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx
@@ -0,0 +1,215 @@
+import * as zod from "zod"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { HttpTypes } from "@medusajs/types"
+import { Button, Input, toast } from "@medusajs/ui"
+import { useForm } from "react-hook-form"
+import { useTranslation } from "react-i18next"
+
+import { Form } from "../../../../../components/common/form"
+import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
+import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
+import { useUpdateOrder } from "../../../../../hooks/api/orders"
+import { CountrySelect } from "../../../../../components/inputs/country-select"
+
+type EditOrderShippingAddressFormProps = {
+ order: HttpTypes.AdminOrder
+}
+
+const EditOrderShippingAddressSchema = zod.object({
+ address_1: zod.string().min(1),
+ address_2: zod.string().optional(),
+ country_code: zod.string().min(2).max(2),
+ city: zod.string().optional(),
+ postal_code: zod.string().optional(),
+ province: zod.string().optional(),
+ company: zod.string().optional(),
+ phone: zod.string().optional(),
+})
+
+export function EditOrderShippingAddressForm({
+ order,
+}: EditOrderShippingAddressFormProps) {
+ const { t } = useTranslation()
+ const { handleSuccess } = useRouteModal()
+
+ const form = useForm>({
+ defaultValues: {
+ address_1: order.shipping_address?.address_1 || "",
+ address_2: order.shipping_address?.address_2 || "",
+ city: order.shipping_address?.city || "",
+ company: order.shipping_address?.company || "",
+ country_code: order.shipping_address?.country_code || "",
+ phone: order.shipping_address?.phone || "",
+ postal_code: order.shipping_address?.postal_code || "",
+ province: order.shipping_address?.province || "",
+ },
+ resolver: zodResolver(EditOrderShippingAddressSchema),
+ })
+
+ const { mutateAsync, isPending } = useUpdateOrder(order.id)
+
+ const handleSubmit = form.handleSubmit(async (data) => {
+ try {
+ await mutateAsync({
+ shipping_address: data,
+ })
+ toast.success(t("orders.edit.shippingAddress.requestSuccess"))
+ handleSuccess()
+ } catch (error) {
+ toast.error((error as Error).message)
+ }
+ })
+
+ return (
+
+
+
+
+
{
+ return (
+
+ {t("fields.address")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.address2")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.postalCode")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.city")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.country")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.state")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.company")}
+
+
+
+
+
+ )
+ }}
+ />
+ {
+ return (
+
+ {t("fields.phone")}
+
+
+
+
+
+ )
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts
new file mode 100644
index 0000000000..b317ef3184
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts
@@ -0,0 +1 @@
+export * from "./edit-order-shipping-address-form"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts
new file mode 100644
index 0000000000..0dca73c20e
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts
@@ -0,0 +1 @@
+export { OrderEditShippingAddress as Component } from "./order-edit-shipping-address"
diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx
new file mode 100644
index 0000000000..dbb982a073
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx
@@ -0,0 +1,31 @@
+import { Heading } from "@medusajs/ui"
+import { useTranslation } from "react-i18next"
+import { useParams } from "react-router-dom"
+
+import { RouteDrawer } from "../../../components/modals"
+import { useOrder } from "../../../hooks/api"
+import { DEFAULT_FIELDS } from "../order-detail/constants"
+import { EditOrderShippingAddressForm } from "./components/edit-order-shipping-address-form"
+
+export const OrderEditShippingAddress = () => {
+ const { t } = useTranslation()
+ const params = useParams()
+
+ const { order, isPending, isError } = useOrder(params.id!, {
+ fields: DEFAULT_FIELDS,
+ })
+
+ if (!isPending && isError) {
+ throw new Error("Order not found")
+ }
+
+ return (
+
+
+ {t("orders.edit.shippingAddress.title")}
+
+
+ {order && }
+
+ )
+}
diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts
index 54a265cebc..b78ac81e9d 100644
--- a/packages/core/core-flows/src/order/workflows/update-order.ts
+++ b/packages/core/core-flows/src/order/workflows/update-order.ts
@@ -121,45 +121,62 @@ export const updateOrderWorkflow = createWorkflow(
return { ...input, ...update }
})
- updateOrdersStep({
+ const updatedOrders = updateOrdersStep({
selector: { id: input.id },
update: updateInput,
})
- const orderChangeInput = transform({ input, order }, ({ input, order }) => {
- const changes: RegisterOrderChangeDTO[] = []
- if (input.shipping_address) {
- changes.push({
- change_type: "update_order" as const,
- order_id: input.id,
- reference: "shipping_address",
- reference_id: order.shipping_address?.id, // save previous address id as reference
- details: input.shipping_address as Record, // save what changed on the address
- })
- }
+ const orderChangeInput = transform(
+ { input, updatedOrders, order },
+ ({ input, updatedOrders, order }) => {
+ const updatedOrder = updatedOrders[0]
- if (input.billing_address) {
- changes.push({
- change_type: "update_order" as const,
- order_id: input.id,
- reference: "billing_address",
- reference_id: order.billing_address?.id,
- details: input.billing_address as Record,
- })
- }
+ const changes: RegisterOrderChangeDTO[] = []
+ if (input.shipping_address) {
+ changes.push({
+ change_type: "update_order" as const,
+ order_id: input.id,
+ created_by: input.user_id,
+ confirmed_by: input.user_id,
+ details: {
+ type: "shipping_address",
+ old: order.shipping_address,
+ new: updatedOrder.shipping_address,
+ },
+ })
+ }
- if (input.email) {
- changes.push({
- change_type: "update_order" as const,
- order_id: input.id,
- reference: "email",
- reference_id: order.email,
- details: { email: input.email },
- })
- }
+ if (input.billing_address) {
+ changes.push({
+ change_type: "update_order" as const,
+ order_id: input.id,
+ created_by: input.user_id,
+ confirmed_by: input.user_id,
+ details: {
+ type: "billing_address",
+ old: order.billing_address,
+ new: updatedOrder.billing_address,
+ },
+ })
+ }
- return changes
- })
+ if (input.email) {
+ changes.push({
+ change_type: "update_order" as const,
+ order_id: input.id,
+ created_by: input.user_id,
+ confirmed_by: input.user_id,
+ details: {
+ type: "email",
+ old: order.email,
+ new: input.email,
+ },
+ })
+ }
+
+ return changes
+ }
+ )
registerOrderChangesStep(orderChangeInput)
diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts
index 550126a5ee..096a31ef49 100644
--- a/packages/core/js-sdk/src/admin/order.ts
+++ b/packages/core/js-sdk/src/admin/order.ts
@@ -64,6 +64,47 @@ export class Order {
)
}
+ /**
+ * This method updates an order. It sends a request to the
+ * [Update Order Email](https://docs.medusajs.com/api/admin#orders_postordersid)
+ * API route.
+ *
+ * @param id - The order's ID.
+ * @param body - The update details.
+ * @param headers - Headers to pass in the request
+ * @returns The order's details.
+ *
+ * @example
+ * sdk.admin.order.update(
+ * "order_123",
+ * {
+ * email: "new_email@example.com",
+ * shipping_address: {
+ * first_name: "John",
+ * last_name: "Doe",
+ * address_1: "123 Main St",
+ * }
+ * }
+ * )
+ * .then(({ order }) => {
+ * console.log(order)
+ * })
+ */
+ async update(
+ id: string,
+ body: HttpTypes.AdminUpdateOrder,
+ headers?: ClientHeaders
+ ) {
+ return await this.client.fetch(
+ `/admin/orders/${id}`,
+ {
+ method: "POST",
+ headers,
+ body,
+ }
+ )
+ }
+
/**
* This method retrieves the preview of an order based on its last associated change. It sends a request to the
* [Get Order Preview](https://docs.medusajs.com/api/admin#orders_getordersidpreview) API route.
diff --git a/packages/core/types/src/http/order/admin/payload.ts b/packages/core/types/src/http/order/admin/payload.ts
index e46edf2c7a..d0f601318a 100644
--- a/packages/core/types/src/http/order/admin/payload.ts
+++ b/packages/core/types/src/http/order/admin/payload.ts
@@ -1,3 +1,18 @@
+export interface AdminUpdateOrder {
+ /**
+ * The order's email.
+ */
+ email?: string
+ /**
+ * The order's shipping address.
+ */
+ shipping_address?: OrderAddress
+ /**
+ * The order's billing address.
+ */
+ billing_address?: OrderAddress
+}
+
export interface AdminCreateOrderFulfillment {
/**
* The items to add to the fulfillment.
@@ -82,3 +97,60 @@ export interface AdminRequestOrderTransfer {
internal_note?: string
description?: string
}
+
+export interface OrderAddress {
+ /**
+ * The first name of the address.
+ */
+ first_name?: string
+
+ /**
+ * The last name of the address.
+ */
+ last_name?: string
+
+ /**
+ * The phone number of the address.
+ */
+ phone?: string
+
+ /**
+ * The company of the address.
+ */
+ company?: string
+
+ /**
+ * The first address line of the address.
+ */
+ address_1?: string
+
+ /**
+ * The second address line of the address.
+ */
+ address_2?: string
+
+ /**
+ * The city of the address.
+ */
+ city?: string
+
+ /**
+ * The country code of the address.
+ */
+ country_code?: string
+
+ /**
+ * The province/state of the address.
+ */
+ province?: string
+
+ /**
+ * The postal code of the address.
+ */
+ postal_code?: string
+
+ /**
+ * Holds custom data in key-value pairs.
+ */
+ metadata?: Record | null
+}
diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts
index 358aefa56b..f8c6b6bd2a 100644
--- a/packages/core/types/src/order/mutations.ts
+++ b/packages/core/types/src/order/mutations.ts
@@ -1077,9 +1077,14 @@ export interface RegisterOrderChangeDTO {
internal_note?: string | null
/**
- * The user or customer that requested the order change.
+ * The user that created the order change.
*/
- requested_by?: string
+ created_by?: string
+
+ /**
+ * The user or customer that confirmed the order change.
+ */
+ confirmed_by?: string
/**
* Holds custom data in key-value pairs.
diff --git a/packages/core/types/src/workflow/order/update-order.ts b/packages/core/types/src/workflow/order/update-order.ts
index 32cdd47bc2..e9c0b0357f 100644
--- a/packages/core/types/src/workflow/order/update-order.ts
+++ b/packages/core/types/src/workflow/order/update-order.ts
@@ -2,6 +2,7 @@ import { UpsertOrderAddressDTO } from "../../order"
export type UpdateOrderWorkflowInput = {
id: string
+ user_id: string
shipping_address?: UpsertOrderAddressDTO
billing_address?: UpsertOrderAddressDTO
email?: string
diff --git a/packages/medusa/src/api/admin/orders/[id]/route.ts b/packages/medusa/src/api/admin/orders/[id]/route.ts
index 715b097197..4fce9e3f29 100644
--- a/packages/medusa/src/api/admin/orders/[id]/route.ts
+++ b/packages/medusa/src/api/admin/orders/[id]/route.ts
@@ -38,6 +38,7 @@ export const POST = async (
await updateOrderWorkflow(req.scope).run({
input: {
...req.validatedBody,
+ user_id: req.auth_context.actor_id,
id: req.params.id,
},
})
diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts
index 149d4e77e2..3a15c3cd7f 100644
--- a/packages/modules/order/src/services/order-module-service.ts
+++ b/packages/modules/order/src/services/order-module-service.ts
@@ -2268,14 +2268,14 @@ export default class OrderModuleService<
description: d.description,
metadata: d.metadata,
confirmed_at: new Date(),
+ created_by: d.created_by,
+ confirmed_by: d.confirmed_by,
status: OrderChangeStatus.CONFIRMED,
version: orderVersionsMap.get(d.order_id)!,
actions: [
{
action: ChangeActionType.UPDATE_ORDER_PROPERTIES,
details: d.details,
- reference: d.reference,
- reference_id: d.reference_id,
version: orderVersionsMap.get(d.order_id)!,
applied: true,
},