feat(fulfillment): Separate list and context rules validation (#6674)
**What** - Add method to validate fulfillment option from the provider - Separate list/list and count from context rules validation and add listShippingOptionsForContext FIXES CORE-1861
This commit is contained in:
committed by
GitHub
parent
1956dce80a
commit
3188e703b3
@@ -1413,7 +1413,7 @@ medusaIntegrationTestRunner({
|
||||
geo_zones: [
|
||||
{
|
||||
type: "country",
|
||||
country_code: "us",
|
||||
country_code: "dk",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1637,7 +1637,7 @@ medusaIntegrationTestRunner({
|
||||
geo_zones: [
|
||||
{
|
||||
type: "country",
|
||||
country_code: "us",
|
||||
country_code: "dk",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ListShippingOptionsForCartWorkflowInputDTO } from "@medusajs/types"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
WorkflowData,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { listShippingOptionsForContextStep } from "../../../shipping-options"
|
||||
@@ -33,12 +33,10 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
return {
|
||||
context: {
|
||||
fulfillment_set_id: fulfillmentSetIds,
|
||||
service_zone: {
|
||||
geo_zones: {
|
||||
city: data.input.shipping_address?.city,
|
||||
country_code: data.input.shipping_address?.country_code,
|
||||
province_code: data.input.shipping_address?.province,
|
||||
},
|
||||
address: {
|
||||
city: data.input.shipping_address?.city,
|
||||
country_code: data.input.shipping_address?.country_code,
|
||||
province_code: data.input.shipping_address?.province,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
IFulfillmentModuleService,
|
||||
ShippingOptionDTO,
|
||||
} from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
context: Record<string, unknown>
|
||||
@@ -20,10 +20,11 @@ export const listShippingOptionsForContextStep = createStep(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
const shippingOptions = await fulfillmentService.listShippingOptions(
|
||||
data.context,
|
||||
data.config
|
||||
)
|
||||
const shippingOptions =
|
||||
await fulfillmentService.listShippingOptionsForContext(
|
||||
data.context,
|
||||
data.config
|
||||
)
|
||||
|
||||
return new StepResponse(shippingOptions)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Modules} from "@medusajs/modules-sdk"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
CreateFulfillmentSetDTO,
|
||||
CreateServiceZoneDTO,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
ServiceZoneDTO,
|
||||
UpdateFulfillmentSetDTO,
|
||||
} from "@medusajs/types"
|
||||
import {GeoZoneType} from "@medusajs/utils"
|
||||
import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils"
|
||||
import { GeoZoneType } from "@medusajs/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(100000)
|
||||
|
||||
@@ -17,123 +17,123 @@ moduleIntegrationTestRunner({
|
||||
describe("Fulfillment Module Service", () => {
|
||||
describe("read", () => {
|
||||
it("should list fulfillment sets with a filter", async function () {
|
||||
const createdSet1 = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const createdSet2 = await service.create({
|
||||
name: "test2",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "_test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
let listedSets = await service.list(
|
||||
{
|
||||
type: createdSet1.type,
|
||||
},
|
||||
{
|
||||
relations: ["service_zones"],
|
||||
}
|
||||
)
|
||||
|
||||
const listedSets2 = await service.list(
|
||||
{
|
||||
type: createdSet1.type,
|
||||
},
|
||||
{
|
||||
relations: ["service_zones"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
|
||||
// Respecting order id by default
|
||||
expect(listedSets[1].service_zones).toEqual([
|
||||
expect.objectContaining({ name: "test" }),
|
||||
expect.objectContaining({ name: "test2" }),
|
||||
expect.objectContaining({ name: "_test" }),
|
||||
])
|
||||
|
||||
expect(listedSets2).toEqual(listedSets2)
|
||||
|
||||
listedSets = await service.list({
|
||||
name: createdSet2.name,
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedSets = await service.list({
|
||||
service_zones: { name: "test" },
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedSets = await service.list({
|
||||
service_zones: { geo_zones: { country_code: "fr" } },
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
const createdSet1 = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const createdSet2 = await service.create({
|
||||
name: "test2",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "_test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
let listedSets = await service.list(
|
||||
{
|
||||
type: createdSet1.type,
|
||||
},
|
||||
{
|
||||
relations: ["service_zones"],
|
||||
}
|
||||
)
|
||||
|
||||
const listedSets2 = await service.list(
|
||||
{
|
||||
type: createdSet1.type,
|
||||
},
|
||||
{
|
||||
relations: ["service_zones"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
|
||||
// Respecting order id by default
|
||||
expect(listedSets[1].service_zones).toEqual([
|
||||
expect.objectContaining({ name: "test" }),
|
||||
expect.objectContaining({ name: "test2" }),
|
||||
expect.objectContaining({ name: "_test" }),
|
||||
])
|
||||
|
||||
expect(listedSets2).toEqual(listedSets2)
|
||||
|
||||
listedSets = await service.list({
|
||||
name: createdSet2.name,
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedSets = await service.list({
|
||||
service_zones: { name: "test" },
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedSets = await service.list({
|
||||
service_zones: { geo_zones: { country_code: "fr" } },
|
||||
})
|
||||
|
||||
expect(listedSets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedSets).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdSet1.id }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("mutations", () => {
|
||||
@@ -349,6 +349,7 @@ moduleIntegrationTestRunner({
|
||||
{
|
||||
type: GeoZoneType.CITY,
|
||||
country_code: "fr",
|
||||
province_code: "test",
|
||||
city: "lyon",
|
||||
},
|
||||
],
|
||||
@@ -400,6 +401,91 @@ moduleIntegrationTestRunner({
|
||||
expect(err).toBeDefined()
|
||||
expect(err.constraint).toBe("IDX_fulfillment_set_name_unique")
|
||||
})
|
||||
|
||||
it("should fail on creating a new fulfillment set with new service zones and new geo zones that are not valid", async function () {
|
||||
let data: CreateFulfillmentSetDTO = {
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.PROVINCE,
|
||||
country_code: "fr",
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let err = await service.create(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property province_code for geo zone type province"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.CITY,
|
||||
country_code: "fr",
|
||||
province_code: "test",
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.create(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property city for geo zone type city"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.ZIP,
|
||||
postal_expression: "test",
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.create(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property country_code for geo zone type zip"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: "unknown",
|
||||
postal_expression: "test",
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.create(data).catch((e) => e)
|
||||
expect(err.message).toBe(`Invalid geo zone type: unknown`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update", () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Modules} from "@medusajs/modules-sdk"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
CreateGeoZoneDTO,
|
||||
IFulfillmentModuleService,
|
||||
UpdateGeoZoneDTO,
|
||||
} from "@medusajs/types"
|
||||
import {GeoZoneType} from "@medusajs/utils"
|
||||
import {moduleIntegrationTestRunner, SuiteOptions} from "medusa-test-utils"
|
||||
import { GeoZoneType } from "@medusajs/utils"
|
||||
import { moduleIntegrationTestRunner, SuiteOptions } from "medusa-test-utils"
|
||||
|
||||
jest.setTimeout(100000)
|
||||
|
||||
@@ -15,52 +15,52 @@ moduleIntegrationTestRunner({
|
||||
describe("Fulfillment Module Service", () => {
|
||||
describe("read", () => {
|
||||
it("should list geo zones with a filter", async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
|
||||
const createdZone1 = await service.createGeoZones({
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
})
|
||||
const createdZone2 = await service.createGeoZones({
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "us",
|
||||
})
|
||||
|
||||
let listedZones = await service.listGeoZones({
|
||||
type: createdZone1.type,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedZones = await service.listGeoZones({
|
||||
country_code: createdZone2.country_code,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
|
||||
const createdZone1 = await service.createGeoZones({
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
})
|
||||
const createdZone2 = await service.createGeoZones({
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "us",
|
||||
})
|
||||
|
||||
let listedZones = await service.listGeoZones({
|
||||
type: createdZone1.type,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedZones = await service.listGeoZones({
|
||||
country_code: createdZone2.country_code,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("mutations", () => {
|
||||
@@ -131,6 +131,60 @@ moduleIntegrationTestRunner({
|
||||
++i
|
||||
}
|
||||
})
|
||||
|
||||
it("should fail to create new geo zones that are not valid", async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
|
||||
let data: CreateGeoZoneDTO = {
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.PROVINCE,
|
||||
country_code: "fr",
|
||||
} as any
|
||||
|
||||
let err = await service.createGeoZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property province_code for geo zone type province"
|
||||
)
|
||||
|
||||
data = {
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.CITY,
|
||||
country_code: "fr",
|
||||
province_code: "test",
|
||||
} as any
|
||||
|
||||
err = await service.createGeoZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property city for geo zone type city"
|
||||
)
|
||||
|
||||
data = {
|
||||
service_zone_id: serviceZone.id,
|
||||
type: GeoZoneType.ZIP,
|
||||
postal_expression: "test",
|
||||
} as any
|
||||
|
||||
err = await service.createGeoZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property country_code for geo zone type zip"
|
||||
)
|
||||
|
||||
data = {
|
||||
service_zone_id: serviceZone.id,
|
||||
type: "unknown",
|
||||
postal_expression: "test",
|
||||
} as any
|
||||
|
||||
err = await service.createGeoZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(`Invalid geo zone type: unknown`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update", () => {
|
||||
|
||||
@@ -16,56 +16,56 @@ moduleIntegrationTestRunner({
|
||||
describe("Fulfillment Module Service", () => {
|
||||
describe("read", () => {
|
||||
it("should list service zones with a filter", async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
|
||||
const createdZone1 = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const createdZone2 = await service.createServiceZones({
|
||||
name: "test2",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
let listedZones = await service.listServiceZones({
|
||||
name: createdZone2.name,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedZones = await service.listServiceZones({
|
||||
geo_zones: { country_code: "fr" },
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
|
||||
const createdZone1 = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const createdZone2 = await service.createServiceZones({
|
||||
name: "test2",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.COUNTRY,
|
||||
country_code: "fr",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
let listedZones = await service.listServiceZones({
|
||||
name: createdZone2.name,
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
|
||||
listedZones = await service.listServiceZones({
|
||||
geo_zones: { country_code: "fr" },
|
||||
})
|
||||
|
||||
expect(listedZones).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone2.id }),
|
||||
])
|
||||
)
|
||||
expect(listedZones).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: createdZone1.id }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("mutations", () => {
|
||||
@@ -189,6 +189,76 @@ moduleIntegrationTestRunner({
|
||||
expect(err).toBeDefined()
|
||||
expect(err.constraint).toBe("IDX_service_zone_name_unique")
|
||||
})
|
||||
|
||||
it("should fail on creating a service zone and new geo zones that are not valid", async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
|
||||
let data: CreateServiceZoneDTO = {
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.PROVINCE,
|
||||
country_code: "fr",
|
||||
} as any,
|
||||
],
|
||||
}
|
||||
|
||||
let err = await service.createServiceZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property province_code for geo zone type province"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.CITY,
|
||||
country_code: "fr",
|
||||
province_code: "test",
|
||||
} as any,
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.createServiceZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property city for geo zone type city"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.ZIP,
|
||||
postal_expression: "test",
|
||||
} as any,
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.createServiceZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(
|
||||
"Missing required property country_code for geo zone type zip"
|
||||
)
|
||||
|
||||
data = {
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
geo_zones: [
|
||||
{
|
||||
type: "unknown",
|
||||
postal_expression: "test",
|
||||
} as any,
|
||||
],
|
||||
}
|
||||
|
||||
err = await service.createServiceZones(data).catch((e) => e)
|
||||
expect(err.message).toBe(`Invalid geo zone type: unknown`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update", () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { generateCreateShippingOptionsData } from "../../__fixtures__"
|
||||
import { resolve } from "path"
|
||||
import { FulfillmentProviderService } from "@services"
|
||||
import { FulfillmentProviderServiceFixtures } from "../../__fixtures__/providers"
|
||||
import { GeoZoneType } from "@medusajs/utils"
|
||||
|
||||
jest.setTimeout(100000)
|
||||
|
||||
@@ -35,10 +36,7 @@ const providerId = FulfillmentProviderService.getRegistrationIdentifier(
|
||||
moduleIntegrationTestRunner({
|
||||
moduleName: Modules.FULFILLMENT,
|
||||
moduleOptions,
|
||||
testSuite: ({
|
||||
MikroOrmWrapper,
|
||||
service,
|
||||
}: SuiteOptions<IFulfillmentModuleService>) => {
|
||||
testSuite: ({ service }: SuiteOptions<IFulfillmentModuleService>) => {
|
||||
describe("Fulfillment Module Service", () => {
|
||||
describe("read", () => {
|
||||
it("should list shipping options with a filter", async function () {
|
||||
@@ -158,7 +156,7 @@ moduleIntegrationTestRunner({
|
||||
}),
|
||||
])
|
||||
|
||||
let listedOptions = await service.listShippingOptions({
|
||||
let listedOptions = await service.listShippingOptionsForContext({
|
||||
context: {
|
||||
"test-attribute": "test",
|
||||
"test-attribute2": {
|
||||
@@ -175,8 +173,12 @@ moduleIntegrationTestRunner({
|
||||
])
|
||||
)
|
||||
|
||||
listedOptions = await service.listShippingOptions({
|
||||
fulfillment_set_id: { $ne: fulfillmentSet.id },
|
||||
listedOptions = await service.listShippingOptionsForContext({
|
||||
service_zone: {
|
||||
fulfillment_set: {
|
||||
id: { $ne: fulfillmentSet.id },
|
||||
},
|
||||
},
|
||||
context: {
|
||||
"test-attribute": "test",
|
||||
"test-attribute2": {
|
||||
@@ -187,8 +189,12 @@ moduleIntegrationTestRunner({
|
||||
|
||||
expect(listedOptions).toHaveLength(0)
|
||||
|
||||
listedOptions = await service.listShippingOptions({
|
||||
fulfillment_set_type: "non-existing-type",
|
||||
listedOptions = await service.listShippingOptionsForContext({
|
||||
service_zone: {
|
||||
fulfillment_set: {
|
||||
type: "non-existing-type",
|
||||
},
|
||||
},
|
||||
context: {
|
||||
"test-attribute": "test",
|
||||
"test-attribute2": {
|
||||
@@ -199,6 +205,207 @@ moduleIntegrationTestRunner({
|
||||
|
||||
expect(listedOptions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it(`should list the shipping options for a context with a specific address`, async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
geo_zones: [
|
||||
{
|
||||
type: GeoZoneType.ZIP,
|
||||
country_code: "fr",
|
||||
province_code: "rhone",
|
||||
city: "paris",
|
||||
postal_expression: "75006",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [shippingOption1, , shippingOption3] =
|
||||
await service.createShippingOptions([
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
value: ["test"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
value: ["test-test"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
{
|
||||
attribute: "test-attribute2.options",
|
||||
operator: "in",
|
||||
value: ["test", "test2"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
||||
let shippingOptions = await service.listShippingOptionsForContext({
|
||||
address: {
|
||||
country_code: "fr",
|
||||
province_code: "rhone",
|
||||
city: "paris",
|
||||
postal_expression: "75006",
|
||||
},
|
||||
})
|
||||
|
||||
expect(shippingOptions).toHaveLength(3)
|
||||
|
||||
shippingOptions = await service.listShippingOptionsForContext({
|
||||
address: {
|
||||
country_code: "fr",
|
||||
province_code: "rhone",
|
||||
city: "paris",
|
||||
postal_expression: "75001",
|
||||
},
|
||||
})
|
||||
|
||||
expect(shippingOptions).toHaveLength(0)
|
||||
|
||||
shippingOptions = await service.listShippingOptionsForContext({
|
||||
address: {
|
||||
country_code: "fr",
|
||||
province_code: "rhone",
|
||||
city: "paris",
|
||||
postal_expression: "75006",
|
||||
},
|
||||
context: {
|
||||
"test-attribute": "test",
|
||||
"test-attribute2": {
|
||||
options: "test2",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(shippingOptions).toHaveLength(2)
|
||||
expect(shippingOptions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: shippingOption1.id }),
|
||||
expect.objectContaining({ id: shippingOption3.id }),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should validate if a shipping option is applicable to a context", async function () {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
service_zones: [
|
||||
{
|
||||
name: "test",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [shippingOption1, shippingOption2, shippingOption3] =
|
||||
await service.createShippingOptions([
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
value: ["test"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
value: ["test-test"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
generateCreateShippingOptionsData({
|
||||
service_zone_id: fulfillmentSet.service_zones[0].id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
provider_id: providerId,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
{
|
||||
attribute: "test-attribute2.options",
|
||||
operator: "in",
|
||||
value: ["test", "test2"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
])
|
||||
|
||||
let listedOptions = await service.listShippingOptions()
|
||||
|
||||
expect(listedOptions).toHaveLength(3)
|
||||
|
||||
const context = {
|
||||
"test-attribute": "test",
|
||||
"test-attribute2": {
|
||||
options: "test2",
|
||||
},
|
||||
}
|
||||
|
||||
const isShippingOption1Applicable =
|
||||
await service.validateShippingOption(shippingOption1.id, context)
|
||||
expect(isShippingOption1Applicable).toBeTruthy()
|
||||
|
||||
const isShippingOption2Applicable =
|
||||
await service.validateShippingOption(shippingOption2.id, context)
|
||||
expect(isShippingOption2Applicable).toBeFalsy()
|
||||
|
||||
const isShippingOption3Applicable =
|
||||
await service.validateShippingOption(shippingOption3.id, context)
|
||||
expect(isShippingOption3Applicable).toBeTruthy()
|
||||
})
|
||||
|
||||
describe("mutations", () => {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"watch:test": "tsc --build tsconfig.spec.json --watch",
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json",
|
||||
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
|
||||
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
|
||||
"test": "jest --runInBand --bail --forceExit --passWithNoTests -- src/**/__tests__/**/*.ts",
|
||||
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
|
||||
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate",
|
||||
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial -n InitialSetupMigration",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
describe("noop", function () {
|
||||
it("should run", function () {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
Context,
|
||||
DAL,
|
||||
FilterableFulfillmentSetProps,
|
||||
FilterableShippingOptionProps,
|
||||
FilterQuery,
|
||||
FindConfig,
|
||||
FulfillmentDTO,
|
||||
FulfillmentTypes,
|
||||
@@ -131,69 +129,17 @@ export default class FulfillmentModuleService<
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
protected static normalizeShippingOptionsListParams(
|
||||
filters: FilterableShippingOptionProps = {},
|
||||
config: FindConfig<ShippingOptionDTO> = {}
|
||||
) {
|
||||
let { fulfillment_set_id, fulfillment_set_type, context, ...where } =
|
||||
filters
|
||||
|
||||
const normalizedConfig = { ...config }
|
||||
normalizedConfig.relations = [
|
||||
"rules",
|
||||
"type",
|
||||
"shipping_profile",
|
||||
"provider",
|
||||
...(normalizedConfig.relations ?? []),
|
||||
]
|
||||
// The assumption is that there won't be an infinite amount of shipping options. So if a context filtering needs to be applied we can retrieve them all.
|
||||
normalizedConfig.take =
|
||||
normalizedConfig.take ?? (context ? null : undefined)
|
||||
|
||||
let normalizedFilters = { ...where } as FilterQuery
|
||||
|
||||
if (fulfillment_set_id || fulfillment_set_type) {
|
||||
const fulfillmentSetConstraints = {}
|
||||
|
||||
if (fulfillment_set_id) {
|
||||
fulfillmentSetConstraints["id"] = fulfillment_set_id
|
||||
}
|
||||
|
||||
if (fulfillment_set_type) {
|
||||
fulfillmentSetConstraints["type"] = fulfillment_set_type
|
||||
}
|
||||
|
||||
normalizedFilters = {
|
||||
...normalizedFilters,
|
||||
service_zone: {
|
||||
fulfillment_set: fulfillmentSetConstraints,
|
||||
},
|
||||
}
|
||||
|
||||
normalizedConfig.relations.push("service_zone.fulfillment_set")
|
||||
}
|
||||
|
||||
normalizedConfig.relations = Array.from(new Set(normalizedConfig.relations))
|
||||
|
||||
return {
|
||||
filters: normalizedFilters,
|
||||
config: normalizedConfig,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
// @ts-ignore
|
||||
async listShippingOptions(
|
||||
filters: FilterableShippingOptionProps = {},
|
||||
async listShippingOptionsForContext(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
|
||||
config: FindConfig<ShippingOptionDTO> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.ShippingOptionDTO[]> {
|
||||
const {
|
||||
filters: normalizedFilters,
|
||||
config: normalizedConfig,
|
||||
context,
|
||||
} = FulfillmentModuleService.normalizeShippingOptionsListParams(
|
||||
config: normalizedConfig,
|
||||
filters: normalizedFilters,
|
||||
} = FulfillmentModuleService.normalizeListShippingOptionsForContextParams(
|
||||
filters,
|
||||
config
|
||||
)
|
||||
@@ -204,7 +150,6 @@ export default class FulfillmentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// Apply rules context filtering
|
||||
if (context) {
|
||||
shippingOptions = shippingOptions.filter((shippingOption) => {
|
||||
if (!shippingOption.rules?.length) {
|
||||
@@ -245,14 +190,6 @@ export default class FulfillmentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
async retrieveFulfillmentOptions(
|
||||
providerId: string
|
||||
): Promise<Record<string, any>[]> {
|
||||
return await this.fulfillmentProviderService_.getFulfillmentOptions(
|
||||
providerId
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async listFulfillments(
|
||||
filters: FulfillmentTypes.FilterableFulfillmentProps = {},
|
||||
@@ -331,6 +268,20 @@ export default class FulfillmentModuleService<
|
||||
): Promise<TEntity | TEntity[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
for (const fulfillmentSet of data_) {
|
||||
if (fulfillmentSet.service_zones?.length) {
|
||||
for (const serviceZone of fulfillmentSet.service_zones) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createdFulfillmentSets = await this.fulfillmentSetService_.create(
|
||||
data_,
|
||||
sharedContext
|
||||
@@ -384,6 +335,14 @@ export default class FulfillmentModuleService<
|
||||
return []
|
||||
}
|
||||
|
||||
for (const serviceZone of data_) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(serviceZone.geo_zones)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createdServiceZones = await this.serviceZoneService_.create(
|
||||
data_,
|
||||
sharedContext
|
||||
@@ -520,13 +479,17 @@ export default class FulfillmentModuleService<
|
||||
| FulfillmentTypes.CreateGeoZoneDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
FulfillmentModuleService.validateGeoZones(data_)
|
||||
|
||||
const createdGeoZones = await this.geoZoneService_.create(
|
||||
data,
|
||||
data_,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize<FulfillmentTypes.GeoZoneDTO[]>(
|
||||
createdGeoZones,
|
||||
Array.isArray(data) ? createdGeoZones : createdGeoZones[0],
|
||||
{
|
||||
populate: true,
|
||||
}
|
||||
@@ -777,6 +740,11 @@ export default class FulfillmentModuleService<
|
||||
fulfillmentSet.service_zones = fulfillmentSet.service_zones.map(
|
||||
(serviceZone) => {
|
||||
if (!("id" in serviceZone)) {
|
||||
if (serviceZone.geo_zones?.length) {
|
||||
FulfillmentModuleService.validateGeoZones(
|
||||
serviceZone.geo_zones
|
||||
)
|
||||
}
|
||||
return serviceZone
|
||||
}
|
||||
return serviceZonesMap.get(serviceZone.id)!
|
||||
@@ -940,6 +908,7 @@ export default class FulfillmentModuleService<
|
||||
|
||||
serviceZone.geo_zones = serviceZone.geo_zones.map((geoZone) => {
|
||||
if (!("id" in geoZone)) {
|
||||
FulfillmentModuleService.validateGeoZones([geoZone])
|
||||
return geoZone
|
||||
}
|
||||
return geoZonesMap.get(geoZone.id)!
|
||||
@@ -1138,6 +1107,14 @@ export default class FulfillmentModuleService<
|
||||
| FulfillmentTypes.UpdateGeoZoneDTO[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<FulfillmentTypes.GeoZoneDTO | FulfillmentTypes.GeoZoneDTO[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
FulfillmentModuleService.validateGeoZones(data_)
|
||||
|
||||
const updatedGeoZones = await this.geoZoneService_.update(
|
||||
data,
|
||||
sharedContext
|
||||
@@ -1271,6 +1248,41 @@ export default class FulfillmentModuleService<
|
||||
return Array.isArray(result) ? result[0] : result
|
||||
}
|
||||
|
||||
async retrieveFulfillmentOptions(
|
||||
providerId: string
|
||||
): Promise<Record<string, any>[]> {
|
||||
return await this.fulfillmentProviderService_.getFulfillmentOptions(
|
||||
providerId
|
||||
)
|
||||
}
|
||||
|
||||
async validateFulfillmentOption(
|
||||
providerId: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<boolean> {
|
||||
return await this.fulfillmentProviderService_.validateOption(
|
||||
providerId,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async validateShippingOption(
|
||||
shippingOptionId: string,
|
||||
context: Record<string, unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const shippingOptions = await this.listShippingOptionsForContext(
|
||||
{ id: shippingOptionId, context },
|
||||
{
|
||||
relations: ["rules"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return !!shippingOptions.length
|
||||
}
|
||||
|
||||
protected static canCancelFulfillmentOrThrow(fulfillment: Fulfillment) {
|
||||
if (fulfillment.shipped_at) {
|
||||
throw new MedusaError(
|
||||
@@ -1336,4 +1348,213 @@ export default class FulfillmentModuleService<
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected static validateGeoZones(
|
||||
geoZones: (
|
||||
| (Partial<FulfillmentTypes.CreateGeoZoneDTO> & { type: string })
|
||||
| (Partial<FulfillmentTypes.UpdateGeoZoneDTO> & { type: string })
|
||||
)[]
|
||||
) {
|
||||
const requirePropForType = {
|
||||
country: ["country_code"],
|
||||
province: ["country_code", "province_code"],
|
||||
city: ["country_code", "province_code", "city"],
|
||||
zip: ["country_code", "province_code", "city", "postal_expression"],
|
||||
}
|
||||
|
||||
for (const geoZone of geoZones) {
|
||||
if (!requirePropForType[geoZone.type]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid geo zone type: ${geoZone.type}`
|
||||
)
|
||||
}
|
||||
|
||||
for (const prop of requirePropForType[geoZone.type]) {
|
||||
if (!geoZone[prop]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Missing required property ${prop} for geo zone type ${geoZone.type}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static normalizeListShippingOptionsForContextParams(
|
||||
filters: FulfillmentTypes.FilterableShippingOptionForContextProps,
|
||||
config: FindConfig<ShippingOptionDTO> = {}
|
||||
) {
|
||||
let {
|
||||
fulfillment_set_id,
|
||||
fulfillment_set_type,
|
||||
address,
|
||||
context,
|
||||
...where
|
||||
} = filters
|
||||
|
||||
const normalizedConfig = { ...config }
|
||||
normalizedConfig.relations = [
|
||||
"rules",
|
||||
"type",
|
||||
"shipping_profile",
|
||||
"provider",
|
||||
...(normalizedConfig.relations ?? []),
|
||||
]
|
||||
|
||||
normalizedConfig.take =
|
||||
normalizedConfig.take ?? (context ? null : undefined)
|
||||
|
||||
let normalizedFilters = { ...where }
|
||||
|
||||
if (fulfillment_set_id || fulfillment_set_type) {
|
||||
const fulfillmentSetConstraints = {}
|
||||
|
||||
if (fulfillment_set_id) {
|
||||
fulfillmentSetConstraints["id"] = fulfillment_set_id
|
||||
}
|
||||
|
||||
if (fulfillment_set_type) {
|
||||
fulfillmentSetConstraints["type"] = fulfillment_set_type
|
||||
}
|
||||
|
||||
normalizedFilters = {
|
||||
...normalizedFilters,
|
||||
service_zone: {
|
||||
...(normalizedFilters.service_zone ?? {}),
|
||||
fulfillment_set: {
|
||||
...(normalizedFilters.service_zone?.fulfillment_set ?? {}),
|
||||
...fulfillmentSetConstraints,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
normalizedConfig.relations.push("service_zone.fulfillment_set")
|
||||
}
|
||||
|
||||
if (address) {
|
||||
const geoZoneConstraints =
|
||||
FulfillmentModuleService.buildGeoZoneConstraintsFromAddress(address)
|
||||
|
||||
normalizedFilters = {
|
||||
...normalizedFilters,
|
||||
service_zone: {
|
||||
...(normalizedFilters.service_zone ?? {}),
|
||||
geo_zones: {
|
||||
$or: geoZoneConstraints.map((geoZoneConstraint) => ({
|
||||
// Apply eventually provided constraints on the geo zone along side the address constraints
|
||||
...(normalizedFilters.service_zone?.geo_zones ?? {}),
|
||||
...geoZoneConstraint,
|
||||
})),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
normalizedConfig.relations.push("service_zone.geo_zones")
|
||||
}
|
||||
|
||||
normalizedConfig.relations = Array.from(new Set(normalizedConfig.relations))
|
||||
|
||||
return {
|
||||
filters: normalizedFilters,
|
||||
config: normalizedConfig,
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the constraints for the geo zones based on the address properties
|
||||
* available and the hierarchy of required properties.
|
||||
* We build a OR constraint from the narrowest to the broadest
|
||||
* e.g. if we have a postal expression we build a constraint for the postal expression require props of type zip
|
||||
* and a constraint for the city required props of type city
|
||||
* and a constraint for the province code required props of type province
|
||||
* and a constraint for the country code required props of type country
|
||||
* example:
|
||||
* {
|
||||
* $or: [
|
||||
* {
|
||||
* type: "zip",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB",
|
||||
* city: "Stockholm",
|
||||
* postal_expression: "12345"
|
||||
* },
|
||||
* {
|
||||
* type: "city",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB",
|
||||
* city: "Stockholm"
|
||||
* },
|
||||
* {
|
||||
* type: "province",
|
||||
* country_code: "SE",
|
||||
* province_code: "AB"
|
||||
* },
|
||||
* {
|
||||
* type: "country",
|
||||
* country_code: "SE"
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
private static buildGeoZoneConstraintsFromAddress(
|
||||
address: FulfillmentTypes.FilterableShippingOptionForContextProps["address"]
|
||||
) {
|
||||
/**
|
||||
* Define the hierarchy of required properties for the geo zones.
|
||||
*/
|
||||
const geoZoneRequirePropertyHierarchy = {
|
||||
postal_expression: [
|
||||
"country_code",
|
||||
"province_code",
|
||||
"city",
|
||||
"postal_expression",
|
||||
],
|
||||
city: ["country_code", "province_code", "city"],
|
||||
province_code: ["country_code", "province_code"],
|
||||
country_code: ["country_code"],
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that the address has the required properties for the geo zones
|
||||
* constraints to build after. We are going from the narrowest to the broadest
|
||||
*/
|
||||
Object.entries(geoZoneRequirePropertyHierarchy).forEach(
|
||||
([prop, requiredProps]) => {
|
||||
if (address![prop]) {
|
||||
for (const requiredProp of requiredProps) {
|
||||
if (!address![requiredProp]) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Missing required property ${requiredProp} for address when property ${prop} is set`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const geoZoneConstraints = Object.entries(geoZoneRequirePropertyHierarchy)
|
||||
.map(([prop, requiredProps]) => {
|
||||
if (address![prop]) {
|
||||
return requiredProps.reduce((geoZoneConstraint, prop) => {
|
||||
geoZoneConstraint.type =
|
||||
prop === "postal_expression"
|
||||
? "zip"
|
||||
: prop === "city"
|
||||
? "city"
|
||||
: prop === "province_code"
|
||||
? "province"
|
||||
: "country"
|
||||
geoZoneConstraint[prop] = address![prop]
|
||||
return geoZoneConstraint
|
||||
}, {} as Record<string, string | undefined>)
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((v): v is Record<string, any> => !!v)
|
||||
|
||||
return geoZoneConstraints
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ export default class FulfillmentProviderService extends ModulesSdkUtils.internal
|
||||
return await provider.validateFulfillmentData(optionData, data, context)
|
||||
}
|
||||
|
||||
async validateOption(data: any) {
|
||||
const provider = this.retrieveProviderRegistration(data.provider_id)
|
||||
async validateOption(providerId: string, data: Record<string, unknown>) {
|
||||
const provider = this.retrieveProviderRegistration(providerId)
|
||||
return await provider.validateOption(data)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export function isContextValid(
|
||||
} = {
|
||||
someAreValid: false,
|
||||
}
|
||||
) {
|
||||
): boolean {
|
||||
const { someAreValid } = options
|
||||
|
||||
const loopComparator = someAreValid ? rules.some : rules.every
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { FulfillmentSetDTO } from "./fulfillment-set"
|
||||
import {
|
||||
FilterableFulfillmentSetProps,
|
||||
FulfillmentSetDTO,
|
||||
} from "./fulfillment-set"
|
||||
import { FilterableGeoZoneProps, GeoZoneDTO } from "./geo-zone"
|
||||
import { ShippingOptionDTO } from "./shipping-option"
|
||||
import { BaseFilterable, OperatorMap } from "../../dal"
|
||||
@@ -20,5 +23,6 @@ export interface FilterableServiceZoneProps
|
||||
id?: string | string[] | OperatorMap<string | string[]>
|
||||
name?: string | string[] | OperatorMap<string | string[]>
|
||||
geo_zones?: FilterableGeoZoneProps
|
||||
fulfillment_set?: FilterableFulfillmentSetProps
|
||||
shipping_options?: any // TODO
|
||||
}
|
||||
|
||||
@@ -39,9 +39,7 @@ export interface FilterableShippingOptionProps
|
||||
extends BaseFilterable<FilterableShippingOptionProps> {
|
||||
id?: string | string[] | OperatorMap<string | string[]>
|
||||
name?: string | string[] | OperatorMap<string | string[]>
|
||||
fulfillment_set_id?: string | string[] | OperatorMap<string | string[]>
|
||||
shipping_profile_id?: string | string[] | OperatorMap<string | string[]>
|
||||
fulfillment_set_type?: string | string[] | OperatorMap<string | string[]>
|
||||
price_type?:
|
||||
| ShippingOptionPriceType
|
||||
| ShippingOptionPriceType[]
|
||||
@@ -49,5 +47,22 @@ export interface FilterableShippingOptionProps
|
||||
service_zone?: FilterableServiceZoneProps
|
||||
shipping_option_type?: FilterableShippingOptionTypeProps
|
||||
rules?: FilterableShippingOptionRuleProps
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface FilterableShippingOptionForContextProps
|
||||
extends FilterableShippingOptionProps {
|
||||
fulfillment_set_id?: string | string[] | OperatorMap<string | string[]>
|
||||
fulfillment_set_type?: string | string[] | OperatorMap<string | string[]>
|
||||
/**
|
||||
* The address is a shortcut to filter through geo_zones
|
||||
* and build opinionated validation and filtering around the geo_zones.
|
||||
* For custom filtering you can go through the service_zone.geo_zones directly.
|
||||
*/
|
||||
address?: {
|
||||
country_code?: string
|
||||
province_code?: string
|
||||
city?: string
|
||||
postal_expression?: string
|
||||
}
|
||||
context?: Record<string, any>
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ export interface CreateProvinceGeoZoneDTO extends CreateGeoZoneBaseDTO {
|
||||
|
||||
export interface CreateCityGeoZoneDTO extends CreateGeoZoneBaseDTO {
|
||||
type: "city"
|
||||
province_code: string
|
||||
city: string
|
||||
}
|
||||
|
||||
export interface CreateZipGeoZoneDTO extends CreateGeoZoneBaseDTO {
|
||||
type: "zip"
|
||||
province_code: string
|
||||
city: string
|
||||
postal_expression: Record<string, any>
|
||||
}
|
||||
|
||||
@@ -47,12 +50,15 @@ export interface UpdateProvinceGeoZoneDTO extends UpdateGeoZoneBaseDTO {
|
||||
|
||||
export interface UpdateCityGeoZoneDTO extends UpdateGeoZoneBaseDTO {
|
||||
type: "city"
|
||||
city: string
|
||||
province_code?: string
|
||||
city?: string
|
||||
}
|
||||
|
||||
export interface UpdateZipGeoZoneDTO extends UpdateGeoZoneBaseDTO {
|
||||
type: "zip"
|
||||
postal_expression: Record<string, any>
|
||||
province_code?: string
|
||||
city?: string
|
||||
postal_expression?: Record<string, any>
|
||||
}
|
||||
|
||||
export type UpdateGeoZoneDTO =
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
FilterableFulfillmentSetProps,
|
||||
FilterableGeoZoneProps,
|
||||
FilterableServiceZoneProps,
|
||||
FilterableShippingOptionForContextProps,
|
||||
FilterableShippingOptionProps,
|
||||
FilterableShippingOptionRuleProps,
|
||||
FilterableShippingOptionTypeProps,
|
||||
@@ -326,13 +327,24 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionDTO[]>
|
||||
/**
|
||||
* List and count shipping options
|
||||
* List shipping options and eventually filter the result based on the context and their rules
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listShippingOptionsForContext(
|
||||
filters: FilterableShippingOptionForContextProps,
|
||||
config?: FindConfig<ShippingOptionDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionDTO[]>
|
||||
/**
|
||||
* List and count shipping options without taking into account the context
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listAndCountShippingOptions(
|
||||
filters?: FilterableShippingOptionProps,
|
||||
filters?: Omit<FilterableShippingOptionProps, "context">,
|
||||
config?: FindConfig<ShippingOptionDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ShippingOptionDTO[], number]>
|
||||
@@ -664,4 +676,20 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
retrieveFulfillmentOptions(
|
||||
providerId: string
|
||||
): Promise<Record<string, unknown>[]>
|
||||
|
||||
/**
|
||||
* Validate the given shipping option fulfillment option from the provided data
|
||||
*/
|
||||
validateFulfillmentOption(
|
||||
providerId: string,
|
||||
data: Record<string, unknown>
|
||||
): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Validate if the given shipping option is valid for a given context
|
||||
*/
|
||||
validateShippingOption(
|
||||
shippingOptionId: string,
|
||||
context: Record<string, unknown>
|
||||
): Promise<boolean>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user