fix(medusa): Refund amount on returns in claim flow (#3237)
This commit is contained in:
1 parent
d48606d5dd
commit
968eb8fc6b
8 files changed
+505
-115
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): Refund amount on returns in claim flow
|
||||||
@@ -1441,6 +1441,57 @@ describe("/admin/orders", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Receives return with custom refund amount passed on receive", async () => {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const orderId = "test-order"
|
||||||
|
const itemId = "test-item"
|
||||||
|
|
||||||
|
const returned = await api.post(
|
||||||
|
`/admin/orders/${orderId}/return`,
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
// item has a unit_price of 8000 with a 800 adjustment
|
||||||
|
item_id: itemId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
const returnOrder = returned.data.order.returns[0]
|
||||||
|
|
||||||
|
expect(returned.status).toEqual(200)
|
||||||
|
expect(returnOrder.refund_amount).toEqual(7200)
|
||||||
|
|
||||||
|
const received = await api.post(
|
||||||
|
`/admin/returns/${returnOrder.id}/receive`,
|
||||||
|
{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
item_id: itemId,
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
refund: 0,
|
||||||
|
},
|
||||||
|
adminReqConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
const receivedReturn = received.data.return
|
||||||
|
|
||||||
|
expect(received.status).toEqual(200)
|
||||||
|
expect(receivedReturn.refund_amount).toEqual(0)
|
||||||
|
|
||||||
|
const orderRes = await api.get(`/admin/orders/${orderId}`, adminReqConfig)
|
||||||
|
|
||||||
|
const order = orderRes.data.order
|
||||||
|
|
||||||
|
expect(order.refunds.length).toEqual(0)
|
||||||
|
})
|
||||||
|
|
||||||
it("increases inventory_quantity when return is received", async () => {
|
it("increases inventory_quantity when return is received", async () => {
|
||||||
const api = useApi()
|
const api = useApi()
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,83 @@ describe("Claims", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("creates a refund claim + return", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const order = await createReturnableOrder(dbConnection)
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/orders/${order.id}/claims`,
|
||||||
|
{
|
||||||
|
type: "refund",
|
||||||
|
claim_items: [
|
||||||
|
{
|
||||||
|
item_id: "test-item",
|
||||||
|
reason: "missing_item",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
return_shipping: {
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const claim = response.data.order.claims[0]
|
||||||
|
const refund = response.data.order.refunds[0]
|
||||||
|
const returnOrder = response.data.order.returns[0]
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(claim.refund_amount).toEqual(1200)
|
||||||
|
expect(refund.amount).toEqual(1200)
|
||||||
|
expect(returnOrder.refund_amount).toEqual(1200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("creates a refund claim + return with a custom amount", async () => {
|
||||||
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
const order = await createReturnableOrder(dbConnection)
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
`/admin/orders/${order.id}/claims`,
|
||||||
|
{
|
||||||
|
type: "refund",
|
||||||
|
refund_amount: 500,
|
||||||
|
claim_items: [
|
||||||
|
{
|
||||||
|
item_id: "test-item",
|
||||||
|
reason: "missing_item",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
return_shipping: {
|
||||||
|
price: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer test_token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const claim = response.data.order.claims[0]
|
||||||
|
const refund = response.data.order.refunds[0]
|
||||||
|
const returnOrder = response.data.order.returns[0]
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(claim.refund_amount).toEqual(500)
|
||||||
|
expect(refund.amount).toEqual(500)
|
||||||
|
expect(returnOrder.refund_amount).toEqual(500)
|
||||||
|
})
|
||||||
|
|
||||||
test("creates a replace claim", async () => {
|
test("creates a replace claim", async () => {
|
||||||
await adminSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import ClaimService from "../claim"
|
import ClaimService from "../claim"
|
||||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ const eventBusService = {
|
|||||||
const totalsService = {
|
const totalsService = {
|
||||||
getCalculationContext: jest.fn(() => {}),
|
getCalculationContext: jest.fn(() => {}),
|
||||||
getRefundTotal: jest.fn(() => 1000),
|
getRefundTotal: jest.fn(() => 1000),
|
||||||
|
getLineItemRefund: jest.fn(() => 8000),
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("ClaimService", () => {
|
describe("ClaimService", () => {
|
||||||
@@ -28,6 +29,7 @@ describe("ClaimService", () => {
|
|||||||
{
|
{
|
||||||
id: "itm_1",
|
id: "itm_1",
|
||||||
unit_price: 8000,
|
unit_price: 8000,
|
||||||
|
shipped_quantity: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -134,6 +136,7 @@ describe("ClaimService", () => {
|
|||||||
expect(returnService.create).toHaveBeenCalledWith({
|
expect(returnService.create).toHaveBeenCalledWith({
|
||||||
order_id: "1234",
|
order_id: "1234",
|
||||||
claim_order_id: "claim_134",
|
claim_order_id: "claim_134",
|
||||||
|
refund_amount: 8000,
|
||||||
shipping_method: {
|
shipping_method: {
|
||||||
option_id: "opt_13",
|
option_id: "opt_13",
|
||||||
price: 0,
|
price: 0,
|
||||||
@@ -181,7 +184,7 @@ describe("ClaimService", () => {
|
|||||||
expect(claimRepo.create).toHaveBeenCalledWith({
|
expect(claimRepo.create).toHaveBeenCalledWith({
|
||||||
payment_status: "not_refunded",
|
payment_status: "not_refunded",
|
||||||
no_notification: true,
|
no_notification: true,
|
||||||
refund_amount: 1000,
|
refund_amount: 8000,
|
||||||
type: "refund",
|
type: "refund",
|
||||||
order_id: "1234",
|
order_id: "1234",
|
||||||
additional_items: [
|
additional_items: [
|
||||||
@@ -291,6 +294,206 @@ describe("ClaimService", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("getRefundTotalForClaimLinesOnOrder", () => {
|
||||||
|
const testOrder = (items = [], swaps = [], claims = []) => ({
|
||||||
|
id: "1234",
|
||||||
|
region_id: "order_region",
|
||||||
|
no_notification: true,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
...items,
|
||||||
|
],
|
||||||
|
...swaps,
|
||||||
|
...claims,
|
||||||
|
})
|
||||||
|
|
||||||
|
const claimRepo = MockRepository({})
|
||||||
|
|
||||||
|
const claimService = new ClaimService({
|
||||||
|
manager: MockManager,
|
||||||
|
claimRepository: claimRepo,
|
||||||
|
totalsService,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates refund total for claim with one shipped item", async () => {
|
||||||
|
const order = testOrder([
|
||||||
|
{
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item_id: "itm_1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(1)
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledWith(order, {
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(refund).toEqual(8000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates refund total for claim with one shipped + one pending item", async () => {
|
||||||
|
const order = testOrder([{ id: "itm_2", shipped_quantity: 0 }])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item_id: "itm_1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(1)
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledWith(order, {
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(refund).toEqual(8000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates refund total for claim with two shipped + one pending item", async () => {
|
||||||
|
const order = testOrder([
|
||||||
|
{ id: "itm_2", shipped_quantity: 0 },
|
||||||
|
{ id: "itm_3", shipped_quantity: 1, unit_price: 5000 },
|
||||||
|
])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item_id: "itm_1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
{ item_id: "itm_3", quantity: 1 },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(2)
|
||||||
|
expect(totalsService.getLineItemRefund.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
order,
|
||||||
|
{
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
order,
|
||||||
|
{
|
||||||
|
id: "itm_3",
|
||||||
|
unit_price: 5000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(refund).toEqual(16000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates refund total for claim on swap items", async () => {
|
||||||
|
const order = testOrder([], [[{ id: "itm_1", shipped_quantity: 1 }]])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item_id: "itm_1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(1)
|
||||||
|
expect(totalsService.getLineItemRefund.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
order,
|
||||||
|
{
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(refund).toEqual(8000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calculates refund total for claim on claim items", async () => {
|
||||||
|
const order = testOrder([], [], [{ id: "itm_1", shipped_quantity: 1 }])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
item_id: "itm_1",
|
||||||
|
quantity: 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(1)
|
||||||
|
expect(totalsService.getLineItemRefund.mock.calls).toEqual([
|
||||||
|
[
|
||||||
|
order,
|
||||||
|
{
|
||||||
|
id: "itm_1",
|
||||||
|
unit_price: 8000,
|
||||||
|
quantity: 1,
|
||||||
|
shipped_quantity: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(refund).toEqual(8000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("return 0 when claim lines cannot be found", async () => {
|
||||||
|
const order = testOrder([
|
||||||
|
{ id: "itm_2", shipped_quantity: 0 },
|
||||||
|
{ id: "itm_3", shipped_quantity: 1, unit_price: 5000 },
|
||||||
|
])
|
||||||
|
|
||||||
|
const refund = await claimService.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order,
|
||||||
|
[{ item_id: "itm_10", quantity: 1 }]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(totalsService.getLineItemRefund).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
|
expect(refund).toEqual(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("retrieve", () => {
|
describe("retrieve", () => {
|
||||||
const claimRepo = MockRepository()
|
const claimRepo = MockRepository()
|
||||||
const claimService = new ClaimService({
|
const claimService = new ClaimService({
|
||||||
|
|||||||
@@ -8,13 +8,18 @@ import {
|
|||||||
ClaimType,
|
ClaimType,
|
||||||
FulfillmentItem,
|
FulfillmentItem,
|
||||||
LineItem,
|
LineItem,
|
||||||
|
Order,
|
||||||
ReturnItem,
|
ReturnItem,
|
||||||
} from "../models"
|
} from "../models"
|
||||||
import { AddressRepository } from "../repositories/address"
|
import { AddressRepository } from "../repositories/address"
|
||||||
import { ClaimRepository } from "../repositories/claim"
|
import { ClaimRepository } from "../repositories/claim"
|
||||||
import { LineItemRepository } from "../repositories/line-item"
|
import { LineItemRepository } from "../repositories/line-item"
|
||||||
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
||||||
import { CreateClaimInput, UpdateClaimInput } from "../types/claim"
|
import {
|
||||||
|
CreateClaimInput,
|
||||||
|
CreateClaimItemInput,
|
||||||
|
UpdateClaimInput,
|
||||||
|
} from "../types/claim"
|
||||||
import { FindConfig } from "../types/common"
|
import { FindConfig } from "../types/common"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import { buildQuery, setMetadata } from "../utils"
|
||||||
import ClaimItemService from "./claim-item"
|
import ClaimItemService from "./claim-item"
|
||||||
@@ -204,6 +209,124 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async validateCreateClaimInput(
|
||||||
|
data: CreateClaimInput
|
||||||
|
): Promise<void> {
|
||||||
|
const lineItemServiceTx = this.lineItemService_.withTransaction(
|
||||||
|
this.manager_
|
||||||
|
)
|
||||||
|
|
||||||
|
const { type, claim_items, additional_items, refund_amount } = data
|
||||||
|
|
||||||
|
if (type !== ClaimType.REFUND && type !== ClaimType.REPLACE) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Claim type must be one of "refund" or "replace".`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ClaimType.REPLACE && !additional_items?.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Claims with type "replace" must have at least one additional item.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim_items?.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Claims must have at least one claim item.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refund_amount && type !== ClaimType.REFUND) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Claim has type "${type}" but must be type "refund" to have a refund_amount.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const claimLineItems = await lineItemServiceTx.list(
|
||||||
|
{ id: claim_items.map((c) => c.item_id) },
|
||||||
|
{ relations: ["order", "swap", "claim_order", "tax_lines"] }
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const line of claimLineItems) {
|
||||||
|
if (
|
||||||
|
line.order?.canceled_at ||
|
||||||
|
line.swap?.canceled_at ||
|
||||||
|
line.claim_order?.canceled_at
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot create a claim on a canceled item.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds claim line items on an order and calculates the refund amount.
|
||||||
|
* There are three places too look:
|
||||||
|
* - Order items
|
||||||
|
* - Swap items
|
||||||
|
* - Claim items (from previous claims)
|
||||||
|
* Note, it will attempt to return early from each of these places to avoid having to iterate over all items every time.
|
||||||
|
* @param order - the order to find claim lines on
|
||||||
|
* @param claimItems - the claim items to match against
|
||||||
|
* @return the refund amount
|
||||||
|
*/
|
||||||
|
protected async getRefundTotalForClaimLinesOnOrder(
|
||||||
|
order: Order,
|
||||||
|
claimItems: CreateClaimItemInput[]
|
||||||
|
) {
|
||||||
|
const claimLines = claimItems
|
||||||
|
.map((ci) => {
|
||||||
|
const predicate = (it: LineItem) =>
|
||||||
|
it.shipped_quantity! > 0 &&
|
||||||
|
ci.quantity <= it.shipped_quantity! &&
|
||||||
|
it.id === ci.item_id
|
||||||
|
|
||||||
|
const claimLine = order.items.find(predicate)
|
||||||
|
|
||||||
|
if (claimLine) {
|
||||||
|
return { ...claimLine, quantity: ci.quantity }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.swaps?.length) {
|
||||||
|
for (const swap of order.swaps) {
|
||||||
|
const claimLine = swap.additional_items.find(predicate)
|
||||||
|
|
||||||
|
if (claimLine) {
|
||||||
|
return { ...claimLine, quantity: ci.quantity }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.claims?.length) {
|
||||||
|
for (const claim of order.claims) {
|
||||||
|
const claimLine = claim.additional_items.find(predicate)
|
||||||
|
|
||||||
|
if (claimLine) {
|
||||||
|
return { ...claimLine, quantity: ci.quantity }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.filter(Boolean) as LineItem[]
|
||||||
|
|
||||||
|
const refunds: number[] = []
|
||||||
|
|
||||||
|
for (const item of claimLines) {
|
||||||
|
const refund = await this.totalsService_.getLineItemRefund(order, item)
|
||||||
|
refunds.push(refund)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(refunds.reduce((acc, next) => acc + next, 0))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Claim on an Order. Claims consists of items that are claimed and
|
* Creates a Claim on an Order. Claims consists of items that are claimed and
|
||||||
* optionally items to be sent as replacement for the claimed items. The
|
* optionally items to be sent as replacement for the claimed items. The
|
||||||
@@ -232,25 +355,7 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
...rest
|
...rest
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
const lineItemServiceTx =
|
await this.validateCreateClaimInput(data)
|
||||||
this.lineItemService_.withTransaction(transactionManager)
|
|
||||||
|
|
||||||
for (const item of claim_items) {
|
|
||||||
const line = await lineItemServiceTx.retrieve(item.item_id, {
|
|
||||||
relations: ["order", "swap", "claim_order", "tax_lines"],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
line.order?.canceled_at ||
|
|
||||||
line.swap?.canceled_at ||
|
|
||||||
line.claim_order?.canceled_at
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Cannot create a claim on a canceled item.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let addressId = shipping_address_id || order.shipping_address_id
|
let addressId = shipping_address_id || order.shipping_address_id
|
||||||
if (shipping_address) {
|
if (shipping_address) {
|
||||||
@@ -262,76 +367,18 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
addressId = saved.id
|
addressId = saved.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type !== ClaimType.REFUND && type !== ClaimType.REPLACE) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Claim type must be one of "refund" or "replace".`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === ClaimType.REPLACE && !additional_items?.length) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Claims with type "replace" must have at least one additional item.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!claim_items?.length) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Claims must have at least one claim item.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refund_amount && type !== ClaimType.REFUND) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Claim has type "${type}" but must be type "refund" to have a refund_amount.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let toRefund = refund_amount
|
let toRefund = refund_amount
|
||||||
if (type === ClaimType.REFUND && typeof refund_amount === "undefined") {
|
if (type === ClaimType.REFUND && typeof refund_amount === "undefined") {
|
||||||
const lines = claim_items.map((ci) => {
|
// In case no refund amount is passed, we calculate it based on the claim items on the order
|
||||||
const allOrderItems = order.items
|
toRefund = await this.getRefundTotalForClaimLinesOnOrder(
|
||||||
|
|
||||||
if (order.swaps?.length) {
|
|
||||||
for (const swap of order.swaps) {
|
|
||||||
swap.additional_items.forEach((it) => {
|
|
||||||
if (
|
|
||||||
it.shipped_quantity ||
|
|
||||||
it.shipped_quantity === it.fulfilled_quantity
|
|
||||||
) {
|
|
||||||
allOrderItems.push(it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (order.claims?.length) {
|
|
||||||
for (const claim of order.claims) {
|
|
||||||
claim.additional_items.forEach((it) => {
|
|
||||||
if (
|
|
||||||
it.shipped_quantity ||
|
|
||||||
it.shipped_quantity === it.fulfilled_quantity
|
|
||||||
) {
|
|
||||||
allOrderItems.push(it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItem = allOrderItems.find((oi) => oi.id === ci.item_id)
|
|
||||||
return {
|
|
||||||
...orderItem,
|
|
||||||
quantity: ci.quantity,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
toRefund = await this.totalsService_.getRefundTotal(
|
|
||||||
order,
|
order,
|
||||||
lines as LineItem[]
|
claim_items
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lineItemServiceTx =
|
||||||
|
this.lineItemService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
let newItems: LineItem[] = []
|
let newItems: LineItem[] = []
|
||||||
|
|
||||||
if (isDefined(additional_items)) {
|
if (isDefined(additional_items)) {
|
||||||
@@ -425,6 +472,7 @@ export default class ClaimService extends TransactionBaseService {
|
|||||||
|
|
||||||
if (return_shipping) {
|
if (return_shipping) {
|
||||||
await this.returnService_.withTransaction(transactionManager).create({
|
await this.returnService_.withTransaction(transactionManager).create({
|
||||||
|
refund_amount: toRefund,
|
||||||
order_id: order.id,
|
order_id: order.id,
|
||||||
claim_order_id: result.id,
|
claim_order_id: result.id,
|
||||||
items: claim_items.map(
|
items: claim_items.map(
|
||||||
|
|||||||
@@ -1883,7 +1883,7 @@ class OrderService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundAmount = customRefundAmount || receivedReturn.refund_amount
|
const refundAmount = customRefundAmount ?? receivedReturn.refund_amount
|
||||||
|
|
||||||
const orderRepo = manager.getCustomRepository(this.orderRepository_)
|
const orderRepo = manager.getCustomRepository(this.orderRepository_)
|
||||||
|
|
||||||
@@ -1907,10 +1907,10 @@ class OrderService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (receivedReturn.refund_amount > 0) {
|
if (refundAmount > 0) {
|
||||||
const refund = await this.paymentProviderService_
|
const refund = await this.paymentProviderService_
|
||||||
.withTransaction(manager)
|
.withTransaction(manager)
|
||||||
.refundPayment(order.payments, receivedReturn.refund_amount, "return")
|
.refundPayment(order.payments, refundAmount, "return")
|
||||||
|
|
||||||
order.refunds = [...(order.refunds || []), refund]
|
order.refunds = [...(order.refunds || []), refund]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import { buildQuery, setMetadata } from "../utils"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
FulfillmentProviderService,
|
FulfillmentProviderService,
|
||||||
ProductVariantInventoryService,
|
|
||||||
LineItemService,
|
LineItemService,
|
||||||
OrderService,
|
OrderService,
|
||||||
|
ProductVariantInventoryService,
|
||||||
ReturnReasonService,
|
ReturnReasonService,
|
||||||
ShippingOptionService,
|
ShippingOptionService,
|
||||||
TaxProviderService,
|
TaxProviderService,
|
||||||
@@ -653,7 +653,7 @@ class ReturnService extends TransactionBaseService {
|
|||||||
returnStatus = ReturnStatus.REQUIRES_ACTION
|
returnStatus = ReturnStatus.REQUIRES_ACTION
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalRefundableAmount = refundAmount || returnObj.refund_amount
|
const totalRefundableAmount = refundAmount ?? returnObj.refund_amount
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const updateObj = {
|
const updateObj = {
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { EntityManager, In } from "typeorm"
|
import { EntityManager, In } from "typeorm"
|
||||||
|
|
||||||
import { buildQuery, setMetadata, validateId } from "../utils"
|
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
|
import { buildQuery, setMetadata, validateId } from "../utils"
|
||||||
|
|
||||||
import LineItemAdjustmentService from "./line-item-adjustment"
|
import {
|
||||||
import { FindConfig, Selector, WithRequiredProperty } from "../types/common"
|
Cart,
|
||||||
|
CartType,
|
||||||
|
FulfillmentItem,
|
||||||
|
FulfillmentStatus,
|
||||||
|
LineItem,
|
||||||
|
Order,
|
||||||
|
PaymentSessionStatus,
|
||||||
|
PaymentStatus,
|
||||||
|
ReturnItem,
|
||||||
|
ReturnStatus,
|
||||||
|
Swap,
|
||||||
|
SwapFulfillmentStatus,
|
||||||
|
SwapPaymentStatus,
|
||||||
|
} from "../models"
|
||||||
import { SwapRepository } from "../repositories/swap"
|
import { SwapRepository } from "../repositories/swap"
|
||||||
|
import { FindConfig, Selector, WithRequiredProperty } from "../types/common"
|
||||||
|
import { CreateShipmentConfig } from "../types/fulfillment"
|
||||||
|
import { OrdersReturnItem } from "../types/orders"
|
||||||
import CartService from "./cart"
|
import CartService from "./cart"
|
||||||
import {
|
import {
|
||||||
CustomShippingOptionService,
|
CustomShippingOptionService,
|
||||||
@@ -20,21 +36,7 @@ import {
|
|||||||
ShippingOptionService,
|
ShippingOptionService,
|
||||||
TotalsService,
|
TotalsService,
|
||||||
} from "./index"
|
} from "./index"
|
||||||
import {
|
import LineItemAdjustmentService from "./line-item-adjustment"
|
||||||
Cart,
|
|
||||||
CartType,
|
|
||||||
FulfillmentItem,
|
|
||||||
LineItem,
|
|
||||||
Order,
|
|
||||||
PaymentSessionStatus,
|
|
||||||
ReturnItem,
|
|
||||||
ReturnStatus,
|
|
||||||
Swap,
|
|
||||||
SwapFulfillmentStatus,
|
|
||||||
SwapPaymentStatus,
|
|
||||||
} from "../models"
|
|
||||||
import { CreateShipmentConfig } from "../types/fulfillment"
|
|
||||||
import { OrdersReturnItem } from "../types/orders"
|
|
||||||
|
|
||||||
type InjectedProps = {
|
type InjectedProps = {
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
@@ -325,13 +327,17 @@ class SwapService extends TransactionBaseService {
|
|||||||
): Promise<Swap | never> {
|
): Promise<Swap | never> {
|
||||||
const { no_notification, ...rest } = custom
|
const { no_notification, ...rest } = custom
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
if (
|
if (order.payment_status !== PaymentStatus.CAPTURED) {
|
||||||
order.fulfillment_status === "not_fulfilled" ||
|
|
||||||
order.payment_status !== "captured"
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
"Order cannot be swapped"
|
"Cannot swap an order that has not been captured"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (order.fulfillment_status === FulfillmentStatus.NOT_FULFILLED) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
"Cannot swap an order that has not been fulfilled"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user