feat(core-flows, types): Create return order (#7319)

**what**
- Create return workflow partial implementation
- Update some order domain types
- create order fulfillment link

**NOTE**
this PR is partially done but can still be merged as is, it will require some discussions around the flow and some unknowns or uncertainty in regards to some data and some behaviour
This commit is contained in:
Adrien de Peretti
2024-05-16 16:10:54 +02:00
committed by GitHub
parent 07e5c17f86
commit a775d57255
24 changed files with 1016 additions and 21 deletions
@@ -0,0 +1,473 @@
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
import {
FulfillmentWorkflow,
IOrderModuleService,
IRegionModuleService,
IStockLocationServiceNext,
OrderWorkflow,
ProductDTO,
RegionDTO,
ShippingOptionDTO,
StockLocationDTO,
} from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
import {
createReturnOrderWorkflow,
createShippingOptionsWorkflow,
} from "@medusajs/core-flows"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
RuleOperator,
} from "@medusajs/utils"
jest.setTimeout(500000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
const providerId = "manual_test-provider"
async function prepareDataFixtures({ container }) {
const fulfillmentService = container.resolve(
ModuleRegistrationName.FULFILLMENT
)
const salesChannelService = container.resolve(
ModuleRegistrationName.SALES_CHANNEL
)
const stockLocationModule: IStockLocationServiceNext = container.resolve(
ModuleRegistrationName.STOCK_LOCATION
)
const productModule = container.resolve(ModuleRegistrationName.PRODUCT)
const inventoryModule = container.resolve(ModuleRegistrationName.INVENTORY)
const shippingProfile = await fulfillmentService.createShippingProfiles({
name: "test",
type: "default",
})
const fulfillmentSet = await fulfillmentService.create({
name: "Test fulfillment set",
type: "manual_test",
})
const serviceZone = await fulfillmentService.createServiceZones({
name: "Test service zone",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: "country",
country_code: "US",
},
],
})
const regionService = container.resolve(
ModuleRegistrationName.REGION
) as IRegionModuleService
const [region] = await regionService.create([
{
name: "Test region",
currency_code: "eur",
countries: ["fr"],
},
])
const salesChannel = await salesChannelService.create({
name: "Webshop",
})
const location: StockLocationDTO = await stockLocationModule.create({
name: "Warehouse",
address: {
address_1: "Test",
city: "Test",
country_code: "US",
postal_code: "12345",
phone: "12345",
},
})
const [product] = await productModule.create([
{
title: "Test product",
variants: [
{
title: "Test variant",
sku: "test-variant",
},
],
},
])
const inventoryItem = await inventoryModule.create({
sku: "inv-1234",
})
await inventoryModule.createInventoryLevels([
{
inventory_item_id: inventoryItem.id,
location_id: location.id,
stocked_quantity: 2,
reserved_quantity: 0,
},
])
const remoteLink = container.resolve(ContainerRegistrationKeys.REMOTE_LINK)
await remoteLink.create([
{
[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,
},
},
])
const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput =
{
name: "Return shipping option",
price_type: "flat",
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
provider_id: providerId,
type: {
code: "manual-type",
label: "Manual Type",
description: "Manual Type Description",
},
prices: [
{
currency_code: "usd",
amount: 10,
},
{
region_id: region.id,
amount: 100,
},
],
rules: [
{
attribute: "is_return",
operator: RuleOperator.EQ,
value: '"true"',
},
],
}
const { result } = await createShippingOptionsWorkflow(container).run({
input: [shippingOptionData],
})
const remoteQueryObject = remoteQueryObjectFromString({
entryPoint: "shipping_option",
variables: {
id: result[0].id,
},
fields: [
"id",
"name",
"price_type",
"service_zone_id",
"shipping_profile_id",
"provider_id",
"data",
"metadata",
"type.*",
"created_at",
"updated_at",
"deleted_at",
"shipping_option_type_id",
"prices.*",
],
})
const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const [createdShippingOption] = await remoteQuery(remoteQueryObject)
return {
shippingOption: createdShippingOption,
region,
salesChannel,
location,
product,
}
}
async function createOrderFixture({ container, product }) {
const orderService: IOrderModuleService = container.resolve(
ModuleRegistrationName.ORDER
)
let order = await orderService.create({
region_id: "test_region_idclear",
email: "foo@bar.com",
items: [
{
title: "Custom Item 2",
variant_sku: product.variants[0].sku,
variant_title: product.variants[0].title,
quantity: 1,
unit_price: 50,
adjustments: [
{
code: "VIP_25 ETH",
amount: "0.000000000000000005",
description: "VIP discount",
promotion_id: "prom_123",
provider_id: "coupon_kings",
},
],
} as any,
],
transactions: [
{
amount: 50, // TODO: check calculation, I think it should be 60 wit the shipping but the order total is 50
currency_code: "usd",
},
],
sales_channel_id: "test",
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,
},
],
adjustments: [
{
code: "VIP_10",
amount: 1,
description: "VIP discount",
promotion_id: "prom_123",
},
],
},
],
currency_code: "usd",
customer_id: "joe",
})
await orderService.addOrderAction([
{
action: "FULFILL_ITEM",
order_id: order.id,
version: order.version,
reference: "fullfilment",
reference_id: "fulfill_123",
details: {
reference_id: order.items![0].id,
quantity: 1,
},
},
{
action: "SHIP_ITEM",
order_id: order.id,
version: order.version,
reference: "fullfilment",
reference_id: "fulfill_123",
details: {
reference_id: order.items![0].id,
quantity: 1,
},
},
])
await orderService.applyPendingOrderActions(order.id)
order = await orderService.retrieve(order.id, {
relations: ["items"],
})
return order
}
medusaIntegrationTestRunner({
env,
testSuite: ({ getContainer }) => {
let container
beforeAll(() => {
container = getContainer()
})
describe("Create return order workflow", () => {
let shippingOption: ShippingOptionDTO
let region: RegionDTO
let location: StockLocationDTO
let product: ProductDTO
let orderService: IOrderModuleService
beforeEach(async () => {
const fixtures = await prepareDataFixtures({
container,
})
shippingOption = fixtures.shippingOption
region = fixtures.region
location = fixtures.location
product = fixtures.product
orderService = container.resolve(ModuleRegistrationName.ORDER)
})
it("should create a return order", async () => {
const order = await createOrderFixture({ container, product })
const createReturnOrderData: OrderWorkflow.CreateOrderReturnWorkflowInput =
{
order_id: order.id,
return_shipping: {
option_id: shippingOption.id,
},
items: [
{
id: order.items![0].id,
quantity: 1,
},
],
}
await createReturnOrderWorkflow(container).run({
input: createReturnOrderData,
throwOnError: false,
})
const remoteQuery = container.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const remoteQueryObject = remoteQueryObjectFromString({
entryPoint: "order",
variables: {
id: order.id,
},
fields: [
"*",
"items.*",
"shipping_methods.*",
"total",
"item_total",
"fulfillments.*",
],
})
const [returnOrder] = await remoteQuery(remoteQueryObject)
expect(returnOrder).toEqual(
expect.objectContaining({
id: expect.any(String),
display_id: 1,
region_id: "test_region_idclear",
customer_id: "joe",
version: 2,
sales_channel_id: "test", // TODO: What about order with a sales channel but a shipping option link to a stock from another channel?
status: "pending",
is_draft_order: false,
email: "foo@bar.com",
currency_code: "usd",
shipping_address_id: expect.any(String),
billing_address_id: expect.any(String),
items: [
expect.objectContaining({
id: order.items![0].id,
title: "Custom Item 2",
variant_sku: product.variants[0].sku,
variant_title: product.variants[0].title,
requires_shipping: true,
is_discountable: true,
is_tax_inclusive: false,
compare_at_unit_price: null,
unit_price: 50,
quantity: 1,
detail: expect.objectContaining({
id: expect.any(String),
order_id: expect.any(String),
version: 2,
item_id: expect.any(String),
quantity: 1,
fulfilled_quantity: 1,
shipped_quantity: 1,
return_requested_quantity: 1,
return_received_quantity: 0,
return_dismissed_quantity: 0,
written_off_quantity: 0,
}),
}),
],
shipping_methods: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
name: "Test shipping method",
description: null,
is_tax_inclusive: false,
shipping_option_id: null,
amount: 10,
order_id: expect.any(String),
}),
expect.objectContaining({
id: expect.any(String),
name: shippingOption.name,
description: null,
is_tax_inclusive: false,
shipping_option_id: shippingOption.id,
amount: 10,
order_id: expect.any(String),
}),
]),
fulfillments: [
expect.objectContaining({
id: expect.any(String),
location_id: location.id,
provider_id: providerId,
shipping_option_id: shippingOption.id,
// TODO: Validate the address once we are fixed on it
/*delivery_address: {
id: "fuladdr_01HY0RTAP0P1EEAFK7BXJ0BKBN",
},*/
}),
],
})
)
})
})
},
})