feat: delete shipping options (#6993)

This commit is contained in:
Adrien de Peretti
2024-04-09 15:30:22 +02:00
committed by GitHub
parent 21be6ff7ed
commit e1a0960e20
18 changed files with 524 additions and 38 deletions

View File

@@ -237,8 +237,7 @@ medusaIntegrationTestRunner({
const shippingOptionId = response.data.shipping_option.id
const updateShippingOptionPayload = {
}
const updateShippingOptionPayload = {}
let err = await api
.post(
@@ -337,6 +336,54 @@ medusaIntegrationTestRunner({
)
})
})
describe("DELETE /admin/shipping-options/:id", () => {
it("should delete a shipping option successfully", async () => {
const shippingOptionPayload = {
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
prices: [
{
currency_code: "usd",
amount: 1000,
},
{
region_id: region.id,
amount: 1000,
},
],
rules: [shippingOptionRule],
}
const response = await api.post(
`/admin/shipping-options`,
shippingOptionPayload,
adminHeaders
)
const shippingOptionId = response.data.shipping_option.id
await api.delete(
`/admin/shipping-options/${shippingOptionId}`,
adminHeaders
)
const shippingOptions = await api.get(
`/admin/shipping-options`,
adminHeaders
)
expect(shippingOptions.data.shipping_options).toHaveLength(0)
})
})
})
},
})

View File

@@ -0,0 +1,263 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import {
FulfillmentSetDTO,
FulfillmentWorkflow,
IFulfillmentModuleService,
IRegionModuleService,
ServiceZoneDTO,
ShippingProfileDTO,
} from "@medusajs/types"
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
import {
createShippingOptionsWorkflow,
deleteShippingOptionsWorkflow,
} from "@medusajs/core-flows"
import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
RuleOperator,
} from "@medusajs/utils"
jest.setTimeout(100000)
const env = { MEDUSA_FF_MEDUSA_V2: true }
const provider_id = "manual_test-provider"
medusaIntegrationTestRunner({
env,
testSuite: ({ getContainer }) => {
let service: IFulfillmentModuleService
let container
beforeAll(() => {
container = getContainer()
service = container.resolve(ModuleRegistrationName.FULFILLMENT)
})
describe("Fulfillment workflows", () => {
let fulfillmentSet: FulfillmentSetDTO
let serviceZone: ServiceZoneDTO
let shippingProfile: ShippingProfileDTO
beforeEach(async () => {
shippingProfile = await service.createShippingProfiles({
name: "test",
type: "default",
})
fulfillmentSet = await service.create({
name: "Test fulfillment set",
type: "manual_test",
})
serviceZone = await service.createServiceZones({
name: "Test service zone",
fulfillment_set_id: fulfillmentSet.id,
geo_zones: [
{
type: "country",
country_code: "US",
},
],
})
})
it("should delete shipping options", async () => {
const regionService = container.resolve(
ModuleRegistrationName.REGION
) as IRegionModuleService
const [region] = await regionService.create([
{
name: "Test region",
currency_code: "eur",
countries: ["fr"],
},
])
const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput =
{
name: "Test shipping option",
price_type: "flat",
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
provider_id,
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: "total",
operator: RuleOperator.EQ,
value: "100",
},
],
}
const { result } = await createShippingOptionsWorkflow(container).run({
input: [shippingOptionData],
})
await deleteShippingOptionsWorkflow(container).run({
input: { ids: [result[0].id] },
})
const remoteQuery = container.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
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 createdShippingOption = await remoteQuery(remoteQueryObject)
expect(createdShippingOption).toHaveLength(0)
})
it("should revert the deleted shipping options", async () => {
const regionService = container.resolve(
ModuleRegistrationName.REGION
) as IRegionModuleService
const [region] = await regionService.create([
{
name: "Test region",
currency_code: "eur",
countries: ["fr"],
},
])
const shippingOptionData: FulfillmentWorkflow.CreateShippingOptionsWorkflowInput =
{
name: "Test shipping option",
price_type: "flat",
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
provider_id,
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: "total",
operator: RuleOperator.EQ,
value: "100",
},
],
}
const deleteWorkflow = await deleteShippingOptionsWorkflow(container)
deleteWorkflow.addAction(
"throw",
{
invoke: async function failStep() {
throw new Error(`Failed to delete shipping options`)
},
},
{
noCompensation: true,
}
)
const { result } = await createShippingOptionsWorkflow(container).run({
input: [shippingOptionData],
})
const { errors } = await deleteWorkflow.run({
input: { ids: [result[0].id] },
throwOnError: false,
})
expect(errors).toHaveLength(1)
expect(errors[0].error.message).toEqual(
`Failed to delete shipping options`
)
const remoteQuery = container.resolve(
ContainerRegistrationKeys.REMOTE_QUERY
)
const remoteQueryObject = remoteQueryObjectFromString({
entryPoint: "shipping_option",
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",
],
})
const createdShippingOptions = await remoteQuery(remoteQueryObject)
expect(createdShippingOptions).toHaveLength(1)
expect(createdShippingOptions[0]).toEqual(
expect.objectContaining({
name: shippingOptionData.name,
price_type: shippingOptionData.price_type,
service_zone_id: serviceZone.id,
shipping_profile_id: shippingProfile.id,
provider_id: provider_id,
data: null,
metadata: null,
type: expect.objectContaining({
id: expect.any(String),
}),
})
)
})
})
},
})

