feat(core-flows,dashboard): add fixes to allow only outbound or inbound claims (#8590)
what: - allows completing claim with only inbound items - allows completing claim with only outbound items - validates against creating claims when an active change order is present
This commit is contained in:
@@ -850,8 +850,9 @@
|
||||
"sendNotification": "Send notification",
|
||||
"sendNotificationHint": "Notify customer about return.",
|
||||
"returnTotal": "Return total",
|
||||
"inboundTotal": "Inbound total",
|
||||
"refundAmount": "Refund amount",
|
||||
"outstandingAmount": "Outstanding amount",
|
||||
"outstandingAmount": "Difference amount",
|
||||
"reason": "Reason",
|
||||
"reasonHint": "Choose why the customer want to return items.",
|
||||
"note": "Note",
|
||||
@@ -886,7 +887,10 @@
|
||||
},
|
||||
"claims": {
|
||||
"create": "Create Claim",
|
||||
"manage": "Manage Claim",
|
||||
"outbound": "Outbound",
|
||||
"outboundItemAdded": "{{itemsCount}}x added through claim",
|
||||
"outboundTotal": "Outbound total",
|
||||
"outboundShipping": "Outbound shipping",
|
||||
"outboundShippingHint": "Choose which method you want to use.",
|
||||
"refundAmount": "Estimated difference",
|
||||
@@ -2369,8 +2373,8 @@
|
||||
"variants": "Variants",
|
||||
"orders": "Orders",
|
||||
"account": "Account",
|
||||
"total": "Total",
|
||||
"paidTotal": "Paid total",
|
||||
"total": "Order Total",
|
||||
"paidTotal": "Total captured",
|
||||
"totalExclTax": "Total excl. tax",
|
||||
"subtotal": "Subtotal",
|
||||
"shipping": "Shipping",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AdminPaymentCollection } from "@medusajs/types"
|
||||
|
||||
export const getTotalCaptured = (
|
||||
paymentCollections: AdminPaymentCollection[]
|
||||
) =>
|
||||
paymentCollections.reduce((acc, paymentCollection) => {
|
||||
acc =
|
||||
acc +
|
||||
((paymentCollection.captured_amount as number) -
|
||||
(paymentCollection.refunded_amount as number))
|
||||
return acc
|
||||
}, 0)
|
||||
+53
-5
@@ -232,6 +232,12 @@ export const ClaimCreateForm = ({
|
||||
)
|
||||
)
|
||||
|
||||
const outboundShipping = preview.shipping_methods.find((s) => {
|
||||
const action = s.actions?.find((a) => a.action === "SHIPPING_ADD")
|
||||
|
||||
return action && !!!action?.return?.id
|
||||
})
|
||||
|
||||
const {
|
||||
fields: inboundItems,
|
||||
append,
|
||||
@@ -456,7 +462,6 @@ export const ClaimCreateForm = ({
|
||||
}, [preview.shipping_methods])
|
||||
|
||||
const returnTotal = preview.return_requested_total
|
||||
const refundAmount = returnTotal - shippingTotal
|
||||
|
||||
return (
|
||||
<RouteFocusModal.Form form={form}>
|
||||
@@ -672,20 +677,49 @@ export const ClaimCreateForm = ({
|
||||
<div className="mt-8 border-y border-dotted py-4">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.returns.returnTotal")}
|
||||
{t("orders.returns.inboundTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
returnTotal ? -1 * returnTotal : returnTotal,
|
||||
inboundPreviewItems.reduce((acc, item) => {
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "RETURN_ITEM"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
|
||||
return acc
|
||||
}, 0) * -1,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.claims.outboundTotal")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{getStylizedAmount(
|
||||
outboundPreviewItems.reduce((acc, item) => {
|
||||
const action = item.actions?.find(
|
||||
(act) => act.action === "ITEM_ADD"
|
||||
)
|
||||
acc = acc + (action?.amount || 0)
|
||||
|
||||
return acc
|
||||
}, 0),
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.returns.inboundShipping")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle flex items-center">
|
||||
{!isShippingPriceEdit && (
|
||||
<IconButton
|
||||
@@ -699,6 +733,7 @@ export const ClaimCreateForm = ({
|
||||
<PencilSquare />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{isShippingPriceEdit ? (
|
||||
<CurrencyInput
|
||||
id="js-shipping-input"
|
||||
@@ -750,13 +785,26 @@ export const ClaimCreateForm = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="txt-small text-ui-fg-subtle">
|
||||
{t("orders.claims.outboundShipping")}
|
||||
</span>
|
||||
|
||||
<span className="txt-small text-ui-fg-subtle flex items-center">
|
||||
{getStylizedAmount(
|
||||
outboundShipping?.amount ?? 0,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between border-t border-dotted pt-4">
|
||||
<span className="txt-small font-medium">
|
||||
{t("orders.claims.refundAmount")}
|
||||
</span>
|
||||
<span className="txt-small font-medium">
|
||||
{getStylizedAmount(
|
||||
refundAmount ? -1 * refundAmount : refundAmount,
|
||||
preview.summary.pending_difference,
|
||||
order.currency_code
|
||||
)}
|
||||
</span>
|
||||
|
||||
+13
-5
@@ -169,12 +169,12 @@ export const ClaimOutboundSection = ({
|
||||
))
|
||||
|
||||
for (const itemToRemove of itemsToRemove) {
|
||||
const actionId = previewOutboundItems
|
||||
const action = previewOutboundItems
|
||||
.find((i) => i.variant_id === itemToRemove)
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")?.id
|
||||
?.actions?.find((a) => a.action === "ITEM_ADD")
|
||||
|
||||
if (actionId) {
|
||||
await removeOutboundItem(actionId, {
|
||||
if (action?.id) {
|
||||
await removeOutboundItem(action?.id, {
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
@@ -194,7 +194,15 @@ export const ClaimOutboundSection = ({
|
||||
|
||||
const promises = outboundShippingMethods
|
||||
.filter(Boolean)
|
||||
.map((action) => deleteOutboundShipping(action.id))
|
||||
.map((outboundShippingMethod) => {
|
||||
const action = outboundShippingMethod.actions?.find(
|
||||
(a) => a.action === "SHIPPING_ADD"
|
||||
)
|
||||
|
||||
if (action) {
|
||||
deleteOutboundShipping(action.id)
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
|
||||
+9
-14
@@ -3,7 +3,7 @@ import {
|
||||
Payment as MedusaPayment,
|
||||
Refund as MedusaRefund,
|
||||
} from "@medusajs/medusa"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { AdminPaymentCollection, HttpTypes } from "@medusajs/types"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
getStylizedAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
|
||||
import { getTotalCaptured } from "../../../../../lib/payment"
|
||||
|
||||
type OrderPaymentSectionProps = {
|
||||
order: HttpTypes.AdminOrder
|
||||
@@ -56,7 +57,10 @@ export const OrderPaymentSection = ({ order }: OrderPaymentSectionProps) => {
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
|
||||
<Total payments={payments} currencyCode={order.currency_code} />
|
||||
<Total
|
||||
paymentCollections={order.payment_collections}
|
||||
currencyCode={order.currency_code}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -310,30 +314,21 @@ const PaymentBreakdown = ({
|
||||
}
|
||||
|
||||
const Total = ({
|
||||
payments,
|
||||
paymentCollections,
|
||||
currencyCode,
|
||||
}: {
|
||||
payments: MedusaPayment[]
|
||||
paymentCollections: AdminPaymentCollection[]
|
||||
currencyCode: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const refunds = payments.map((payment) => payment.refunds).flat(1)
|
||||
const paid = payments.reduce((acc, payment) => acc + payment.amount, 0)
|
||||
const refunded = refunds.reduce(
|
||||
(acc, refund) => acc + (refund.amount || 0),
|
||||
0
|
||||
)
|
||||
|
||||
const total = paid - refunded
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{t("orders.payment.totalPaidByCustomer")}
|
||||
</Text>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{getStylizedAmount(total, currencyCode)}
|
||||
{getStylizedAmount(getTotalCaptured(paymentCollections), currencyCode)}
|
||||
</Text>
|
||||
</div>
|
||||
)
|
||||
|
||||
+149
-42
@@ -1,38 +1,46 @@
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import {
|
||||
ArrowDownRightMini,
|
||||
ArrowLongRight,
|
||||
ArrowUturnLeft,
|
||||
DocumentText,
|
||||
ExclamationCircle,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
AdminClaim,
|
||||
AdminOrder,
|
||||
AdminOrderPreview,
|
||||
AdminReturn,
|
||||
OrderLineItemDTO,
|
||||
ReservationItemDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ArrowDownRightMini,
|
||||
ArrowUturnLeft,
|
||||
ExclamationCircle,
|
||||
ArrowLongRight,
|
||||
} from "@medusajs/icons"
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Container,
|
||||
Copy,
|
||||
Heading,
|
||||
StatusBadge,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@medusajs/ui"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||
import { useClaims } from "../../../../../hooks/api/claims.tsx"
|
||||
import { useOrderPreview } from "../../../../../hooks/api/orders.tsx"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReturns } from "../../../../../hooks/api/returns"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import {
|
||||
getLocaleAmount,
|
||||
getStylizedAmount,
|
||||
} from "../../../../../lib/money-amount-helpers"
|
||||
import { useReservationItems } from "../../../../../hooks/api/reservations"
|
||||
import { useReturns } from "../../../../../hooks/api/returns"
|
||||
import { useDate } from "../../../../../hooks/use-date"
|
||||
import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx"
|
||||
import { getTotalCaptured } from "../../../../../lib/payment.ts"
|
||||
|
||||
type OrderSummarySectionProps = {
|
||||
order: AdminOrder
|
||||
@@ -46,6 +54,8 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
line_item_id: order.items.map((i) => i.id),
|
||||
})
|
||||
|
||||
const { order: orderPreview } = useOrderPreview(order.id!)
|
||||
|
||||
const { returns = [] } = useReturns({
|
||||
status: "requested",
|
||||
order_id: order.id,
|
||||
@@ -84,7 +94,7 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
|
||||
return (
|
||||
<Container className="divide-y divide-dashed p-0">
|
||||
<Header order={order} />
|
||||
<Header order={order} orderPreview={orderPreview} />
|
||||
<ItemBreakdown order={order} />
|
||||
<CostBreakdown order={order} />
|
||||
<Total order={order} />
|
||||
@@ -125,7 +135,13 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ order }: { order: AdminOrder }) => {
|
||||
const Header = ({
|
||||
order,
|
||||
orderPreview,
|
||||
}: {
|
||||
order: AdminOrder
|
||||
orderPreview?: AdminOrderPreview
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
@@ -151,7 +167,9 @@ const Header = ({ order }: { order: AdminOrder }) => {
|
||||
icon: <ArrowUturnLeft />,
|
||||
},
|
||||
{
|
||||
label: t("orders.claims.create"),
|
||||
label: orderPreview?.order_change?.id
|
||||
? t("orders.claims.manage")
|
||||
: t("orders.claims.create"),
|
||||
to: `/orders/${order.id}/claims`,
|
||||
icon: <ExclamationCircle />,
|
||||
},
|
||||
@@ -168,11 +186,13 @@ const Item = ({
|
||||
currencyCode,
|
||||
reservation,
|
||||
returns,
|
||||
claims,
|
||||
}: {
|
||||
item: OrderLineItemDTO
|
||||
currencyCode: string
|
||||
reservation?: ReservationItemDTO | null
|
||||
returns: AdminReturn[]
|
||||
claims: AdminClaim[]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const isInventoryManaged = item.variant.manage_inventory
|
||||
@@ -240,6 +260,10 @@ const Item = ({
|
||||
{returns.map((r) => (
|
||||
<ReturnBreakdown key={r.id} orderReturn={r} itemId={item.id} />
|
||||
))}
|
||||
|
||||
{claims.map((claim) => (
|
||||
<ClaimBreakdown key={claim.id} claim={claim} itemId={item.id} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -249,9 +273,14 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
line_item_id: order.items.map((i) => i.id),
|
||||
})
|
||||
|
||||
const { claims } = useClaims({
|
||||
order_id: order.id,
|
||||
fields: "*additional_items",
|
||||
})
|
||||
|
||||
const { returns } = useReturns({
|
||||
order_id: order.id,
|
||||
fields: "*items",
|
||||
fields: "*items,*items.reason",
|
||||
})
|
||||
|
||||
const itemsReturnsMap = useMemo(() => {
|
||||
@@ -290,6 +319,7 @@ const ItemBreakdown = ({ order }: { order: AdminOrder }) => {
|
||||
currencyCode={order.currency_code}
|
||||
reservation={reservation}
|
||||
returns={itemsReturnsMap[item.id] || []}
|
||||
claims={claims || []}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -363,43 +393,100 @@ const ReturnBreakdown = ({
|
||||
|
||||
if (
|
||||
!["requested", "received", "partially_received"].includes(
|
||||
orderReturn.status
|
||||
orderReturn.status || ""
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isRequested = orderReturn.status === "requested"
|
||||
const item = orderReturn?.items?.find((ri) => ri.item_id === itemId)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={orderReturn.id}
|
||||
className="text-ui-fg-subtle bg-ui-bg-subtle flex flex-row justify-between gap-y-2 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t(
|
||||
`orders.returns.${
|
||||
isRequested ? "returnRequestedInfo" : "returnReceivedInfo"
|
||||
}`,
|
||||
{
|
||||
requestedItemsCount: orderReturn.items.find(
|
||||
(ri) => ri.item_id === itemId
|
||||
)[isRequested ? "quantity" : "received_quantity"],
|
||||
}
|
||||
item && (
|
||||
<div
|
||||
key={orderReturn.id}
|
||||
className="txt-compact-small-plus text-ui-fg-subtle bg-ui-bg-subtle border-dotted border-t-2 border-b-2 flex flex-row justify-between gap-y-2 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||
<Text>
|
||||
{t(
|
||||
`orders.returns.${
|
||||
isRequested ? "returnRequestedInfo" : "returnReceivedInfo"
|
||||
}`,
|
||||
{
|
||||
requestedItemsCount:
|
||||
item?.[isRequested ? "quantity" : "received_quantity"],
|
||||
}
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{item?.note && (
|
||||
<Tooltip content={item.note}>
|
||||
<DocumentText className="text-ui-tag-neutral-icon inline ml-1" />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{item?.reason && (
|
||||
<Badge
|
||||
size="2xsmall"
|
||||
className="cursor-default select-none capitalize"
|
||||
rounded="full"
|
||||
>
|
||||
{item?.reason?.label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{orderReturn && (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{getRelativeDate(
|
||||
isRequested ? orderReturn.created_at : orderReturn.received_at
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const ClaimBreakdown = ({
|
||||
claim,
|
||||
itemId,
|
||||
}: {
|
||||
claim: AdminClaim
|
||||
itemId: string
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { getRelativeDate } = useDate()
|
||||
const items = claim.additional_items.filter(
|
||||
(item) => item.item?.id === itemId
|
||||
)
|
||||
|
||||
return (
|
||||
!!items.length && (
|
||||
<div
|
||||
key={claim.id}
|
||||
className="txt-compact-small-plus text-ui-fg-subtle bg-ui-bg-subtle border-dotted border-t-2 border-b-2 flex flex-row justify-between gap-y-2 px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownRightMini className="text-ui-fg-muted" />
|
||||
<Text>
|
||||
{t(`orders.claims.outboundItemAdded`, {
|
||||
itemsCount: items.reduce(
|
||||
(acc, item) => (acc = acc + item.quantity),
|
||||
0
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{getRelativeDate(claim.created_at)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{isRequested && (
|
||||
<Text size="small" leading="compact" className="text-ui-fg-muted">
|
||||
{getRelativeDate(
|
||||
isRequested ? orderReturn.created_at : orderReturn.received_at
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -416,12 +503,32 @@ const Total = ({ order }: { order: AdminOrder }) => {
|
||||
{getStylizedAmount(order.total, order.currency_code)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-base flex items-center justify-between">
|
||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||
{t("fields.paidTotal")}
|
||||
</Text>
|
||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||
{/*TODO*/}-
|
||||
{getStylizedAmount(
|
||||
getTotalCaptured(order.payment_collections || []),
|
||||
order.currency_code
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="text-ui-fg-base flex items-center justify-between">
|
||||
<Text
|
||||
className="text-ui-fg-subtle text-semibold"
|
||||
size="small"
|
||||
leading="compact"
|
||||
>
|
||||
{t("orders.returns.outstandingAmount")}
|
||||
</Text>
|
||||
<Text className="text-ui-fg-subtle" size="small" leading="compact">
|
||||
{getStylizedAmount(
|
||||
order.summary.difference_sum || 0,
|
||||
order.currency_code
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ const DEFAULT_RELATIONS = [
|
||||
"*items.variant.product",
|
||||
"*items.variant.options",
|
||||
"+items.variant.manage_inventory",
|
||||
"+summary",
|
||||
"*shipping_address",
|
||||
"*billing_address",
|
||||
"*sales_channel",
|
||||
|
||||
Reference in New Issue
Block a user