feat(*): shipping options update API (#6987)

**What**
- Update the `updateShippingOptions` module service API to follow the newest convention
- Add upsert support for shipping options
- update shipping options workflow
- update shipping options api end point and validation
- update shipping options all integration tests

FIXES CORE-1926
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-04-07 16:28:59 +00:00
1 parent f65fbff535
commit f132929c7e
20 files changed
+916 -74

No files matched your search

@@ -1,43 +0,0 @@
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import {
FulfillmentWorkflow,
IFulfillmentModuleService,
ShippingOptionDTO,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
type StepInput = Omit<
FulfillmentWorkflow.CreateShippingOptionsWorkflowInput,
"prices"
>[]
export const createShippingOptionsStepId = "create-shipping-options-step"
export const createShippingOptionsStep = createStep(
createShippingOptionsStepId,
async (input: StepInput, { container }) => {
if (!input?.length) {
return new StepResponse([], [])
}
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
const createdShippingOptions: ShippingOptionDTO[] =
await fulfillmentService.createShippingOptions(input)
const shippingOptionIds = createdShippingOptions.map((s) => s.id)
return new StepResponse(createdShippingOptions, shippingOptionIds)
},
async (shippingOptionIds, { container }) => {
if (!shippingOptionIds?.length) {
return
}
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
await fulfillmentService.deleteShippingOptions(shippingOptionIds)
}
)
@@ -2,6 +2,6 @@ export * from "./add-rules-to-fulfillment-shipping-option"
export * from "./add-shipping-options-prices"
export * from "./create-fulfillment-set"
export * from "./create-service-zones"
export * from "./create-shipping-options"
export * from "./upsert-shipping-options"
export * from "./delete-service-zones"
export * from "./remove-rules-from-fulfillment-shipping-option"
@@ -0,0 +1,100 @@
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import {
FulfillmentWorkflow,
IFulfillmentModuleService,
ShippingOptionDTO,
} from "@medusajs/types"
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
arrayDifference,
getSelectsAndRelationsFromObjectArray,
} from "@medusajs/utils"
import { UpsertShippingOptionDTO } from "@medusajs/types/src"
type StepInput = Omit<
| FulfillmentWorkflow.CreateShippingOptionsWorkflowInput
| FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput,
"prices"
>[]
export const upsertShippingOptionsStepId = "create-shipping-options-step"
export const upsertShippingOptionsStep = createStep(
upsertShippingOptionsStepId,
async (input: StepInput, { container }) => {
if (!input?.length) {
return new StepResponse([], {})
}
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
const toUpdate: FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[] =
[]
;(
input as FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[]
).forEach((inputItem) => {
if (!!inputItem.id) {
return toUpdate.push(inputItem)
}
return
})
let toUpdatePreviousData: ShippingOptionDTO[] = []
if (toUpdate.length) {
const { selects, relations } =
getSelectsAndRelationsFromObjectArray(toUpdate)
toUpdatePreviousData = await fulfillmentService.listShippingOptions(
{
id: toUpdate.map((s) => s.id),
},
{
select: selects,
relations,
}
)
}
const upsertedShippingOptions: ShippingOptionDTO[] =
await fulfillmentService.upsertShippingOptions(
input as UpsertShippingOptionDTO[]
)
const upsertedShippingOptionIds = upsertedShippingOptions.map((s) => s.id)
const updatedIds = toUpdate.map((s) => s.id)
return new StepResponse(upsertedShippingOptions, {
updatedPreviousData: toUpdatePreviousData,
createdIds: arrayDifference(
upsertedShippingOptionIds,
updatedIds
) as string[],
})
},
async (shippingOptionIds: any, { container }) => {
if (
!shippingOptionIds?.updatedPreviousData?.length &&
!shippingOptionIds?.createdIds?.length
) {
return
}
const fulfillmentService = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
if (shippingOptionIds.updatedPreviousData.length) {
await fulfillmentService.upsertShippingOptions(
shippingOptionIds.updatedPreviousData
)
}
if (shippingOptionIds.createdIds.length) {
await fulfillmentService.deleteShippingOptions(
shippingOptionIds.createdIds
)
}
}
)
@@ -6,7 +6,7 @@ import {
} from "@medusajs/workflows-sdk"
import {
createShippingOptionsPriceSetsStep,
createShippingOptionsStep,
upsertShippingOptionsStep,
} from "../steps"
import { setShippingOptionsPriceSetsStep } from "../steps/set-shipping-options-price-sets"
import { createPricingRuleTypesStep } from "../../pricing"
@@ -34,7 +34,7 @@ export const createShippingOptionsWorkflow = createWorkflow(
}
})
const createdShippingOptions = createShippingOptionsStep(
const createdShippingOptions = upsertShippingOptionsStep(
data.shippingOptions
)
@@ -1,6 +1,7 @@
export * from "./add-rules-to-fulfillment-shipping-option"
export * from "./create-service-zones"
export * from "./create-shipping-options"
export * from "./update-shipping-options"
export * from "./delete-service-zones"
export * from "./remove-rules-from-fulfillment-shipping-option"
export * from "./update-service-zones"
@@ -0,0 +1,92 @@
import { FulfillmentWorkflow } from "@medusajs/types"
import {
createWorkflow,
transform,
WorkflowData,
} from "@medusajs/workflows-sdk"
import { upsertShippingOptionsStep } from "../steps"
export const updateShippingOptionsWorkflowId =
"update-shipping-options-workflow"
export const updateShippingOptionsWorkflow = createWorkflow(
updateShippingOptionsWorkflowId,
(
input: WorkflowData<
FulfillmentWorkflow.UpdateShippingOptionsWorkflowInput[]
>
): WorkflowData<FulfillmentWorkflow.UpdateShippingOptionsWorkflowOutput> => {
const data = transform(input, (data) => {
const shippingOptionsIndexToPrices = data.map((option, index) => {
return {
shipping_option_index: index,
prices: option.prices,
}
})
return {
shippingOptions: data,
shippingOptionsIndexToPrices,
}
})
const updatedShippingOptions = upsertShippingOptionsStep(
data.shippingOptions
)
/*const normalizedShippingOptionsPrices = transform(
{
shippingOptions: updatedShippingOptions,
shippingOptionsIndexToPrices: data.shippingOptionsIndexToPrices,
},
(data) => {
const ruleTypes = new Set<Partial<RuleTypeDTO>>()
const shippingOptionsPrices = data.shippingOptionsIndexToPrices.map(
({ shipping_option_index, prices }) => {
prices?.forEach((price) => {
if ("region_id" in price) {
ruleTypes.add({
name: "region_id",
rule_attribute: "region_id",
})
}
})
return {
id: data.shippingOptions[shipping_option_index].id,
prices,
}
}
)
return {
shippingOptionsPrices,
ruleTypes: Array.from(ruleTypes) as UpdateRuleTypeDTO[],
}
}
)*/
/*updatePricingRuleTypesStep(normalizedShippingOptionsPrices.ruleTypes)*/
/*const shippingOptionsPriceSetsLinkData = updateShippingOptionsPriceSetsStep(
normalizedShippingOptionsPrices.shippingOptionsPrices
)
const normalizedLinkData = transform(
{
shippingOptionsPriceSetsLinkData,
},
(data) => {
return data.shippingOptionsPriceSetsLinkData.map((item) => {
return {
id: item.id,
price_sets: [item.priceSetId],
}
})
}
)*/
/*setShippingOptionsPriceSetsStep(normalizedLinkData)*/
return updatedShippingOptions
}
)
@@ -9,6 +9,7 @@ import { resolve } from "path"
import { FulfillmentProviderService } from "@services"
import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers"
import { GeoZoneType } from "@medusajs/utils"
import { UpdateShippingOptionDTO } from "@medusajs/types/src"
jest.setTimeout(100000)
@@ -592,7 +593,7 @@ moduleIntegrationTestRunner({
shippingOptionData
)
const updateData = {
const updateData: UpdateShippingOptionDTO = {
id: shippingOption.id,
name: "updated-test",
price_type: "calculated",
@@ -617,6 +618,7 @@ moduleIntegrationTestRunner({
}
const updatedShippingOption = await service.updateShippingOptions(
updateData.id!,
updateData
)
@@ -690,7 +692,7 @@ moduleIntegrationTestRunner({
shippingOptionData
)
const updateData = {
const updateData: Partial<UpdateShippingOptionDTO> = {
id: shippingOption.id,
name: "updated-test",
price_type: "calculated",
@@ -702,7 +704,7 @@ moduleIntegrationTestRunner({
},
}
await service.updateShippingOptions(updateData)
await service.updateShippingOptions(updateData.id!, updateData)
const updatedShippingOption = await service.retrieveShippingOption(
shippingOption.id,
@@ -837,7 +839,7 @@ moduleIntegrationTestRunner({
},
]
const updatedShippingOption = await service.updateShippingOptions(
const updatedShippingOption = await service.upsertShippingOptions(
updateData
)
@@ -943,7 +945,7 @@ moduleIntegrationTestRunner({
}
const err = await service
.updateShippingOptions(shippingOptionData)
.updateShippingOptions(shippingOptionData.id!, shippingOptionData)
.catch((e) => e)
expect(err).toBeDefined()
@@ -988,7 +990,7 @@ moduleIntegrationTestRunner({
]
const err = await service
.updateShippingOptions(updateData)
.updateShippingOptions(updateData[0].id!, updateData[0])
.catch((e) => e)
expect(err).toBeDefined()
@@ -1035,7 +1037,7 @@ moduleIntegrationTestRunner({
]
const err = await service
.updateShippingOptions(updateData)
.updateShippingOptions(updateData[0].id!, updateData[0])
.catch((e) => e)
expect(err).toBeDefined()
@@ -1,4 +1,3 @@
import { Modules } from "@medusajs/modules-sdk"
import {
Context,
DAL,
@@ -26,6 +25,7 @@ import {
getSetDifference,
isString,
promiseAll,
Modules
} from "@medusajs/utils"
import {
Fulfillment,
@@ -40,6 +40,7 @@ import {
import { isContextValid, validateRules } from "@utils"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import FulfillmentProviderService from "./fulfillment-provider"
import { UpdateShippingOptionsInput } from "../types/service"
const generateMethodForModels = [
ServiceZone,
@@ -1046,40 +1047,56 @@ export default class FulfillmentModuleService<
}
updateShippingOptions(
data: FulfillmentTypes.UpdateShippingOptionDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
updateShippingOptions(
id: string,
data: FulfillmentTypes.UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO>
updateShippingOptions(
selector: FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
@InjectManager("baseRepository_")
async updateShippingOptions(
data:
| FulfillmentTypes.UpdateShippingOptionDTO[]
| FulfillmentTypes.UpdateShippingOptionDTO,
idOrSelector: string | FulfillmentTypes.FilterableShippingOptionProps,
data: FulfillmentTypes.UpdateShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
> {
const normalizedInput: UpdateShippingOptionsInput[] = []
if (isString(idOrSelector)) {
normalizedInput.push({ id: idOrSelector, ...data })
} else {
const shippingOptions = await this.shippingOptionService_.list(
idOrSelector,
{},
sharedContext
)
shippingOptions.forEach((shippingOption) => {
normalizedInput.push({ id: shippingOption.id, ...data })
})
}
const updatedShippingOptions = await this.updateShippingOptions_(
data,
normalizedInput,
sharedContext
)
return await this.baseRepository_.serialize<
const serialized = await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
>(updatedShippingOptions, {
populate: true,
})
return isString(idOrSelector) ? serialized[0] : serialized
}
@InjectTransactionManager("baseRepository_")
async updateShippingOptions_(
data:
| FulfillmentTypes.UpdateShippingOptionDTO[]
| FulfillmentTypes.UpdateShippingOptionDTO,
data: UpdateShippingOptionsInput[] | UpdateShippingOptionsInput,
@MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionEntity | TShippingOptionEntity[]> {
const dataArray = Array.isArray(data) ? data : [data]
@@ -1182,6 +1199,82 @@ export default class FulfillmentModuleService<
: updatedShippingOptions[0]
}
async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO[],
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO[]>
async upsertShippingOptions(
data: FulfillmentTypes.UpsertShippingOptionDTO,
sharedContext?: Context
): Promise<FulfillmentTypes.ShippingOptionDTO>
@InjectManager("baseRepository_")
async upsertShippingOptions(
data:
| FulfillmentTypes.UpsertShippingOptionDTO[]
| FulfillmentTypes.UpsertShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
> {
const upsertedShippingOptions = await this.upsertShippingOptions_(
data,
sharedContext
)
const allShippingOptions = await this.baseRepository_.serialize<
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
>(upsertedShippingOptions)
return Array.isArray(data) ? allShippingOptions : allShippingOptions[0]
}
@InjectTransactionManager("baseRepository_")
async upsertShippingOptions_(
data:
| FulfillmentTypes.UpsertShippingOptionDTO[]
| FulfillmentTypes.UpsertShippingOptionDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<TShippingOptionEntity[] | TShippingOptionEntity> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(shippingOption): shippingOption is UpdateShippingOptionsInput =>
!!shippingOption.id
)
const forCreate = input.filter(
(
shippingOption
): shippingOption is FulfillmentTypes.CreateShippingOptionDTO =>
!shippingOption.id
)
let created: TShippingOptionEntity[] = []
let updated: TShippingOptionEntity[] = []
if (forCreate.length) {
const createdShippingOptions = await this.createShippingOptions_(
forCreate,
sharedContext
)
const toPush = Array.isArray(createdShippingOptions)
? createdShippingOptions
: [createdShippingOptions]
created.push(...toPush)
}
if (forUpdate.length) {
const updatedShippingOptions = await this.updateShippingOptions_(
forUpdate,
sharedContext
)
const toPush = Array.isArray(updatedShippingOptions)
? updatedShippingOptions
: [updatedShippingOptions]
updated.push(...toPush)
}
return [...created, ...updated]
}
updateShippingProfiles(
data: FulfillmentTypes.UpdateShippingProfileDTO[],
sharedContext?: Context
@@ -1415,7 +1508,7 @@ export default class FulfillmentModuleService<
protected static validateMissingShippingOptions_(
shippingOptions: ShippingOption[],
shippingOptionsData: FulfillmentTypes.UpdateShippingOptionDTO[]
shippingOptionsData: UpdateShippingOptionsInput[]
) {
const missingShippingOptionIds = arrayDifference(
shippingOptionsData.map((s) => s.id),
@@ -0,0 +1,6 @@
import { FulfillmentTypes } from "@medusajs/types"
export type UpdateShippingOptionsInput = Required<
Pick<FulfillmentTypes.UpdateShippingOptionDTO, "id">
> &
FulfillmentTypes.UpdateShippingOptionDTO
@@ -0,0 +1,44 @@
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { AdminShippingOptionRetrieveResponse } from "@medusajs/types"
import { AdminUpdateShippingOptionType } from "../validators"
import { updateShippingOptionsWorkflow } from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../../types/routing"
export const POST = async (
req: AuthenticatedMedusaRequest<AdminUpdateShippingOptionType>,
res: MedusaResponse<AdminShippingOptionRetrieveResponse>
) => {
const shippingOptionPayload = req.validatedBody
const workflow = updateShippingOptionsWorkflow(req.scope)
const { result, errors } = await workflow.run({
input: [shippingOptionPayload],
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
const shippingOptionId = result[0].id
const query = remoteQueryObjectFromString({
entryPoint: "shipping_options",
variables: {
id: shippingOptionId,
},
fields: req.remoteQueryConfig.fields,
})
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const [shippingOption] = await remoteQuery(query)
res.status(200).json({ shipping_option: shippingOption })
}
@@ -5,6 +5,7 @@ import {
AdminGetShippingOptionParams,
AdminShippingOptionRulesBatchAdd,
AdminShippingOptionRulesBatchRemove,
AdminUpdateShippingOption,
} from "./validators"
import { retrieveTransformQueryConfig } from "./query-config"
import { validateAndTransformBody } from "../../utils/validate-body"
@@ -28,6 +29,18 @@ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
],
},
{
method: ["POST"],
matcher: "/admin/shipping-options/:id",
middlewares: [
validateAndTransformQuery(
AdminGetShippingOptionParams,
retrieveTransformQueryConfig
),
validateAndTransformBody(AdminUpdateShippingOption),
],
},
{
method: ["POST"],
matcher: "/admin/shipping-options/:id/rules/batch/add",
@@ -43,7 +43,7 @@ export type AdminShippingOptionRulesBatchRemoveType = z.infer<
* SHIPPING OPTIONS
*/
export const AdminCreateShippingOptionType = z
export const AdminCreateShippingOptionTypeObject = z
.object({
label: z.string(),
description: z.string(),
@@ -74,7 +74,7 @@ export const AdminCreateShippingOption = z
data: z.record(z.unknown()).optional(),
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum),
provider_id: z.string(),
type: AdminCreateShippingOptionType,
type: AdminCreateShippingOptionTypeObject,
prices: AdminCreateShippingOptionPriceWithCurrency.or(
AdminCreateShippingOptionPriceWithRegion
).array(),
@@ -85,3 +85,18 @@ export const AdminCreateShippingOption = z
export type AdminCreateShippingOptionType = z.infer<
typeof AdminCreateShippingOption
>
export const AdminUpdateShippingOption = z
.object({
id: z.string(),
name: z.string().optional(),
data: z.record(z.unknown()).optional(),
price_type: z.nativeEnum(ShippingOptionPriceTypeEnum).optional(),
provider_id: z.string().optional(),
type: AdminCreateShippingOptionTypeObject.optional(),
})
.strict()
export type AdminUpdateShippingOptionType = z.infer<
typeof AdminUpdateShippingOption
>
@@ -14,7 +14,7 @@ export interface CreateShippingOptionDTO {
}
export interface UpdateShippingOptionDTO {
id: string
id?: string
name?: string
price_type?: ShippingOptionPriceType
service_zone_id?: string
@@ -27,3 +27,7 @@ export interface UpdateShippingOptionDTO {
| { id: string }
)[]
}
export interface UpsertShippingOptionDTO extends UpdateShippingOptionDTO {
}
+21 -4
View File
@@ -35,6 +35,7 @@ import {
UpdateShippingOptionRuleDTO,
UpdateShippingProfileDTO,
UpsertServiceZoneDTO,
UpsertShippingOptionDTO,
} from "./mutations"
import { CreateFulfillmentDTO } from "./mutations/fulfillment"
import { CreateShippingProfileDTO } from "./mutations/shipping-profile"
@@ -376,19 +377,35 @@ export interface IFulfillmentModuleService extends IModuleService {
data: CreateShippingOptionDTO,
sharedContext?: Context
): Promise<ShippingOptionDTO>
/**
* Update a shipping option
* @param id
* @param data
* @param sharedContext
*/
updateShippingOptions(
data: UpdateShippingOptionDTO[],
sharedContext?: Context
): Promise<ShippingOptionDTO[]>
updateShippingOptions(
id: string,
data: UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<ShippingOptionDTO>
updateShippingOptions(
selector: FilterableShippingOptionProps,
data: UpdateShippingOptionDTO,
sharedContext?: Context
): Promise<ShippingOptionDTO[]>
/**
* Upsert a shipping option
*/
upsertShippingOptions(
data: UpsertShippingOptionDTO,
sharedContext?: Context
): Promise<ShippingOptionDTO>
upsertShippingOptions(
data: UpsertShippingOptionDTO[],
sharedContext?: Context
): Promise<ShippingOptionDTO[]>
/**
* Delete a shippingOption
* @param ids
@@ -1,2 +1,3 @@
export * from "./create-shipping-options"
export * from "./update-shipping-options"
export * from "./service-zones"
@@ -0,0 +1,36 @@
import { ShippingOptionPriceType } from "../../fulfillment"
import { RuleOperatorType } from "../../common"
export interface UpdateShippingOptionsWorkflowInput {
id: string
name?: string
service_zone_id?: string
shipping_profile_id?: string
data?: Record<string, unknown>
price_type?: ShippingOptionPriceType
provider_id?: string
type?: {
label: string
description: string
code: string
}
prices?: (
| {
currency_code: string
amount: number
}
| {
region_id: string
amount: number
}
)[]
rules?: {
attribute: string
operator: RuleOperatorType
value: string | string[]
}[]
}
export type UpdateShippingOptionsWorkflowOutput = {
id: string
}[]