* feat: carry over promotions toggle on exchanges * fix: inital flag value, return the flag on preview * fix: validation of allocation type * fix: revert client changes * fix: invert condition * feat: recompute adjustments when outbound item is updated * fix: condition again * fix: display more accurate inbound/outbound totals for exchanges * fix: make exchanges specs green * feat: more testing cases * wip: pr feedback * fix: use plural for the flag on Admin * fix: schema test, route refactor * feat: tooltip * feat: refactor to use update workflow * feat: display applied promotion per item on order details, show copy sku on hover * feat: refactor edits and exchanges to have common flag toggle flow * fix: delete empty file * fix: exchange_id param query
1591 lines
44 KiB
TypeScript
1591 lines
44 KiB
TypeScript
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
|
import { IOrderModuleService, IPromotionModuleService } from "@medusajs/types"
|
|
import {
|
|
ContainerRegistrationKeys,
|
|
Modules,
|
|
ProductStatus,
|
|
PromotionStatus,
|
|
PromotionType,
|
|
RuleOperator,
|
|
} from "@medusajs/utils"
|
|
import {
|
|
adminHeaders,
|
|
createAdminUser,
|
|
} from "../../../helpers/create-admin-user"
|
|
import { setupTaxStructure } from "../../../modules/__tests__/fixtures/tax"
|
|
|
|
jest.setTimeout(300000)
|
|
|
|
medusaIntegrationTestRunner({
|
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
|
let order, order2
|
|
let returnShippingOption
|
|
let outboundShippingOption
|
|
let shippingProfile
|
|
let fulfillmentSet
|
|
let returnReason
|
|
let inventoryItem
|
|
let inventoryItemExtra
|
|
let inventoryItemExtra2
|
|
let location
|
|
let productExtra
|
|
let productExtra2
|
|
const shippingProviderId = "manual_test-provider"
|
|
|
|
beforeEach(async () => {
|
|
const container = getContainer()
|
|
await createAdminUser(dbConnection, adminHeaders, container)
|
|
|
|
const region = (
|
|
await api.post(
|
|
"/admin/regions",
|
|
{
|
|
name: "test-region",
|
|
currency_code: "usd",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.region
|
|
|
|
shippingProfile = (
|
|
await api.post(
|
|
`/admin/shipping-profiles`,
|
|
{
|
|
name: "Test",
|
|
type: "default",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.shipping_profile
|
|
|
|
const customer = (
|
|
await api.post(
|
|
"/admin/customers",
|
|
{
|
|
first_name: "joe",
|
|
email: "joe@admin.com",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.customer
|
|
|
|
const salesChannel = (
|
|
await api.post(
|
|
"/admin/sales-channels",
|
|
{
|
|
name: "Test channel",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.sales_channel
|
|
|
|
const product = (
|
|
await api.post(
|
|
"/admin/products",
|
|
{
|
|
title: "Test product",
|
|
status: ProductStatus.PUBLISHED,
|
|
shipping_profile_id: shippingProfile.id,
|
|
options: [{ title: "size", values: ["large", "small"] }],
|
|
variants: [
|
|
{
|
|
title: "Test variant",
|
|
sku: "test-variant",
|
|
options: { size: "large" },
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 10,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.product
|
|
|
|
productExtra = (
|
|
await api.post(
|
|
"/admin/products",
|
|
{
|
|
title: "Extra product",
|
|
status: ProductStatus.PUBLISHED,
|
|
shipping_profile_id: shippingProfile.id,
|
|
options: [{ title: "size", values: ["large", "small"] }],
|
|
variants: [
|
|
{
|
|
title: "my variant",
|
|
sku: "variant-sku",
|
|
options: { size: "large" },
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 123456.1234657890123456789,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.product
|
|
|
|
productExtra2 = (
|
|
await api.post(
|
|
"/admin/products",
|
|
{
|
|
title: "Extra product 2, same price",
|
|
status: ProductStatus.PUBLISHED,
|
|
shipping_profile_id: shippingProfile.id,
|
|
options: [{ title: "size", values: ["large", "small"] }],
|
|
variants: [
|
|
{
|
|
title: "my variant 2",
|
|
sku: "variant-sku-2",
|
|
options: { size: "large" },
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 25,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.product
|
|
|
|
returnReason = (
|
|
await api.post(
|
|
"/admin/return-reasons",
|
|
{
|
|
value: "return-reason-test",
|
|
label: "Test return reason",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.return_reason
|
|
|
|
const orderModule = container.resolve(Modules.ORDER)
|
|
|
|
order = await orderModule.createOrders({
|
|
region_id: region.id,
|
|
email: "foo@bar.com",
|
|
items: [
|
|
{
|
|
title: "Custom Item 2",
|
|
variant_id: product.variants[0].id,
|
|
quantity: 2,
|
|
unit_price: 25,
|
|
},
|
|
],
|
|
sales_channel_id: salesChannel.id,
|
|
shipping_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
phone: "12345",
|
|
},
|
|
billing_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
},
|
|
shipping_methods: [
|
|
{
|
|
name: "Test shipping method",
|
|
amount: 10,
|
|
data: {},
|
|
tax_lines: [
|
|
{
|
|
description: "shipping Tax 1",
|
|
tax_rate_id: "tax_usa_shipping",
|
|
code: "code",
|
|
rate: 10,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
currency_code: "usd",
|
|
customer_id: customer.id,
|
|
})
|
|
|
|
order2 = await orderModule.createOrders({
|
|
region_id: region.id,
|
|
email: "foo@bar2.com",
|
|
items: [
|
|
{
|
|
title: "Custom Iasdasd2",
|
|
quantity: 1,
|
|
unit_price: 20,
|
|
},
|
|
],
|
|
sales_channel_id: salesChannel.id,
|
|
shipping_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
phone: "12345",
|
|
},
|
|
billing_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
},
|
|
currency_code: "usd",
|
|
customer_id: customer.id,
|
|
})
|
|
|
|
location = (
|
|
await api.post(
|
|
`/admin/stock-locations`,
|
|
{
|
|
name: "Test location",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.stock_location
|
|
|
|
location = (
|
|
await api.post(
|
|
`/admin/stock-locations/${location.id}/fulfillment-sets?fields=*fulfillment_sets`,
|
|
{
|
|
name: "Test",
|
|
type: "test-type",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.stock_location
|
|
|
|
fulfillmentSet = (
|
|
await api.post(
|
|
`/admin/fulfillment-sets/${location.fulfillment_sets[0].id}/service-zones`,
|
|
{
|
|
name: "Test",
|
|
geo_zones: [{ type: "country", country_code: "us" }],
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.fulfillment_set
|
|
|
|
inventoryItem = (
|
|
await api.post(
|
|
`/admin/inventory-items`,
|
|
{ sku: "inv-1234" },
|
|
adminHeaders
|
|
)
|
|
).data.inventory_item
|
|
|
|
await api.post(
|
|
`/admin/inventory-items/${inventoryItem.id}/location-levels`,
|
|
{
|
|
location_id: location.id,
|
|
stocked_quantity: 2,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
inventoryItemExtra = (
|
|
await api.get(`/admin/inventory-items?sku=variant-sku`, adminHeaders)
|
|
).data.inventory_items[0]
|
|
|
|
inventoryItemExtra2 = (
|
|
await api.get(`/admin/inventory-items?sku=variant-sku-2`, adminHeaders)
|
|
).data.inventory_items[0]
|
|
|
|
await api.post(
|
|
`/admin/inventory-items/${inventoryItemExtra.id}/location-levels`,
|
|
{
|
|
location_id: location.id,
|
|
stocked_quantity: 4,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/inventory-items/${inventoryItemExtra2.id}/location-levels`,
|
|
{
|
|
location_id: location.id,
|
|
stocked_quantity: 2,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const remoteLink = container.resolve(
|
|
ContainerRegistrationKeys.REMOTE_LINK
|
|
)
|
|
|
|
await remoteLink.create([
|
|
{
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: location.id,
|
|
},
|
|
[Modules.FULFILLMENT]: {
|
|
fulfillment_provider_id: shippingProviderId,
|
|
},
|
|
},
|
|
{
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: location.id,
|
|
},
|
|
[Modules.FULFILLMENT]: {
|
|
fulfillment_set_id: fulfillmentSet.id,
|
|
},
|
|
},
|
|
{
|
|
[Modules.SALES_CHANNEL]: {
|
|
sales_channel_id: salesChannel.id,
|
|
},
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: location.id,
|
|
},
|
|
},
|
|
{
|
|
[Modules.PRODUCT]: {
|
|
variant_id: product.variants[0].id,
|
|
},
|
|
[Modules.INVENTORY]: {
|
|
inventory_item_id: inventoryItem.id,
|
|
},
|
|
},
|
|
{
|
|
[Modules.PRODUCT]: {
|
|
variant_id: productExtra.variants[0].id,
|
|
},
|
|
[Modules.INVENTORY]: {
|
|
inventory_item_id: inventoryItemExtra.id,
|
|
},
|
|
},
|
|
{
|
|
[Modules.PRODUCT]: {
|
|
variant_id: productExtra2.variants[0].id,
|
|
},
|
|
[Modules.INVENTORY]: {
|
|
inventory_item_id: inventoryItemExtra2.id,
|
|
},
|
|
},
|
|
])
|
|
|
|
// create reservation for inventory item that is initially on the order
|
|
const inventoryModule = container.resolve(Modules.INVENTORY)
|
|
await inventoryModule.createReservationItems([
|
|
{
|
|
inventory_item_id: inventoryItem.id,
|
|
location_id: location.id,
|
|
quantity: 2,
|
|
line_item_id: order.items[0].id,
|
|
},
|
|
])
|
|
|
|
const shippingOptionPayload = {
|
|
name: "Return shipping",
|
|
service_zone_id: fulfillmentSet.service_zones[0].id,
|
|
shipping_profile_id: shippingProfile.id,
|
|
provider_id: shippingProviderId,
|
|
price_type: "flat",
|
|
type: {
|
|
label: "Test type",
|
|
description: "Test description",
|
|
code: "test-code",
|
|
},
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 1000,
|
|
},
|
|
],
|
|
rules: [
|
|
{
|
|
operator: RuleOperator.EQ,
|
|
attribute: "is_return",
|
|
value: "true",
|
|
},
|
|
],
|
|
}
|
|
|
|
const outboundShippingOptionPayload = {
|
|
name: "Oubound shipping",
|
|
service_zone_id: fulfillmentSet.service_zones[0].id,
|
|
shipping_profile_id: shippingProfile.id,
|
|
provider_id: shippingProviderId,
|
|
price_type: "flat",
|
|
type: {
|
|
label: "Test type",
|
|
description: "Test description",
|
|
code: "test-code",
|
|
},
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 20,
|
|
},
|
|
],
|
|
rules: [
|
|
{
|
|
operator: RuleOperator.EQ,
|
|
attribute: "is_return",
|
|
value: "false",
|
|
},
|
|
{
|
|
operator: RuleOperator.EQ,
|
|
attribute: "enabled_in_store",
|
|
value: "true",
|
|
},
|
|
],
|
|
}
|
|
outboundShippingOption = (
|
|
await api.post(
|
|
"/admin/shipping-options",
|
|
outboundShippingOptionPayload,
|
|
adminHeaders
|
|
)
|
|
).data.shipping_option
|
|
|
|
returnShippingOption = (
|
|
await api.post(
|
|
"/admin/shipping-options",
|
|
shippingOptionPayload,
|
|
adminHeaders
|
|
)
|
|
).data.shipping_option
|
|
|
|
const item = order.items[0]
|
|
|
|
await api.post(
|
|
`/admin/orders/${order.id}/fulfillments`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item.id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/orders/${order2.id}/fulfillments`,
|
|
{
|
|
items: [
|
|
{
|
|
id: order2.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await setupTaxStructure(container.resolve(Modules.TAX))
|
|
})
|
|
|
|
describe("Exchanges lifecycle", () => {
|
|
it("test full exchange flow", async () => {
|
|
const orderBefore = (
|
|
await api.get(`/admin/orders/${order.id}`, adminHeaders)
|
|
).data.order
|
|
|
|
let result = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: order.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
expect(result.data.exchange.created_by).toEqual(expect.any(String))
|
|
|
|
const exchangeId = result.data.exchange.id
|
|
|
|
const item = order.items[0]
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item.id,
|
|
reason_id: returnReason.id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// New Item
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productExtra2.variants[0].id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
const returnId = result.data.exchange.return_id
|
|
|
|
result = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
|
.order
|
|
|
|
expect(orderBefore.total).toBe(61)
|
|
expect(result.total).toBe(112)
|
|
|
|
// receive return
|
|
await api.post(`/admin/returns/${returnId}/receive`, {}, adminHeaders)
|
|
await api.post(
|
|
`/admin/returns/${returnId}/receive-items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item.id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/returns/${returnId}/receive/confirm`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
result = (await api.get(`/admin/orders/${order.id}`, adminHeaders)).data
|
|
.order
|
|
|
|
expect(orderBefore.total).toBe(61)
|
|
expect(result.total).toBe(62) // +1 is from taxes of the new item
|
|
})
|
|
|
|
it("Full flow with 2 orders", async () => {
|
|
let result = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: order.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
expect(result.data.exchange.created_by).toEqual(expect.any(String))
|
|
|
|
const exchangeId = result.data.exchange.id
|
|
|
|
let r2 = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: order2.id,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const exchangeId2 = r2.data.exchange.id
|
|
const item2 = order2.items[0]
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId2}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item2.id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId2}/inbound/shipping-method`,
|
|
{
|
|
shipping_option_id: returnShippingOption.id,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const { response } = await api
|
|
.post(`/admin/exchanges/${exchangeId2}/request`, {}, adminHeaders)
|
|
.catch((e) => e)
|
|
|
|
expect(response.data).toEqual({
|
|
type: "invalid_data",
|
|
message:
|
|
"Order exchange request should have at least 1 item inbound and 1 item outbound",
|
|
})
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId2}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productExtra.variants[0].id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId2}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
const item = order.items[0]
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item.id,
|
|
reason_id: returnReason.id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/shipping-method`,
|
|
{
|
|
shipping_option_id: returnShippingOption.id,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// updated the requested quantity
|
|
const updateReturnItemActionId =
|
|
result.data.order_preview.items[0].actions[0].id
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items/${updateReturnItemActionId}`,
|
|
{
|
|
quantity: 1,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// New Items
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productExtra.variants[0].id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
result = await api.post(
|
|
`/admin/exchanges/${exchangeId}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
result = (
|
|
await api.get(
|
|
`/admin/exchanges?fields=+metadata,*additional_items`,
|
|
adminHeaders
|
|
)
|
|
).data.exchanges
|
|
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0].additional_items).toHaveLength(1)
|
|
expect(result[0].canceled_at).toBeNull()
|
|
|
|
const return_ = (
|
|
await api.get(
|
|
`/admin/returns/${result[0].return_id}?fields=*fulfillments`,
|
|
adminHeaders
|
|
)
|
|
).data.return
|
|
|
|
expect(return_.fulfillments).toHaveLength(1)
|
|
expect(return_.fulfillments[0].canceled_at).toBeNull()
|
|
|
|
// all exchange return fulfillments should be canceled before canceling the exchange
|
|
await api.post(
|
|
`/admin/fulfillments/${return_.fulfillments[0].id}/cancel`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/cancel`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
result = (
|
|
await api.get(
|
|
`/admin/exchanges?fields=*additional_items`,
|
|
adminHeaders
|
|
)
|
|
).data.exchanges
|
|
expect(result[0].canceled_at).toBeDefined()
|
|
})
|
|
|
|
describe("with inbound and outbound items", () => {
|
|
let exchange
|
|
let orderPreview
|
|
|
|
beforeEach(async () => {
|
|
exchange = (
|
|
await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: order.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.exchange
|
|
|
|
const item = order.items[0]
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchange.id}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: item.id,
|
|
reason_id: returnReason.id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchange.id}/inbound/shipping-method`,
|
|
{ shipping_option_id: returnShippingOption.id },
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchange.id}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productExtra.variants[0].id,
|
|
quantity: 2,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchange.id}/outbound/shipping-method`,
|
|
{ shipping_option_id: outboundShippingOption.id },
|
|
adminHeaders
|
|
)
|
|
|
|
orderPreview = (
|
|
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
|
|
).data.order
|
|
})
|
|
|
|
it("should remove outbound shipping method when outbound items are completely removed", async () => {
|
|
orderPreview = (
|
|
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
|
|
).data.order
|
|
|
|
const exchangeItems = orderPreview.items.filter(
|
|
(item) =>
|
|
!!item.actions?.find((action) => action.action === "ITEM_ADD")
|
|
)
|
|
|
|
const exchangeShippingMethods = orderPreview.shipping_methods.filter(
|
|
(item) =>
|
|
!!item.actions?.find(
|
|
(action) =>
|
|
action.action === "SHIPPING_ADD" && !action.return_id
|
|
)
|
|
)
|
|
|
|
expect(exchangeItems).toHaveLength(1)
|
|
expect(exchangeShippingMethods).toHaveLength(1)
|
|
|
|
await api.delete(
|
|
`/admin/exchanges/${exchange.id}/outbound/items/${exchangeItems[0].actions[0].id}`,
|
|
adminHeaders
|
|
)
|
|
|
|
orderPreview = (
|
|
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
|
|
).data.order
|
|
|
|
const updatedExchangeItems = orderPreview.items.filter(
|
|
(item) =>
|
|
!!item.actions?.find((action) => action.action === "ITEM_ADD")
|
|
)
|
|
|
|
const updatedClaimShippingMethods =
|
|
orderPreview.shipping_methods.filter(
|
|
(item) =>
|
|
!!item.actions?.find(
|
|
(action) =>
|
|
action.action === "SHIPPING_ADD" && !action.return_id
|
|
)
|
|
)
|
|
|
|
expect(updatedExchangeItems).toHaveLength(0)
|
|
expect(updatedClaimShippingMethods).toHaveLength(0)
|
|
})
|
|
|
|
it("should remove inbound shipping method when inbound items are completely removed", async () => {
|
|
orderPreview = (
|
|
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
|
|
).data.order
|
|
|
|
const exchangeItems = orderPreview.items.filter(
|
|
(item) =>
|
|
!!item.actions?.find((action) => action.action === "RETURN_ITEM")
|
|
)
|
|
|
|
const exchangeShippingMethods = orderPreview.shipping_methods.filter(
|
|
(item) =>
|
|
!!item.actions?.find(
|
|
(action) =>
|
|
action.action === "SHIPPING_ADD" && !!action.return_id
|
|
)
|
|
)
|
|
|
|
expect(exchangeItems).toHaveLength(1)
|
|
expect(exchangeShippingMethods).toHaveLength(1)
|
|
|
|
await api.delete(
|
|
`/admin/exchanges/${exchange.id}/inbound/items/${exchangeItems[0].actions[0].id}`,
|
|
adminHeaders
|
|
)
|
|
|
|
orderPreview = (
|
|
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
|
|
).data.order
|
|
|
|
const updatedExchangeItems = orderPreview.items.filter(
|
|
(item) =>
|
|
!!item.actions?.find((action) => action.action === "RETURN_ITEM")
|
|
)
|
|
|
|
const updatedClaimShippingMethods =
|
|
orderPreview.shipping_methods.filter(
|
|
(item) =>
|
|
!!item.actions?.find(
|
|
(action) =>
|
|
action.action === "SHIPPING_ADD" && !!action.return_id
|
|
)
|
|
)
|
|
|
|
expect(updatedExchangeItems).toHaveLength(0)
|
|
expect(updatedClaimShippingMethods).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe("Exchange adjustments", () => {
|
|
let appliedPromotion
|
|
let promotionModule: IPromotionModuleService
|
|
let orderModule: IOrderModuleService
|
|
let remoteLink
|
|
let orderWithPromotion
|
|
let productForAdjustmentTest
|
|
|
|
beforeEach(async () => {
|
|
const container = getContainer()
|
|
promotionModule = container.resolve(Modules.PROMOTION)
|
|
orderModule = container.resolve(Modules.ORDER)
|
|
remoteLink = container.resolve(ContainerRegistrationKeys.LINK)
|
|
|
|
productForAdjustmentTest = (
|
|
await api.post(
|
|
"/admin/products",
|
|
{
|
|
title: "Product for adjustment test",
|
|
status: ProductStatus.PUBLISHED,
|
|
shipping_profile_id: shippingProfile.id,
|
|
options: [{ title: "size", values: ["large", "small"] }],
|
|
variants: [
|
|
{
|
|
title: "Test variant",
|
|
sku: "test-variant-adjustment",
|
|
manage_inventory: false,
|
|
options: { size: "large" },
|
|
prices: [
|
|
{
|
|
currency_code: "usd",
|
|
amount: 12,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
).data.product
|
|
|
|
appliedPromotion = await promotionModule.createPromotions({
|
|
code: "PROMOTION_APPLIED",
|
|
type: PromotionType.STANDARD,
|
|
status: PromotionStatus.ACTIVE,
|
|
application_method: {
|
|
type: "percentage",
|
|
target_type: "order",
|
|
allocation: "each",
|
|
value: 10,
|
|
max_quantity: 5,
|
|
currency_code: "usd",
|
|
target_rules: [],
|
|
},
|
|
})
|
|
|
|
await remoteLink.create([
|
|
{
|
|
[Modules.SALES_CHANNEL]: {
|
|
sales_channel_id: (
|
|
await api.get("/admin/sales-channels", adminHeaders)
|
|
).data.sales_channels[0].id,
|
|
},
|
|
[Modules.STOCK_LOCATION]: {
|
|
stock_location_id: location.id,
|
|
},
|
|
},
|
|
])
|
|
|
|
// @ts-ignore
|
|
orderWithPromotion = await orderModule.createOrders({
|
|
email: "foo@bar.com",
|
|
region_id: (
|
|
await api.get("/admin/regions", adminHeaders)
|
|
).data.regions[0].id,
|
|
sales_channel_id: (
|
|
await api.get("/admin/sales-channels", adminHeaders)
|
|
).data.sales_channels[0].id,
|
|
items: [
|
|
{
|
|
// @ts-ignore
|
|
id: "item-1",
|
|
title: "Custom Item",
|
|
quantity: 1,
|
|
unit_price: 10,
|
|
},
|
|
],
|
|
shipping_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
phone: "12345",
|
|
},
|
|
billing_address: {
|
|
first_name: "Test",
|
|
last_name: "Test",
|
|
address_1: "Test",
|
|
city: "Test",
|
|
country_code: "US",
|
|
postal_code: "12345",
|
|
},
|
|
currency_code: "usd",
|
|
})
|
|
|
|
await orderModule.createOrderLineItemTaxLines(orderWithPromotion.id, [
|
|
{
|
|
// @ts-ignore
|
|
item_id: "item-1",
|
|
code: "standard",
|
|
rate: 10,
|
|
description: "tax-1",
|
|
provider_id: "system",
|
|
total: 1.2,
|
|
subtotal: 1.2,
|
|
},
|
|
])
|
|
|
|
await orderModule.createOrderLineItemAdjustments([
|
|
{
|
|
version: 1,
|
|
code: appliedPromotion.code!,
|
|
amount: 1,
|
|
item_id: "item-1",
|
|
promotion_id: appliedPromotion.id,
|
|
},
|
|
])
|
|
|
|
await remoteLink.create({
|
|
[Modules.ORDER]: { order_id: orderWithPromotion.id },
|
|
[Modules.PROMOTION]: { promotion_id: appliedPromotion.id },
|
|
})
|
|
})
|
|
|
|
it("should update adjustments when adding an inbound and outbound item", async () => {
|
|
// First item -> 10$ | 10% discount tax excl. | 10% tax
|
|
// Second item -> 12$ | 10% discount tax excl. | 2% tax
|
|
|
|
// fulfill item so it can be returned
|
|
await api.post(
|
|
`/admin/orders/${orderWithPromotion.id}/fulfillments`,
|
|
{
|
|
items: [
|
|
{
|
|
id: orderWithPromotion.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
let result = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: orderWithPromotion.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const exchangeId = result.data.exchange.id
|
|
const orderId = result.data.exchange.order_id
|
|
|
|
result = (await api.get(`/admin/orders/${orderId}`, adminHeaders))
|
|
.data.order
|
|
|
|
expect(result.original_total).toEqual(11) // $10 + 10% tax
|
|
expect(result.total).toEqual(10 * 0.9 * 1.1) // ($10 - 10% discount) + 10% tax
|
|
|
|
const orderChange = (
|
|
await api.get(`/admin/orders/${orderId}/preview`, adminHeaders)
|
|
).data.order.order_change
|
|
|
|
// opt in for carry over promotions
|
|
await api.post(
|
|
`/admin/order-changes/${orderChange.id}`,
|
|
{
|
|
carry_over_promotions: true,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// Add outbound item with price $12, 10% discount and 10% tax
|
|
result = (
|
|
await api
|
|
.post(
|
|
`/admin/exchanges/${exchangeId}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
.catch((e) => console.log(e))
|
|
).data.order_preview
|
|
|
|
expect(result.total).toEqual(20.916) // 10 * 0.9 * 1.1 + 12 * 0.9 * 1.02
|
|
expect(result.original_total).toEqual(23.24) // 10 * 1.1 + 12 * 1.02
|
|
|
|
// Confirm that the adjustment values are correct
|
|
const adjustments = result.items[0].adjustments
|
|
const adjustments2 = result.items[1].adjustments
|
|
expect(adjustments).toEqual([
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
])
|
|
expect(adjustments2).toEqual([
|
|
expect.objectContaining({
|
|
amount: 1.2,
|
|
}),
|
|
])
|
|
|
|
let orderResult = (
|
|
await api.get(`/admin/orders/${orderId}`, adminHeaders)
|
|
).data.order
|
|
|
|
// confirm original order is not updated
|
|
expect(orderResult.total).toEqual(9.9) // initial item 10$ and 10% discount and 10% tax
|
|
expect(orderResult.original_total).toEqual(11) // initial item 10$ + 10% tax
|
|
|
|
const originalItemId = result.items[0].id
|
|
|
|
// Request inbound item return
|
|
result = (
|
|
await api
|
|
.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: originalItemId,
|
|
reason_id: returnReason.id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
.catch((e) => console.log(e))
|
|
).data.order_preview
|
|
|
|
const returnId = result.order_change.return_id
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
orderResult = (
|
|
await api.get(`/admin/orders/${orderId}`, adminHeaders)
|
|
).data.order
|
|
|
|
// after exchange request order contains both items and adjustments untill return is received
|
|
expect(orderResult.total).toEqual(20.916) // 10 * 0.9 * 1.1 + 12 * 0.9 * 1.02
|
|
expect(orderResult.original_total).toEqual(23.24) // 10 * 1.1 + 12 * 1.02
|
|
|
|
await api.post(`/admin/returns/${returnId}/receive`, {}, adminHeaders)
|
|
|
|
orderResult = (
|
|
await api.get(`/admin/orders/${orderId}`, adminHeaders)
|
|
).data.order
|
|
|
|
// still the same state while return receive process is pending
|
|
expect(orderResult.total).toEqual(20.916) // 10 * 0.9 * 1.1 + 12 * 0.9 * 1.02
|
|
expect(orderResult.original_total).toEqual(23.24) // 10 * 1.1 + 12 * 1.02
|
|
|
|
await api.post(
|
|
`/admin/returns/${returnId}/receive-items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: originalItemId,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
orderResult = (
|
|
await api.get(`/admin/orders/${orderId}`, adminHeaders)
|
|
).data.order
|
|
|
|
// still the same state while return receive process is pending
|
|
expect(orderResult.total).toEqual(20.916) // 10 * 0.9 * 1.1 + 12 * 0.9 * 1.02
|
|
expect(orderResult.original_total).toEqual(23.24) // 10 * 1.1 + 12 * 1.02
|
|
|
|
await api.post(
|
|
`/admin/returns/${returnId}/receive/confirm`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
const orderResult2 = (
|
|
await api.get(`/admin/orders/${orderId}`, adminHeaders)
|
|
).data.order
|
|
|
|
// after confirmation only first added item is active
|
|
expect(orderResult2.total).toEqual(11.016)
|
|
expect(orderResult2.original_total).toEqual(12.24)
|
|
})
|
|
|
|
it("should enable carry_over_promotions flag and apply promotions to outbound items (flag disabled before request)", async () => {
|
|
// fulfill item so it can be returned
|
|
await api.post(
|
|
`/admin/orders/${orderWithPromotion.id}/fulfillments`,
|
|
{
|
|
items: [
|
|
{
|
|
id: orderWithPromotion.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
let result = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: orderWithPromotion.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const exchangeId = result.data.exchange.id
|
|
|
|
// Query order change for the exchange
|
|
const orderChange = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order.order_change
|
|
|
|
const orderChangeId = orderChange.id
|
|
|
|
// return original item
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: orderWithPromotion.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// add outbound item
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// Initially, promotions should be disabled by default when adding outbound items
|
|
let orderPreview = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
expect(orderPreview.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [], // outbound item has no adjustments initially
|
|
}),
|
|
])
|
|
)
|
|
|
|
// Enable carry_over_promotions
|
|
await api.post(
|
|
`/admin/order-changes/${orderChangeId}`,
|
|
{
|
|
carry_over_promotions: true,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// Verify adjustments are added
|
|
orderPreview = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
expect(orderPreview.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [
|
|
// outbound item has adjustments after carry_over_promotions is enabled
|
|
expect.objectContaining({
|
|
amount: 1.2,
|
|
}),
|
|
],
|
|
}),
|
|
])
|
|
)
|
|
|
|
// Disable carry_over_promotions
|
|
await api.post(
|
|
`/admin/order-changes/${orderChangeId}`,
|
|
{
|
|
carry_over_promotions: false,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// Verify adjustments are removed again
|
|
orderPreview = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
expect(orderPreview.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [], // outbound item has no adjustments
|
|
}),
|
|
])
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
const finalOrder = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
// items adjustment state is equal to the last state of the order preview (flag disabled)
|
|
expect(finalOrder.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [],
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
|
|
it("should enable carry_over_promotions flag and apply promotions to outbound items (flag enabled before request)", async () => {
|
|
// fulfill item so it can be returned
|
|
await api.post(
|
|
`/admin/orders/${orderWithPromotion.id}/fulfillments`,
|
|
{
|
|
items: [
|
|
{
|
|
id: orderWithPromotion.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
let result = await api.post(
|
|
"/admin/exchanges",
|
|
{
|
|
order_id: orderWithPromotion.id,
|
|
description: "Test",
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
const exchangeId = result.data.exchange.id
|
|
|
|
// Query order change for the exchange
|
|
const orderChange = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order.order_change
|
|
|
|
const orderChangeId = orderChange.id
|
|
|
|
// return original item
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/inbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
id: orderWithPromotion.items[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// add outbound item
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/outbound/items`,
|
|
{
|
|
items: [
|
|
{
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
quantity: 1,
|
|
},
|
|
],
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
let orderPreview = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
expect(orderPreview.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [], // outbound item has no adjustments initially
|
|
}),
|
|
])
|
|
)
|
|
|
|
// Enable carry_over_promotions
|
|
await api.post(
|
|
`/admin/order-changes/${orderChangeId}`,
|
|
{
|
|
carry_over_promotions: true,
|
|
},
|
|
adminHeaders
|
|
)
|
|
|
|
// Verify adjustments are added
|
|
orderPreview = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}/preview`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
expect(orderPreview.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [
|
|
// outbound item has adjustments after carry_over_promotions is enabled
|
|
expect.objectContaining({
|
|
amount: 1.2,
|
|
}),
|
|
],
|
|
}),
|
|
])
|
|
)
|
|
|
|
await api.post(
|
|
`/admin/exchanges/${exchangeId}/request`,
|
|
{},
|
|
adminHeaders
|
|
)
|
|
|
|
const finalOrder = (
|
|
await api.get(
|
|
`/admin/orders/${orderWithPromotion.id}`,
|
|
adminHeaders
|
|
)
|
|
).data.order
|
|
|
|
// items adjustment state is equal to the last state of the order preview (flag enabled)
|
|
expect(finalOrder.items).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "item-1", // original item
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1,
|
|
}),
|
|
],
|
|
}),
|
|
expect.objectContaining({
|
|
variant_id: productForAdjustmentTest.variants[0].id,
|
|
adjustments: [
|
|
expect.objectContaining({
|
|
amount: 1.2,
|
|
}),
|
|
],
|
|
}),
|
|
])
|
|
)
|
|
})
|
|
})
|
|
})
|
|
},
|
|
})
|