View File

@@ -0,0 +1,41 @@
import {
DeleteEntityInput,
ModuleRegistrationName,
} from "@medusajs/modules-sdk"
import { IFulfillmentModuleService } from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import { Modules } from "@medusajs/utils"
export const deleteShippingOptionsStepId = "delete-shipping-options-step"
export const deleteShippingOptionsStep = createStep(
deleteShippingOptionsStepId,
async (ids: string[], { container }) => {
if (!ids?.length) {
return
}
const service = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
const softDeletedEntities = await service.softDeleteShippingOptions(ids)
return new StepResponse(
{
[Modules.FULFILLMENT]: softDeletedEntities,
} as DeleteEntityInput,
ids
)
},
async (prevIds, { container }) => {
if (!prevIds?.length) {
return
}
const service = container.resolve<IFulfillmentModuleService>(
ModuleRegistrationName.FULFILLMENT
)
await service.restoreShippingOptions(prevIds)
}
)

View File

@@ -4,5 +4,6 @@ export * from "./create-fulfillment-set"
export * from "./create-service-zones"
export * from "./upsert-shipping-options"
export * from "./delete-service-zones"
export * from "./delete-shipping-options"
export * from "./create-shipping-profiles"
export * from "./remove-rules-from-fulfillment-shipping-option"

View File

@@ -0,0 +1,17 @@
import { FulfillmentWorkflow } from "@medusajs/types"
import { createWorkflow, WorkflowData } from "@medusajs/workflows-sdk"
import { deleteShippingOptionsStep } from "../steps"
import { removeRemoteLinkStep } from "../../common"
export const deleteShippingOptionsWorkflowId =
"delete-shipping-options-workflow"
export const deleteShippingOptionsWorkflow = createWorkflow(
deleteShippingOptionsWorkflowId,
(
input: WorkflowData<FulfillmentWorkflow.DeleteShippingOptionsWorkflowInput>
) => {
const softDeletedEntities = deleteShippingOptionsStep(input.ids)
removeRemoteLinkStep(softDeletedEntities)
}
)

View File

@@ -3,6 +3,7 @@ export * from "./create-service-zones"
export * from "./create-shipping-options"
export * from "./create-shipping-profiles"
export * from "./delete-service-zones"
export * from "./delete-shipping-options"
export * from "./remove-rules-from-fulfillment-shipping-option"
export * from "./update-service-zones"
export * from "./update-shipping-options"

View File

