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:
@@ -436,238 +436,334 @@ medusaIntegrationTestRunner({
|
||||
describe("Claims lifecycle", () => {
|
||||
let claimId
|
||||
|
||||
beforeEach(async () => {
|
||||
let r2 = await api.post(
|
||||
"/admin/claims",
|
||||
{
|
||||
order_id: order2.id,
|
||||
type: ClaimType.REFUND,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const claimId2 = r2.data.claim.id
|
||||
const item2 = order2.items[0]
|
||||
|
||||
const {
|
||||
data: {
|
||||
order_preview: {
|
||||
items: [previewItem],
|
||||
describe("with inbound and outbound items", () => {
|
||||
beforeEach(async () => {
|
||||
let r2 = await api.post(
|
||||
"/admin/claims",
|
||||
{
|
||||
order_id: order2.id,
|
||||
type: ClaimType.REFUND,
|
||||
},
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/items`,
|
||||
{ items: [{ id: item2.id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId2}/inbound/items/${previewItem.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: { return: returnData },
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/items`,
|
||||
{ items: [{ id: item2.id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/returns/${returnData.id}`,
|
||||
{
|
||||
location_id: location.id,
|
||||
no_notification: true,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: {
|
||||
order_preview: { shipping_methods: inboundShippingMethods },
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
const inboundShippingMethod = inboundShippingMethods.find(
|
||||
(m) => m.shipping_option_id == returnShippingOption.id
|
||||
)
|
||||
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method/${inboundShippingMethod.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(`/admin/claims/${claimId2}/request`, {}, adminHeaders)
|
||||
|
||||
claimId = baseClaim.id
|
||||
const item = order.items[0]
|
||||
|
||||
let result = await api.post(
|
||||
`/admin/claims/${claimId}/inbound/items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
reason_id: returnReason.id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: {
|
||||
order_preview: { shipping_methods: outboundShippingMethods },
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const outboundShippingMethod = outboundShippingMethods.find(
|
||||
(m) => m.shipping_option_id == outboundShippingOption.id
|
||||
)
|
||||
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method/${outboundShippingMethod.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// updated the requested quantity
|
||||
const updateReturnItemActionId =
|
||||
result.data.order_preview.items[0].actions[0].id
|
||||
|
||||
result = await api.post(
|
||||
`/admin/claims/${claimId}/inbound/items/${updateReturnItemActionId}`,
|
||||
{
|
||||
quantity: 1,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// New Items
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
variant_id: productExtra.variants[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// Claim Items
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/claim-items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
reason: ClaimReason.PRODUCTION_FAILURE,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
result = await api.post(
|
||||
`/admin/claims/${claimId}/request`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
result = (
|
||||
await api.get(
|
||||
`/admin/claims?fields=*claim_items,*additional_items`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.claims
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].additional_items).toHaveLength(1)
|
||||
expect(result[0].claim_items).toHaveLength(1)
|
||||
expect(result[0].canceled_at).toBeNull()
|
||||
const claimId2 = r2.data.claim.id
|
||||
const item2 = order2.items[0]
|
||||
|
||||
const reservationsResponse = (
|
||||
await api.get(
|
||||
`/admin/reservations?location_id[]=${location.id}`,
|
||||
const {
|
||||
data: {
|
||||
order_preview: {
|
||||
items: [previewItem],
|
||||
},
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/items`,
|
||||
{ items: [{ id: item2.id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
).data
|
||||
|
||||
expect(reservationsResponse.reservations).toHaveLength(1)
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId2}/inbound/items/${previewItem.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: { return: returnData },
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/items`,
|
||||
{ items: [{ id: item2.id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/returns/${returnData.id}`,
|
||||
{
|
||||
location_id: location.id,
|
||||
no_notification: true,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: {
|
||||
order_preview: { shipping_methods: inboundShippingMethods },
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
const inboundShippingMethod = inboundShippingMethods.find(
|
||||
(m) => m.shipping_option_id == returnShippingOption.id
|
||||
)
|
||||
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method/${inboundShippingMethod.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId2}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(`/admin/claims/${claimId2}/request`, {}, adminHeaders)
|
||||
|
||||
claimId = baseClaim.id
|
||||
const item = order.items[0]
|
||||
|
||||
let result = await api.post(
|
||||
`/admin/claims/${claimId}/inbound/items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
reason_id: returnReason.id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/inbound/shipping-method`,
|
||||
{ shipping_option_id: returnShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const {
|
||||
data: {
|
||||
order_preview: { shipping_methods: outboundShippingMethods },
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const outboundShippingMethod = outboundShippingMethods.find(
|
||||
(m) => m.shipping_option_id == outboundShippingOption.id
|
||||
)
|
||||
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method/${outboundShippingMethod.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// updated the requested quantity
|
||||
const updateReturnItemActionId =
|
||||
result.data.order_preview.items[0].actions[0].id
|
||||
|
||||
result = await api.post(
|
||||
`/admin/claims/${claimId}/inbound/items/${updateReturnItemActionId}`,
|
||||
{
|
||||
quantity: 1,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// New Items
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
variant_id: productExtra.variants[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// Claim Items
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/claim-items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
reason: ClaimReason.PRODUCTION_FAILURE,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
result = await api.post(
|
||||
`/admin/claims/${claimId}/request`,
|
||||
{},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
result = (
|
||||
await api.get(
|
||||
`/admin/claims?fields=*claim_items,*additional_items`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.claims
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].additional_items).toHaveLength(1)
|
||||
expect(result[0].claim_items).toHaveLength(1)
|
||||
expect(result[0].canceled_at).toBeNull()
|
||||
|
||||
const reservationsResponse = (
|
||||
await api.get(
|
||||
`/admin/reservations?location_id[]=${location.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
).data
|
||||
|
||||
expect(reservationsResponse.reservations).toHaveLength(1)
|
||||
})
|
||||
|
||||
it("should complete flow with fulfilled items successfully", async () => {
|
||||
const fulfillOrder = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
const fulfillableItem = fulfillOrder.items.find(
|
||||
(item) => item.detail.fulfilled_quantity === 0
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/orders/${order.id}/fulfillments`,
|
||||
{
|
||||
location_id: location.id,
|
||||
items: [{ id: fulfillableItem.id, quantity: 1 }],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("should go through cancel flow successfully", async () => {
|
||||
await api.post(`/admin/claims/${claimId}/cancel`, {}, adminHeaders)
|
||||
|
||||
const [claim] = (
|
||||
await api.get(
|
||||
`/admin/claims?fields=*claim_items,*additional_items`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.claims
|
||||
|
||||
expect(claim.canceled_at).toBeDefined()
|
||||
|
||||
const reservationsResponseAfterCanceling = (
|
||||
await api.get(
|
||||
`/admin/reservations?location_id[]=${location.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
).data
|
||||
|
||||
expect(reservationsResponseAfterCanceling.reservations).toHaveLength(
|
||||
0
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should complete flow with fulfilled items successfully", async () => {
|
||||
const fulfillOrder = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
describe("with only outbound items", () => {
|
||||
beforeEach(async () => {
|
||||
claimId = baseClaim.id
|
||||
const item = order.items[0]
|
||||
|
||||
const fulfillableItem = fulfillOrder.items.find(
|
||||
(item) => item.detail.fulfilled_quantity === 0
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/orders/${order.id}/fulfillments`,
|
||||
{
|
||||
location_id: location.id,
|
||||
items: [{ id: fulfillableItem.id, quantity: 1 }],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("should go through cancel flow successfully", async () => {
|
||||
await api.post(`/admin/claims/${claimId}/cancel`, {}, adminHeaders)
|
||||
|
||||
const [claim] = (
|
||||
await api.get(
|
||||
`/admin/claims?fields=*claim_items,*additional_items`,
|
||||
const {
|
||||
data: {
|
||||
order_preview: { shipping_methods: outboundShippingMethods },
|
||||
},
|
||||
} = await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
).data.claims
|
||||
|
||||
expect(claim.canceled_at).toBeDefined()
|
||||
const outboundShippingMethod = outboundShippingMethods.find(
|
||||
(m) => m.shipping_option_id == outboundShippingOption.id
|
||||
)
|
||||
|
||||
const reservationsResponseAfterCanceling = (
|
||||
await api.get(
|
||||
`/admin/reservations?location_id[]=${location.id}`,
|
||||
// Delete & recreate again to ensure it works for both delete and create
|
||||
await api.delete(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method/${outboundShippingMethod.actions[0].id}`,
|
||||
adminHeaders
|
||||
)
|
||||
).data
|
||||
|
||||
expect(reservationsResponseAfterCanceling.reservations).toHaveLength(0)
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/shipping-method`,
|
||||
{ shipping_option_id: outboundShippingOption.id },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/outbound/items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
variant_id: productExtra.variants[0].id,
|
||||
quantity: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/claims/${claimId}/claim-items`,
|
||||
{
|
||||
items: [
|
||||
{
|
||||
id: item.id,
|
||||
reason: ClaimReason.PRODUCTION_FAILURE,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
it("should complete flow with fulfilled items successfully", async () => {
|
||||
await api.post(`/admin/claims/${claimId}/request`, {}, adminHeaders)
|
||||
|
||||
const claim = (
|
||||
await api.get(
|
||||
`/admin/claims/${claimId}?fields=*claim_items,*additional_items`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.claim
|
||||
|
||||
expect(claim.additional_items).toHaveLength(1)
|
||||
expect(claim.claim_items).toHaveLength(1)
|
||||
expect(claim.canceled_at).toBeNull()
|
||||
|
||||
const fulfillOrder = (
|
||||
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
||||
).data.order
|
||||
|
||||
const fulfillableItem = fulfillOrder.items.find(
|
||||
(item) => item.detail.fulfilled_quantity === 0
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/orders/${order.id}/fulfillments`,
|
||||
{
|
||||
location_id: location.id,
|
||||
items: [{ id: fulfillableItem.id, quantity: 1 }],
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
12
packages/admin-next/dashboard/src/lib/payment.ts
Normal file
12
packages/admin-next/dashboard/src/lib/payment.ts
Normal file
@@ -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)
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -252,13 +252,17 @@ export const confirmClaimRequestWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
updateReturnsStep([
|
||||
{
|
||||
id: returnId,
|
||||
status: ReturnStatus.REQUESTED,
|
||||
requested_at: new Date(),
|
||||
},
|
||||
])
|
||||
when({ returnId }, ({ returnId }) => {
|
||||
return !!returnId
|
||||
}).then(() => {
|
||||
updateReturnsStep([
|
||||
{
|
||||
id: returnId,
|
||||
status: ReturnStatus.REQUESTED,
|
||||
requested_at: new Date(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
const claimId = transform({ createClaimItems }, ({ createClaimItems }) => {
|
||||
return createClaimItems?.[0]?.claim_id
|
||||
@@ -329,9 +333,12 @@ export const confirmClaimRequestWorkflow = createWorkflow(
|
||||
reserveInventoryStep(formatedInventoryItems)
|
||||
})
|
||||
|
||||
when({ returnShippingMethod }, ({ returnShippingMethod }) => {
|
||||
return !!returnShippingMethod
|
||||
}).then(() => {
|
||||
when(
|
||||
{ returnShippingMethod, returnId },
|
||||
({ returnShippingMethod, returnId }) => {
|
||||
return !!returnShippingMethod && !!returnId
|
||||
}
|
||||
).then(() => {
|
||||
const returnShippingOption = useRemoteQueryStep({
|
||||
entry_point: "shipping_options",
|
||||
fields: [
|
||||
|
||||
@@ -56,6 +56,11 @@ export interface BasePaymentCollection {
|
||||
*/
|
||||
authorized_amount?: BigNumberValue
|
||||
|
||||
/**
|
||||
* The amount captured within the associated payment sessions.
|
||||
*/
|
||||
captured_amount?: BigNumberValue
|
||||
|
||||
/**
|
||||
* The amount refunded within the associated payments.
|
||||
*/
|
||||
|
||||
@@ -21,4 +21,6 @@ export interface BaseReturn {
|
||||
no_notification?: boolean
|
||||
refund_amount?: number
|
||||
items: BaseReturnItem[]
|
||||
received_at: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const defaultAdminReturnFields = [
|
||||
"updated_at",
|
||||
"canceled_at",
|
||||
"requested_at",
|
||||
"received_at",
|
||||
]
|
||||
|
||||
export const defaultAdminDetailsReturnFields = [
|
||||
|
||||
Reference in New Issue
Block a user