@@ -9,7 +9,6 @@ const {
} = require("@medusajs/utils")
const { DataSource } = require("typeorm")
const { ContainerRegistrationKeys } = require("@medusajs/utils")
const { migrateMedusaApp } = require("@medusajs/medusa/dist/loaders/medusa-app")
const { logger } = require("@medusajs/medusa-cli/dist/reporter")
module.exports = {
@@ -114,6 +113,9 @@ module.exports = {
featureFlagRouter: asValue(featureFlagRouter),
})
const {
migrateMedusaApp,
} = require("@medusajs/medusa/dist/loaders/medusa-app")
await migrateMedusaApp(
{ configModule, container },
{ registerInContainer: false }

View File

@@ -2,9 +2,15 @@ import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { AdminShippingOptionRetrieveResponse } from "@medusajs/types"
import {
AdminShippingOptionDeleteResponse,
AdminShippingOptionRetrieveResponse,
} from "@medusajs/types"
import { AdminUpdateShippingOptionType } from "../validators"
import { updateShippingOptionsWorkflow } from "@medusajs/core-flows"
import {
deleteShippingOptionsWorkflow,
updateShippingOptionsWorkflow,
} from "@medusajs/core-flows"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
@@ -42,3 +48,25 @@ export const POST = async (
res.status(200).json({ shipping_option: shippingOption })
}
export const DELETE = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<AdminShippingOptionDeleteResponse>
) => {
const shippingOptionId = req.params.id
const workflow = deleteShippingOptionsWorkflow(req.scope)
const { errors } = await workflow.run({
input: { ids: [shippingOptionId] },
throwOnError: false,
})
if (Array.isArray(errors) && errors[0]) {
throw errors[0].error
}
res
.status(200)
.json({ id: shippingOptionId, object: "shipping_option", deleted: true })
}

View File

@@ -3,11 +3,15 @@ import { authenticate } from "../../../utils/authenticate-middleware"
import {
AdminCreateShippingOption,
AdminGetShippingOptionParams,
AdminListShippingOptionParams,
AdminShippingOptionRulesBatchAdd,
AdminShippingOptionRulesBatchRemove,
AdminUpdateShippingOption,
} from "./validators"
import { retrieveTransformQueryConfig } from "./query-config"
import {
listTransformQueryConfig,
retrieveTransformQueryConfig,
} from "./query-config"
import { validateAndTransformBody } from "../../utils/validate-body"
import { validateAndTransformQuery } from "../../utils/validate-query"
@@ -17,6 +21,17 @@ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [authenticate("admin", ["bearer", "session"])],
},
{
method: ["GET"],
matcher: "/admin/shipping-options",
middlewares: [
validateAndTransformQuery(
AdminListShippingOptionParams,
listTransformQueryConfig
),
],
},
{
method: ["POST"],
matcher: "/admin/shipping-options",
@@ -41,6 +56,11 @@ export const adminShippingOptionRoutesMiddlewares: MiddlewareRoute[] = [
],
},
{
method: ["DELETE"],
matcher: "/admin/shipping-options/:id",
},
{
method: ["POST"],
matcher: "/admin/shipping-options/:id/rules/batch/add",

View File

@@ -3,13 +3,43 @@ import {
ContainerRegistrationKeys,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { AdminShippingOptionRetrieveResponse } from "@medusajs/types"
import {
AdminShippingOptionListResponse,
AdminShippingOptionRetrieveResponse
} from "@medusajs/types"
import {
AuthenticatedMedusaRequest,
MedusaResponse,
} from "../../../types/routing"
import { AdminCreateShippingOptionType } from "./validators"
export const GET = async (
req: AuthenticatedMedusaRequest,
res: MedusaResponse<AdminShippingOptionListResponse>
) => {
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
const variables = {
filters: req.filterableFields,
...req.remoteQueryConfig.pagination,
}
const queryObject = remoteQueryObjectFromString({
entryPoint: "shipping_options",
variables,
fields: req.remoteQueryConfig.fields,
})
const { rows: shipping_options, metadata } = await remoteQuery(queryObject)
res.json({
shipping_options,
count: metadata.count,
offset: metadata.skip,
limit: metadata.take,
})
}
export const POST = async (
req: AuthenticatedMedusaRequest<AdminCreateShippingOptionType>,
res: MedusaResponse<AdminShippingOptionRetrieveResponse>

View File

@@ -3,9 +3,13 @@ import {
ShippingOptionPriceType as ShippingOptionPriceTypeEnum,
} from "@medusajs/utils"
import { z } from "zod"
import { createSelectParams } from "../../utils/validators"
import { createFindParams, createSelectParams } from "../../utils/validators"
export const AdminGetShippingOptionParams = createSelectParams()
export const AdminListShippingOptionParams = createFindParams({
offset: 0,
limit: 20,
})
/**
* SHIPPING OPTIONS RULES
@@ -99,4 +103,4 @@ export const AdminUpdateShippingOption = z
export type AdminUpdateShippingOptionType = z.infer<
typeof AdminUpdateShippingOption
>
>

View File

@@ -0,0 +1,19 @@
/**
* The fields returned in the response of a DELETE request.
*/
export type DeleteResponse = {
/**
* The ID of the item that was deleted.
*/
id: string
/**
* The type of the item that was deleted.
*/
object: string
/**
* Whether the item was deleted successfully.
*/
deleted: boolean
}

View File

@@ -1 +1,2 @@
export * from "./paginated-response"
export * from "./deleted-response"

View File

@@ -5,6 +5,7 @@ import { AdminShippingOptionRuleResponse } from "./shipping-option-rule"
import { AdminShippingProfileResponse } from "./shipping-profile"
import { AdminFulfillmentProviderResponse } from "./fulfillment-provider"
import { AdminPriceSetPriceResponse } from "../../pricing"
import { DeleteResponse, PaginatedResponse } from "../../common"
/**
* @experimental
@@ -34,5 +35,17 @@ interface AdminShippingOptionResponse {
* @experimental
*/
export interface AdminShippingOptionRetrieveResponse {
shipping_option: AdminShippingOptionResponse[]
shipping_option: AdminShippingOptionResponse
}
/**
* @experimental
*/
export interface AdminShippingOptionListResponse extends PaginatedResponse {
shipping_options: AdminShippingOptionResponse[]
}
/**
* @experimental
*/
export interface AdminShippingOptionDeleteResponse extends DeleteResponse {}

View File

@@ -0,0 +1,3 @@
export interface DeleteShippingOptionsWorkflowInput {
ids: string[]
}

View File

@@ -1,4 +1,5 @@
export * from "./create-shipping-options"
export * from "./service-zones"
export * from "./delete-shipping-options"
export * from "./shipping-profiles"
export * from "./update-shipping-options"

View File

@@ -115,7 +115,7 @@ describe("Abstract Module Service Factory", () => {
it("should have softDelete method", async () => {
const result = await instance.softDelete("1")
expect(result).toEqual(undefined)
expect(result).toEqual({})
expect(
containerMock.mainModelMockService.softDelete
).toHaveBeenCalledWith(["1"], defaultTransactionContext)
@@ -123,7 +123,7 @@ describe("Abstract Module Service Factory", () => {
it("should have restore method", async () => {
const result = await instance.restore("1")
expect(result).toEqual(undefined)
expect(result).toEqual({})
expect(containerMock.mainModelMockService.restore).toHaveBeenCalledWith(
["1"],
defaultTransactionContext
@@ -175,7 +175,7 @@ describe("Abstract Module Service Factory", () => {
it("should have softDelete method for other models", async () => {
const result = await instance.softDeleteOtherModelMock1s("1")
expect(result).toEqual(undefined)
expect(result).toEqual({})
expect(
containerMock.otherModelMock1Service.softDelete
).toHaveBeenCalledWith(["1"], defaultTransactionContext)
@@ -183,7 +183,7 @@ describe("Abstract Module Service Factory", () => {
it("should have restore method for other models", async () => {
const result = await instance.restoreOtherModelMock1s("1")
expect(result).toEqual(undefined)
expect(result).toEqual({})
expect(containerMock.otherModelMock1Service.restore).toHaveBeenCalledWith(
["1"],
defaultTransactionContext

View File

@@ -12,11 +12,11 @@ import {
SoftDeleteReturn,
} from "@medusajs/types"
import {
MapToConfig,
isString,
kebabCase,
lowerCaseFirst,
mapObjectTo,
MapToConfig,
pluralize,
upperCaseFirst,
} from "../common"
@@ -505,18 +505,15 @@ export function abstractModuleServiceFactory<
}))
)
let mappedCascadedEntitiesMap
if (config.returnLinkableKeys) {
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: config.returnLinkableKeys,
}
)
}
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
const mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: config.returnLinkableKeys,
}
)
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}
@@ -540,17 +537,15 @@ export function abstractModuleServiceFactory<
].restore(primaryKeyValues_, sharedContext)
let mappedCascadedEntitiesMap
if (config.returnLinkableKeys) {
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: config.returnLinkableKeys,
}
)
}
// Map internal table/column names to their respective external linkable keys
// eg: product.id = product_id, variant.id = variant_id
mappedCascadedEntitiesMap = mapObjectTo(
cascadedEntitiesMap,
entityNameToLinkableKeysMap,
{
pick: config.returnLinkableKeys,
}
)
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
}