feat(fulfillment): Shipping options, rules CRUD + rules based context filtering (#6455)
**What**
- Update shipping options with its rules and type
- create/update rules independently
- context based validation fundation
- 🔴 list shipping options with context rules fitlering will come in a separate pr to keep this one smaller
FIXES CORE-1743
FIXES CORE-1764
This commit is contained in:
committed by
GitHub
parent
78b6d46584
commit
788c4a1e36
@@ -812,7 +812,7 @@ moduleIntegrationTestRunner({
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
value: "test-value",
|
||||
value: ["test-value"],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -887,7 +887,7 @@ moduleIntegrationTestRunner({
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
operator: "eq",
|
||||
value: "test-value",
|
||||
},
|
||||
],
|
||||
@@ -909,7 +909,7 @@ moduleIntegrationTestRunner({
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "in",
|
||||
operator: "eq",
|
||||
value: "test-value",
|
||||
},
|
||||
],
|
||||
@@ -953,6 +953,145 @@ moduleIntegrationTestRunner({
|
||||
++i
|
||||
}
|
||||
})
|
||||
|
||||
it("should fail to create a new shipping option with invalid rules", async function () {
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
|
||||
// TODO: change that for a real provider instead of fake data manual inserted data
|
||||
const [{ id: providerId }] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const createData: CreateShippingOptionDTO = {
|
||||
name: "test-option",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: providerId,
|
||||
type: {
|
||||
code: "test-type",
|
||||
description: "test-description",
|
||||
label: "test-label",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "invalid",
|
||||
value: "test-value",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const err = await service
|
||||
.createShippingOptions(createData)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
"Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on create shipping option rules", () => {
|
||||
it("should create a new rule", async () => {
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
|
||||
// service provider
|
||||
const [{ id: providerId }] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOption = await service.createShippingOptions({
|
||||
name: "test-option",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: providerId,
|
||||
type: {
|
||||
code: "test-type",
|
||||
description: "test-description",
|
||||
label: "test-label",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test-attribute",
|
||||
operator: "eq",
|
||||
value: "test-value",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const ruleData = {
|
||||
attribute: "test-attribute",
|
||||
operator: "eq",
|
||||
value: "test-value",
|
||||
shipping_option_id: shippingOption.id,
|
||||
}
|
||||
|
||||
const rule = await service.createShippingOptionRules(ruleData)
|
||||
|
||||
expect(rule).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
attribute: ruleData.attribute,
|
||||
operator: ruleData.operator,
|
||||
value: ruleData.value,
|
||||
shipping_option_id: ruleData.shipping_option_id,
|
||||
})
|
||||
)
|
||||
|
||||
const rules = await service.listShippingOptionRules()
|
||||
expect(rules).toHaveLength(2)
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: rule.id,
|
||||
attribute: ruleData.attribute,
|
||||
operator: ruleData.operator,
|
||||
value: ruleData.value,
|
||||
shipping_option_id: shippingOption.id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: shippingOption.rules[0].id,
|
||||
attribute: shippingOption.rules[0].attribute,
|
||||
operator: shippingOption.rules[0].operator,
|
||||
value: shippingOption.rules[0].value,
|
||||
shipping_option_id: shippingOption.id,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update", () => {
|
||||
@@ -1680,6 +1819,698 @@ moduleIntegrationTestRunner({
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update shipping options", () => {
|
||||
it("should update a shipping option", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = {
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const shippingOption = await service.createShippingOptions(
|
||||
shippingOptionData
|
||||
)
|
||||
|
||||
const updateData = {
|
||||
id: shippingOption.id,
|
||||
name: "updated-test",
|
||||
price_type: "calculated",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "updated-test",
|
||||
description: "updated-test",
|
||||
label: "updated-test",
|
||||
},
|
||||
data: {
|
||||
amount: 2000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "new-test",
|
||||
operator: "eq",
|
||||
value: "new-test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const updatedShippingOption = await service.updateShippingOptions(
|
||||
updateData
|
||||
)
|
||||
|
||||
expect(updatedShippingOption).toEqual(
|
||||
expect.objectContaining({
|
||||
id: updateData.id,
|
||||
name: updateData.name,
|
||||
price_type: updateData.price_type,
|
||||
service_zone_id: updateData.service_zone_id,
|
||||
shipping_profile_id: updateData.shipping_profile_id,
|
||||
service_provider_id: updateData.service_provider_id,
|
||||
shipping_option_type_id: expect.any(String),
|
||||
type: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
code: updateData.type.code,
|
||||
description: updateData.type.description,
|
||||
label: updateData.type.label,
|
||||
}),
|
||||
data: updateData.data,
|
||||
rules: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
attribute: updateData.rules[0].attribute,
|
||||
operator: updateData.rules[0].operator,
|
||||
value: updateData.rules[0].value,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
const rules = await service.listShippingOptionRules()
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: updatedShippingOption.rules[0].id,
|
||||
})
|
||||
)
|
||||
|
||||
const types = await service.listShippingOptionTypes()
|
||||
expect(types).toHaveLength(1)
|
||||
expect(types[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
code: updateData.type.code,
|
||||
description: updateData.type.description,
|
||||
label: updateData.type.label,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update a shipping option without updating the rules or the type", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = {
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const shippingOption = await service.createShippingOptions(
|
||||
shippingOptionData
|
||||
)
|
||||
|
||||
const updateData = {
|
||||
id: shippingOption.id,
|
||||
name: "updated-test",
|
||||
price_type: "calculated",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
data: {
|
||||
amount: 2000,
|
||||
},
|
||||
}
|
||||
|
||||
await service.updateShippingOptions(updateData)
|
||||
|
||||
const updatedShippingOption = await service.retrieveShippingOption(
|
||||
shippingOption.id,
|
||||
{
|
||||
relations: ["rules", "type"],
|
||||
}
|
||||
)
|
||||
|
||||
expect(updatedShippingOption).toEqual(
|
||||
expect.objectContaining({
|
||||
id: updateData.id,
|
||||
name: updateData.name,
|
||||
price_type: updateData.price_type,
|
||||
service_zone_id: updateData.service_zone_id,
|
||||
shipping_profile_id: updateData.shipping_profile_id,
|
||||
service_provider_id: updateData.service_provider_id,
|
||||
shipping_option_type_id: expect.any(String),
|
||||
type: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
code: shippingOptionData.type.code,
|
||||
description: shippingOptionData.type.description,
|
||||
label: shippingOptionData.type.label,
|
||||
}),
|
||||
data: updateData.data,
|
||||
rules: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
attribute: shippingOptionData.rules[0].attribute,
|
||||
operator: shippingOptionData.rules[0].operator,
|
||||
value: shippingOptionData.rules[0].value,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
|
||||
const rules = await service.listShippingOptionRules()
|
||||
expect(rules).toHaveLength(1)
|
||||
expect(rules[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
id: updatedShippingOption.rules[0].id,
|
||||
})
|
||||
)
|
||||
|
||||
const types = await service.listShippingOptionTypes()
|
||||
expect(types).toHaveLength(1)
|
||||
expect(types[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
code: shippingOptionData.type.code,
|
||||
description: shippingOptionData.type.description,
|
||||
label: shippingOptionData.type.label,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should update a collection of shipping options", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = [
|
||||
{
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
price_type: "calculated",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const shippingOptions = await service.createShippingOptions(
|
||||
shippingOptionData
|
||||
)
|
||||
|
||||
const updateData = [
|
||||
{
|
||||
id: shippingOptions[0].id,
|
||||
name: "updated-test",
|
||||
price_type: "calculated",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "updated-test",
|
||||
description: "updated-test",
|
||||
label: "updated-test",
|
||||
},
|
||||
data: {
|
||||
amount: 2000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "new-test",
|
||||
operator: "eq",
|
||||
value: "new-test",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: shippingOptions[1].id,
|
||||
name: "updated-test",
|
||||
price_type: "calculated",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "updated-test",
|
||||
description: "updated-test",
|
||||
label: "updated-test",
|
||||
},
|
||||
data: {
|
||||
amount: 2000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "new-test",
|
||||
operator: "eq",
|
||||
value: "new-test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const updatedShippingOption = await service.updateShippingOptions(
|
||||
updateData
|
||||
)
|
||||
|
||||
for (const data_ of updateData) {
|
||||
const expectedShippingOption = updatedShippingOption.find(
|
||||
(shippingOption) => shippingOption.id === data_.id
|
||||
)
|
||||
expect(expectedShippingOption).toEqual(
|
||||
expect.objectContaining({
|
||||
id: data_.id,
|
||||
name: data_.name,
|
||||
price_type: data_.price_type,
|
||||
service_zone_id: data_.service_zone_id,
|
||||
shipping_profile_id: data_.shipping_profile_id,
|
||||
service_provider_id: data_.service_provider_id,
|
||||
shipping_option_type_id: expect.any(String),
|
||||
type: expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
code: data_.type.code,
|
||||
description: data_.type.description,
|
||||
label: data_.type.label,
|
||||
}),
|
||||
data: data_.data,
|
||||
rules: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
attribute: data_.rules[0].attribute,
|
||||
operator: data_.rules[0].operator,
|
||||
value: data_.rules[0].value,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const rules = await service.listShippingOptionRules()
|
||||
expect(rules).toHaveLength(2)
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: updatedShippingOption[0].rules[0].id,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: updatedShippingOption[1].rules[0].id,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
const types = await service.listShippingOptionTypes()
|
||||
expect(types).toHaveLength(2)
|
||||
expect(types).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: updateData[0].type.code,
|
||||
description: updateData[0].type.description,
|
||||
label: updateData[0].type.label,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
code: updateData[1].type.code,
|
||||
description: updateData[1].type.description,
|
||||
label: updateData[1].type.label,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to update a non-existent shipping option", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = {
|
||||
id: "sp_jdafwfleiwuonl",
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const err = await service
|
||||
.updateShippingOptions(shippingOptionData)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
`The following shipping options do not exist: ${shippingOptionData.id}`
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to update a shipping option when adding non existing rules", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = {
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const shippingOption = await service.createShippingOptions(
|
||||
shippingOptionData
|
||||
)
|
||||
|
||||
const updateData = [
|
||||
{
|
||||
id: shippingOption.id,
|
||||
rules: [
|
||||
{
|
||||
id: "sp_jdafwfleiwuonl",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const err = await service
|
||||
.updateShippingOptions(updateData)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
`The following rules does not exists: ${updateData[0].rules[0].id} on shipping option ${shippingOption.id}`
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to update a shipping option when adding invalid rules", async () => {
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOptionData = {
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const shippingOption = await service.createShippingOptions(
|
||||
shippingOptionData
|
||||
)
|
||||
|
||||
const updateData = [
|
||||
{
|
||||
id: shippingOption.id,
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "invalid",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const err = await service
|
||||
.updateShippingOptions(updateData)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
`Rule operator invalid is not supported. Must be one of in, eq, ne, gt, gte, lt, lte, nin`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("on update shipping option rules", () => {
|
||||
it("should update a shipping option rule", async () => {
|
||||
const shippingProfile = await service.createShippingProfiles({
|
||||
name: "test",
|
||||
type: "default",
|
||||
})
|
||||
const fulfillmentSet = await service.create({
|
||||
name: "test",
|
||||
type: "test-type",
|
||||
})
|
||||
const serviceZone = await service.createServiceZones({
|
||||
name: "test",
|
||||
fulfillment_set_id: fulfillmentSet.id,
|
||||
})
|
||||
const [serviceProvider] =
|
||||
await MikroOrmWrapper.forkManager().execute(
|
||||
"insert into service_provider (id) values ('sp_jdafwfleiwuonl') returning id"
|
||||
)
|
||||
|
||||
const shippingOption = await service.createShippingOptions({
|
||||
name: "test",
|
||||
price_type: "flat",
|
||||
service_zone_id: serviceZone.id,
|
||||
shipping_profile_id: shippingProfile.id,
|
||||
service_provider_id: serviceProvider.id,
|
||||
type: {
|
||||
code: "test",
|
||||
description: "test",
|
||||
label: "test",
|
||||
},
|
||||
data: {
|
||||
amount: 1000,
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
attribute: "test",
|
||||
operator: "eq",
|
||||
value: "test",
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const updateData = {
|
||||
id: shippingOption.rules[0].id,
|
||||
attribute: "updated-test",
|
||||
operator: "eq",
|
||||
value: "updated-test",
|
||||
}
|
||||
|
||||
const updatedRule = await service.updateShippingOptionRules(
|
||||
updateData
|
||||
)
|
||||
|
||||
expect(updatedRule).toEqual(
|
||||
expect.objectContaining({
|
||||
id: updateData.id,
|
||||
attribute: updateData.attribute,
|
||||
operator: updateData.operator,
|
||||
value: updateData.value,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to update a non-existent shipping option rule", async () => {
|
||||
const updateData = {
|
||||
id: "sp_jdafwfleiwuonl",
|
||||
attribute: "updated-test",
|
||||
operator: "eq",
|
||||
value: "updated-test",
|
||||
}
|
||||
|
||||
const err = await service
|
||||
.updateShippingOptionRules(updateData)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(err).toBeDefined()
|
||||
expect(err.message).toBe(
|
||||
`ShippingOptionRule with id "${updateData.id}" not found`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ module.exports = {
|
||||
"^@services": "<rootDir>/src/services",
|
||||
"^@repositories": "<rootDir>/src/repositories",
|
||||
"^@types": "<rootDir>/src/types",
|
||||
"^@utils": "<rootDir>/src/utils",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": [
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts",
|
||||
"test:integration": "jest --runInBand --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",
|
||||
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial -n InitialSetupMigration",
|
||||
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create",
|
||||
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up",
|
||||
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
|
||||
|
||||
@@ -493,7 +493,20 @@
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
"foreignKeys": {
|
||||
"service_zone_fulfillment_set_id_foreign": {
|
||||
"constraintName": "service_zone_fulfillment_set_id_foreign",
|
||||
"columnNames": [
|
||||
"fulfillment_set_id"
|
||||
],
|
||||
"localTableName": "public.service_zone",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.fulfillment_set",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
@@ -673,7 +686,20 @@
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
"foreignKeys": {
|
||||
"geo_zone_service_zone_id_foreign": {
|
||||
"constraintName": "geo_zone_service_zone_id_foreign",
|
||||
"columnNames": [
|
||||
"service_zone_id"
|
||||
],
|
||||
"localTableName": "public.geo_zone",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.service_zone",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
@@ -713,15 +739,6 @@
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"shipping_option_id": {
|
||||
"name": "shipping_option_id",
|
||||
"type": "text",
|
||||
"unsigned": false,
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamptz",
|
||||
@@ -758,16 +775,6 @@
|
||||
"name": "shipping_option_type",
|
||||
"schema": "public",
|
||||
"indexes": [
|
||||
{
|
||||
"keyName": "IDX_shipping_option_type_shipping_option_id",
|
||||
"columnNames": [
|
||||
"shipping_option_id"
|
||||
],
|
||||
"composite": false,
|
||||
"primary": false,
|
||||
"unique": false,
|
||||
"expression": "CREATE INDEX IF NOT EXISTS \"IDX_shipping_option_type_shipping_option_id\" ON \"shipping_option_type\" (shipping_option_id) WHERE deleted_at IS NULL"
|
||||
},
|
||||
{
|
||||
"keyName": "IDX_shipping_option_type_deleted_at",
|
||||
"columnNames": [
|
||||
@@ -1093,6 +1100,18 @@
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {
|
||||
"shipping_option_service_zone_id_foreign": {
|
||||
"constraintName": "shipping_option_service_zone_id_foreign",
|
||||
"columnNames": [
|
||||
"service_zone_id"
|
||||
],
|
||||
"localTableName": "public.shipping_option",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.service_zone",
|
||||
"updateRule": "cascade"
|
||||
},
|
||||
"shipping_option_shipping_profile_id_foreign": {
|
||||
"constraintName": "shipping_option_shipping_profile_id_foreign",
|
||||
"columnNames": [
|
||||
@@ -1161,7 +1180,17 @@
|
||||
"autoincrement": false,
|
||||
"primary": false,
|
||||
"nullable": false,
|
||||
"mappedType": "text"
|
||||
"enumItems": [
|
||||
"in",
|
||||
"eq",
|
||||
"ne",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"nin"
|
||||
],
|
||||
"mappedType": "enum"
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
@@ -1248,7 +1277,20 @@
|
||||
}
|
||||
],
|
||||
"checks": [],
|
||||
"foreignKeys": {}
|
||||
"foreignKeys": {
|
||||
"shipping_option_rule_shipping_option_id_foreign": {
|
||||
"constraintName": "shipping_option_rule_shipping_option_id_foreign",
|
||||
"columnNames": [
|
||||
"shipping_option_id"
|
||||
],
|
||||
"localTableName": "public.shipping_option_rule",
|
||||
"referencedColumnNames": [
|
||||
"id"
|
||||
],
|
||||
"referencedTableName": "public.shipping_option",
|
||||
"updateRule": "cascade"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"columns": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Migration } from '@mikro-orm/migrations';
|
||||
|
||||
export class Migration20240219115644 extends Migration {
|
||||
export class Migration20240221164918_InitialSetupMigration extends Migration {
|
||||
|
||||
async up(): Promise<void> {
|
||||
this.addSql('create table if not exists "fulfillment_address" ("id" text not null, "fulfillment_id" text null, "company" text null, "first_name" text null, "last_name" text null, "address_1" text null, "address_2" text null, "city" text null, "country_code" text null, "province" text null, "postal_code" text null, "phone" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "fulfillment_address_pkey" primary key ("id"));');
|
||||
@@ -26,8 +26,7 @@ export class Migration20240219115644 extends Migration {
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_service_zone_id" ON "geo_zone" (service_zone_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_geo_zone_deleted_at" ON "geo_zone" (deleted_at) WHERE deleted_at IS NOT NULL;');
|
||||
|
||||
this.addSql('create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_shipping_option_id" ON "shipping_option_type" (shipping_option_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('create table if not exists "shipping_option_type" ("id" text not null, "label" text not null, "description" text null, "code" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_type_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_type_deleted_at" ON "shipping_option_type" (deleted_at) WHERE deleted_at IS NOT NULL;');
|
||||
|
||||
this.addSql('create table if not exists "shipping_profile" ("id" text not null, "name" text not null, "type" text not null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_profile_pkey" primary key ("id"));');
|
||||
@@ -42,7 +41,7 @@ export class Migration20240219115644 extends Migration {
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_shipping_option_type_id" ON "shipping_option" (shipping_option_type_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_deleted_at" ON "shipping_option" (deleted_at) WHERE deleted_at IS NOT NULL;');
|
||||
|
||||
this.addSql('create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));');
|
||||
this.addSql('create table if not exists "shipping_option_rule" ("id" text not null, "attribute" text not null, "operator" text check ("operator" in (\'in\', \'eq\', \'ne\', \'gt\', \'gte\', \'lt\', \'lte\', \'nin\')) not null, "value" jsonb null, "shipping_option_id" text not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "shipping_option_rule_pkey" primary key ("id"));');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_shipping_option_id" ON "shipping_option_rule" (shipping_option_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_shipping_option_rule_deleted_at" ON "shipping_option_rule" (deleted_at) WHERE deleted_at IS NOT NULL;');
|
||||
|
||||
@@ -62,10 +61,17 @@ export class Migration20240219115644 extends Migration {
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_fulfillment_id" ON "fulfillment_item" (fulfillment_id) WHERE deleted_at IS NULL;');
|
||||
this.addSql('CREATE INDEX IF NOT EXISTS "IDX_fulfillment_item_deleted_at" ON "fulfillment_item" (deleted_at) WHERE deleted_at IS NOT NULL;');
|
||||
|
||||
this.addSql('alter table if exists "service_zone" add constraint "service_zone_fulfillment_set_id_foreign" foreign key ("fulfillment_set_id") references "fulfillment_set" ("id") on update cascade;');
|
||||
|
||||
this.addSql('alter table if exists "geo_zone" add constraint "geo_zone_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
|
||||
|
||||
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_zone_id_foreign" foreign key ("service_zone_id") references "service_zone" ("id") on update cascade;');
|
||||
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_profile_id_foreign" foreign key ("shipping_profile_id") references "shipping_profile" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_service_provider_id_foreign" foreign key ("service_provider_id") references "service_provider" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table if exists "shipping_option" add constraint "shipping_option_shipping_option_type_id_foreign" foreign key ("shipping_option_type_id") references "shipping_option_type" ("id") on update cascade on delete cascade;');
|
||||
|
||||
this.addSql('alter table if exists "shipping_option_rule" add constraint "shipping_option_rule_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade;');
|
||||
|
||||
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_shipping_option_id_foreign" foreign key ("shipping_option_id") references "shipping_option" ("id") on update cascade on delete set null;');
|
||||
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_provider_id_foreign" foreign key ("provider_id") references "service_provider" ("id") on update cascade;');
|
||||
this.addSql('alter table if exists "fulfillment" add constraint "fulfillment_delivery_address_id_foreign" foreign key ("delivery_address_id") references "fulfillment_address" ("id") on update cascade;');
|
||||
@@ -74,7 +74,11 @@ export default class GeoZone {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
city: string | null = null
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@ManyToOne(() => ServiceZone, {
|
||||
type: "text",
|
||||
mapToPk: true,
|
||||
fieldName: "service_zone_id",
|
||||
})
|
||||
@ServiceZoneIdIndex.MikroORMIndex()
|
||||
service_zone_id: string
|
||||
|
||||
|
||||
@@ -61,7 +61,11 @@ export default class ServiceZone {
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
metadata: Record<string, unknown> | null = null
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@ManyToOne(() => FulfillmentSet, {
|
||||
type: "text",
|
||||
mapToPk: true,
|
||||
fieldName: "fulfillment_set_id",
|
||||
})
|
||||
@FulfillmentSetIdIndex.MikroORMIndex()
|
||||
fulfillment_set_id: string
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DAL } from "@medusajs/types"
|
||||
import {
|
||||
BeforeCreate,
|
||||
Entity,
|
||||
Enum,
|
||||
Filter,
|
||||
ManyToOne,
|
||||
OnInit,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
Property,
|
||||
} from "@mikro-orm/core"
|
||||
import ShippingOption from "./shipping-option"
|
||||
import { RuleOperator } from "@utils"
|
||||
|
||||
type ShippingOptionRuleOptionalProps = DAL.SoftDeletableEntityDateColumns
|
||||
|
||||
@@ -42,17 +44,26 @@ export default class ShippingOptionRule {
|
||||
@Property({ columnType: "text" })
|
||||
attribute: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
operator: string
|
||||
@Enum({
|
||||
items: () => Object.values(RuleOperator),
|
||||
columnType: "text",
|
||||
})
|
||||
operator: Lowercase<keyof typeof RuleOperator>
|
||||
|
||||
@Property({ columnType: "jsonb", nullable: true })
|
||||
value: { value: string | string[] } | null = null
|
||||
value: string | string[] | null = null
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@ManyToOne(() => ShippingOption, {
|
||||
type: "text",
|
||||
mapToPk: true,
|
||||
fieldName: "shipping_option_id",
|
||||
})
|
||||
@ShippingOptionIdIndex.MikroORMIndex()
|
||||
shipping_option_id: string
|
||||
|
||||
@ManyToOne(() => ShippingOption, { persist: false })
|
||||
@ManyToOne(() => ShippingOption, {
|
||||
persist: false,
|
||||
})
|
||||
shipping_option: ShippingOption
|
||||
|
||||
@Property({
|
||||
|
||||
@@ -48,12 +48,8 @@ export default class ShippingOptionType {
|
||||
@Property({ columnType: "text" })
|
||||
code: string
|
||||
|
||||
@Property({ columnType: "text" })
|
||||
@ShippingOptionIdIndex.MikroORMIndex()
|
||||
shipping_option_id: string
|
||||
|
||||
@OneToOne(() => ShippingOption, (so) => so.type, {
|
||||
persist: false,
|
||||
type: "text",
|
||||
})
|
||||
shipping_option: ShippingOption
|
||||
|
||||
@@ -79,12 +75,10 @@ export default class ShippingOptionType {
|
||||
@BeforeCreate()
|
||||
onCreate() {
|
||||
this.id = generateEntityId(this.id, "sotype")
|
||||
this.shipping_option_id ??= this.shipping_option?.id
|
||||
}
|
||||
|
||||
@OnInit()
|
||||
onInit() {
|
||||
this.id = generateEntityId(this.id, "sotype")
|
||||
this.shipping_option_id ??= this.shipping_option?.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export default class ShippingOption {
|
||||
})
|
||||
price_type: ShippingOptionPriceType
|
||||
|
||||
@ManyToOne(() => ShippingProfile, {
|
||||
@ManyToOne(() => ServiceZone, {
|
||||
type: "text",
|
||||
fieldName: "service_zone_id",
|
||||
mapToPk: true,
|
||||
@@ -129,6 +129,7 @@ export default class ShippingOption {
|
||||
@OneToOne(() => ShippingOptionType, (so) => so.shipping_option, {
|
||||
owner: true,
|
||||
cascade: [Cascade.PERSIST, Cascade.REMOVE, "soft-remove"] as any,
|
||||
orphanRemoval: true,
|
||||
fieldName: "shipping_option_type_id",
|
||||
})
|
||||
type: ShippingOptionType
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UpdateFulfillmentSetDTO,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
arrayDifference,
|
||||
getSetDifference,
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
@@ -24,14 +25,19 @@ import {
|
||||
GeoZone,
|
||||
ServiceZone,
|
||||
ShippingOption,
|
||||
ShippingOptionRule,
|
||||
ShippingOptionType,
|
||||
ShippingProfile,
|
||||
} from "@models"
|
||||
import { validateRules } from "@utils"
|
||||
|
||||
const generateMethodForModels = [
|
||||
ServiceZone,
|
||||
ShippingOption,
|
||||
GeoZone,
|
||||
ShippingProfile,
|
||||
ShippingOptionRule,
|
||||
ShippingOptionType,
|
||||
]
|
||||
|
||||
type InjectedDependencies = {
|
||||
@@ -41,6 +47,8 @@ type InjectedDependencies = {
|
||||
geoZoneService: ModulesSdkTypes.InternalModuleService<any>
|
||||
shippingProfileService: ModulesSdkTypes.InternalModuleService<any>
|
||||
shippingOptionService: ModulesSdkTypes.InternalModuleService<any>
|
||||
shippingOptionRuleService: ModulesSdkTypes.InternalModuleService<any>
|
||||
shippingOptionTypeService: ModulesSdkTypes.InternalModuleService<any>
|
||||
}
|
||||
|
||||
export default class FulfillmentModuleService<
|
||||
@@ -48,7 +56,9 @@ export default class FulfillmentModuleService<
|
||||
TServiceZoneEntity extends ServiceZone = ServiceZone,
|
||||
TGeoZoneEntity extends GeoZone = GeoZone,
|
||||
TShippingProfileEntity extends ShippingProfile = ShippingProfile,
|
||||
TShippingOptionEntity extends ShippingOption = ShippingOption
|
||||
TShippingOptionEntity extends ShippingOption = ShippingOption,
|
||||
TShippingOptionRuleEntity extends ShippingOptionRule = ShippingOptionRule,
|
||||
TSippingOptionTypeEntity extends ShippingOptionType = ShippingOptionType
|
||||
>
|
||||
extends ModulesSdkUtils.abstractModuleServiceFactory<
|
||||
InjectedDependencies,
|
||||
@@ -59,6 +69,8 @@ export default class FulfillmentModuleService<
|
||||
ShippingOption: { dto: FulfillmentTypes.ShippingOptionDTO }
|
||||
GeoZone: { dto: FulfillmentTypes.GeoZoneDTO }
|
||||
ShippingProfile: { dto: FulfillmentTypes.ShippingProfileDTO }
|
||||
ShippingOptionRule: { dto: FulfillmentTypes.ShippingOptionRuleDTO }
|
||||
ShippingOptionType: { dto: FulfillmentTypes.ShippingOptionTypeDTO }
|
||||
}
|
||||
>(FulfillmentSet, generateMethodForModels, entityNameToLinkableKeysMap)
|
||||
implements IFulfillmentModuleService
|
||||
@@ -69,6 +81,8 @@ export default class FulfillmentModuleService<
|
||||
protected readonly geoZoneService_: ModulesSdkTypes.InternalModuleService<TGeoZoneEntity>
|
||||
protected readonly shippingProfileService_: ModulesSdkTypes.InternalModuleService<TShippingProfileEntity>
|
||||
protected readonly shippingOptionService_: ModulesSdkTypes.InternalModuleService<TShippingOptionEntity>
|
||||
protected readonly shippingOptionRuleService_: ModulesSdkTypes.InternalModuleService<TShippingOptionRuleEntity>
|
||||
protected readonly shippingOptionTypeService_: ModulesSdkTypes.InternalModuleService<TSippingOptionTypeEntity>
|
||||
|
||||
constructor(
|
||||
{
|
||||
@@ -78,6 +92,8 @@ export default class FulfillmentModuleService<
|
||||
geoZoneService,
|
||||
shippingProfileService,
|
||||
shippingOptionService,
|
||||
shippingOptionRuleService,
|
||||
shippingOptionTypeService,
|
||||
}: InjectedDependencies,
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
@@ -89,6 +105,8 @@ export default class FulfillmentModuleService<
|
||||
this.geoZoneService_ = geoZoneService
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.shippingOptionRuleService_ = shippingOptionRuleService
|
||||
this.shippingOptionTypeService_ = shippingOptionTypeService
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
@@ -235,6 +253,11 @@ export default class FulfillmentModuleService<
|
||||
return []
|
||||
}
|
||||
|
||||
const rules = data_.flatMap((d) => d.rules)
|
||||
if (rules.length) {
|
||||
validateRules(rules as Record<string, unknown>[])
|
||||
}
|
||||
|
||||
const createdShippingOptions = await this.shippingOptionService_.create(
|
||||
data_,
|
||||
sharedContext
|
||||
@@ -328,6 +351,61 @@ export default class FulfillmentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
async createShippingOptionRules(
|
||||
data: FulfillmentTypes.CreateShippingOptionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<FulfillmentTypes.ShippingOptionRuleDTO[]>
|
||||
async createShippingOptionRules(
|
||||
data: FulfillmentTypes.CreateShippingOptionRuleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async createShippingOptionRules(
|
||||
data:
|
||||
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
|
||||
| FulfillmentTypes.CreateShippingOptionRuleDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO[]
|
||||
> {
|
||||
const createdShippingOptionRules = await this.createShippingOptionRules_(
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize<
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO[]
|
||||
>(createdShippingOptionRules, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async createShippingOptionRules_(
|
||||
data:
|
||||
| FulfillmentTypes.CreateShippingOptionRuleDTO[]
|
||||
| FulfillmentTypes.CreateShippingOptionRuleDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TShippingOptionRuleEntity | TShippingOptionRuleEntity[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
validateRules(data_ as unknown as Record<string, unknown>[])
|
||||
|
||||
const createdShippingOptionRules =
|
||||
await this.shippingOptionRuleService_.create(data_, sharedContext)
|
||||
|
||||
return Array.isArray(data)
|
||||
? createdShippingOptionRules
|
||||
: createdShippingOptionRules[0]
|
||||
}
|
||||
|
||||
update(
|
||||
data: FulfillmentTypes.UpdateFulfillmentSetDTO[],
|
||||
sharedContext?: Context
|
||||
@@ -375,6 +453,7 @@ export default class FulfillmentModuleService<
|
||||
},
|
||||
{
|
||||
relations: ["service_zones", "service_zones.geo_zones"],
|
||||
take: fulfillmentSetIds.length,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -557,6 +636,7 @@ export default class FulfillmentModuleService<
|
||||
},
|
||||
{
|
||||
relations: ["geo_zones"],
|
||||
take: serviceZoneIds.length,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
@@ -662,7 +742,7 @@ export default class FulfillmentModuleService<
|
||||
sharedContext?: Context
|
||||
): Promise<FulfillmentTypes.ShippingOptionDTO>
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
@InjectManager("baseRepository_")
|
||||
async updateShippingOptions(
|
||||
data:
|
||||
| FulfillmentTypes.UpdateShippingOptionDTO[]
|
||||
@@ -671,7 +751,123 @@ export default class FulfillmentModuleService<
|
||||
): Promise<
|
||||
FulfillmentTypes.ShippingOptionDTO[] | FulfillmentTypes.ShippingOptionDTO
|
||||
> {
|
||||
return []
|
||||
const updatedShippingOptions = await this.updateShippingOptions_(
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize<
|
||||
FulfillmentTypes.ShippingOptionDTO | FulfillmentTypes.ShippingOptionDTO[]
|
||||
>(updatedShippingOptions, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async updateShippingOptions_(
|
||||
data:
|
||||
| FulfillmentTypes.UpdateShippingOptionDTO[]
|
||||
| FulfillmentTypes.UpdateShippingOptionDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TShippingOptionEntity | TShippingOptionEntity[]> {
|
||||
const dataArray = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!dataArray.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const shippingOptionIds = dataArray.map((s) => s.id)
|
||||
if (!shippingOptionIds.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const shippingOptions = await this.shippingOptionService_.list(
|
||||
{
|
||||
id: shippingOptionIds,
|
||||
},
|
||||
{
|
||||
relations: ["rules"],
|
||||
take: shippingOptionIds.length,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
const existingShippingOptions = new Map(
|
||||
shippingOptions.map((s) => [s.id, s])
|
||||
)
|
||||
|
||||
FulfillmentModuleService.validateMissingShippingOptions_(
|
||||
shippingOptions,
|
||||
dataArray
|
||||
)
|
||||
|
||||
const ruleIdsToDelete: string[] = []
|
||||
dataArray.forEach((shippingOption) => {
|
||||
if (!shippingOption.rules) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingShippingOption = existingShippingOptions.get(
|
||||
shippingOption.id
|
||||
)! // Garuantueed to exist since the validation above have been performed
|
||||
const existingRules = existingShippingOption.rules
|
||||
|
||||
FulfillmentModuleService.validateMissingShippingOptionRules(
|
||||
existingShippingOption,
|
||||
shippingOption
|
||||
)
|
||||
|
||||
const existingRulesMap = new Map(
|
||||
existingRules.map((rule) => [rule.id, rule])
|
||||
)
|
||||
|
||||
const updatedRules = shippingOption.rules
|
||||
const updatedRuleIds = updatedRules
|
||||
.map((r) => "id" in r && r.id)
|
||||
.filter((id): id is string => !!id)
|
||||
|
||||
const toDeleteRuleIds = arrayDifference(
|
||||
updatedRuleIds,
|
||||
Array.from(existingRulesMap.keys())
|
||||
) as string[]
|
||||
|
||||
if (toDeleteRuleIds.length) {
|
||||
ruleIdsToDelete.push(...toDeleteRuleIds)
|
||||
}
|
||||
|
||||
const newRules = updatedRules
|
||||
.map((rule) => {
|
||||
if (!("id" in rule)) {
|
||||
return rule
|
||||
}
|
||||
return
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
validateRules(newRules as Record<string, unknown>[])
|
||||
|
||||
shippingOption.rules = shippingOption.rules.map((rule) => {
|
||||
if (!("id" in rule)) {
|
||||
return rule
|
||||
}
|
||||
return existingRulesMap.get(rule.id)!
|
||||
})
|
||||
})
|
||||
|
||||
if (ruleIdsToDelete.length) {
|
||||
await this.shippingOptionRuleService_.delete(
|
||||
ruleIdsToDelete,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
const updatedShippingOptions = await this.shippingOptionService_.update(
|
||||
dataArray,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return Array.isArray(data)
|
||||
? updatedShippingOptions
|
||||
: updatedShippingOptions[0]
|
||||
}
|
||||
|
||||
updateShippingProfiles(
|
||||
@@ -724,4 +920,107 @@ export default class FulfillmentModuleService<
|
||||
|
||||
return Array.isArray(data) ? serialized : serialized[0]
|
||||
}
|
||||
|
||||
updateShippingOptionRules(
|
||||
data: FulfillmentTypes.UpdateShippingOptionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<FulfillmentTypes.ShippingOptionRuleDTO[]>
|
||||
updateShippingOptionRules(
|
||||
data: FulfillmentTypes.UpdateShippingOptionRuleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<FulfillmentTypes.ShippingOptionRuleDTO>
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async updateShippingOptionRules(
|
||||
data:
|
||||
| FulfillmentTypes.UpdateShippingOptionRuleDTO[]
|
||||
| FulfillmentTypes.UpdateShippingOptionRuleDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO[]
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO
|
||||
> {
|
||||
const updatedShippingOptionRules = await this.updateShippingOptionRules_(
|
||||
data,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return await this.baseRepository_.serialize<
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO
|
||||
| FulfillmentTypes.ShippingOptionRuleDTO[]
|
||||
>(updatedShippingOptionRules, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager("baseRepository_")
|
||||
async updateShippingOptionRules_(
|
||||
data:
|
||||
| FulfillmentTypes.UpdateShippingOptionRuleDTO[]
|
||||
| FulfillmentTypes.UpdateShippingOptionRuleDTO,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TShippingOptionRuleEntity | TShippingOptionRuleEntity[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
|
||||
if (!data_.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
validateRules(data_ as unknown as Record<string, unknown>[])
|
||||
|
||||
const updatedShippingOptionRules =
|
||||
await this.shippingOptionRuleService_.update(data_, sharedContext)
|
||||
|
||||
return Array.isArray(data)
|
||||
? updatedShippingOptionRules
|
||||
: updatedShippingOptionRules[0]
|
||||
}
|
||||
|
||||
protected static validateMissingShippingOptions_(
|
||||
shippingOptions: ShippingOption[],
|
||||
shippingOptionsData: FulfillmentTypes.UpdateShippingOptionDTO[]
|
||||
) {
|
||||
const missingShippingOptionIds = arrayDifference(
|
||||
shippingOptionsData.map((s) => s.id),
|
||||
shippingOptions.map((s) => s.id)
|
||||
)
|
||||
|
||||
if (missingShippingOptionIds.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`The following shipping options do not exist: ${Array.from(
|
||||
missingShippingOptionIds
|
||||
).join(", ")}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected static validateMissingShippingOptionRules(
|
||||
shippingOption: ShippingOption,
|
||||
shippingOptionUpdateData: FulfillmentTypes.UpdateShippingOptionDTO
|
||||
) {
|
||||
if (!shippingOptionUpdateData.rules) {
|
||||
return
|
||||
}
|
||||
|
||||
const existingRules = shippingOption.rules
|
||||
|
||||
const rulesSet = new Set(existingRules.map((r) => r.id))
|
||||
// Only validate the rules that have an id to validate that they really exists in the shipping option
|
||||
const expectedRuleSet = new Set(
|
||||
shippingOptionUpdateData.rules
|
||||
.map((r) => "id" in r && r.id)
|
||||
.filter((id): id is string => !!id)
|
||||
)
|
||||
const nonAlreadyExistingRules = getSetDifference(expectedRuleSet, rulesSet)
|
||||
|
||||
if (nonAlreadyExistingRules.size) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`The following rules does not exists: ${Array.from(
|
||||
nonAlreadyExistingRules
|
||||
).join(", ")} on shipping option ${shippingOptionUpdateData.id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
248
packages/fulfillment/src/utils/__tests__/utils.spec.ts
Normal file
248
packages/fulfillment/src/utils/__tests__/utils.spec.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { isContextValidForRules, RuleOperator } from "../utils"
|
||||
|
||||
describe("isContextValidForRules", () => {
|
||||
const context = {
|
||||
attribute1: "value1",
|
||||
attribute2: "value2",
|
||||
attribute3: "value3",
|
||||
}
|
||||
|
||||
const validRule = {
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.EQ,
|
||||
value: "value1",
|
||||
}
|
||||
|
||||
const invalidRule = {
|
||||
attribute: "attribute2",
|
||||
operator: RuleOperator.EQ,
|
||||
value: "wrongValue",
|
||||
}
|
||||
|
||||
it("returns true when all rules are valid and atLeastOneValidRule is false", () => {
|
||||
const rules = [validRule, validRule]
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true when all rules are valid and atLeastOneValidRule is true", () => {
|
||||
const rules = [validRule, validRule]
|
||||
const options = { atLeastOneValidRule: true }
|
||||
expect(isContextValidForRules(context, rules, options)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns true when some rules are valid and atLeastOneValidRule is true", () => {
|
||||
const rules = [validRule, invalidRule]
|
||||
const options = { atLeastOneValidRule: true }
|
||||
expect(isContextValidForRules(context, rules, options)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when some rules are valid and atLeastOneValidRule is false", () => {
|
||||
const rules = [validRule, invalidRule]
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when no rules are valid and atLeastOneValidRule is true", () => {
|
||||
const rules = [invalidRule, invalidRule]
|
||||
const options = { atLeastOneValidRule: true }
|
||||
expect(isContextValidForRules(context, rules, options)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns false when no rules are valid and atLeastOneValidRule is false", () => {
|
||||
const rules = [invalidRule, invalidRule]
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'gt' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.GT,
|
||||
value: "1", // 2 > 1
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'gt' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.GT,
|
||||
value: "0", // 0 > 0
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "0" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'gte' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.GTE,
|
||||
value: "2", // 2 >= 2
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'gte' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.GTE,
|
||||
value: "3", // 2 >= 3
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'lt' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.LT,
|
||||
value: "3", // 2 < 3
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'lt' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.LT,
|
||||
value: "2", // 2 < 2
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'lte' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.LTE,
|
||||
value: "2", // 2 <= 2
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
// ... existing tests ...
|
||||
|
||||
it("returns false when the 'lte' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.LTE,
|
||||
value: "1", // 2 <= 1
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'in' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.IN,
|
||||
value: ["1", "2", "3"], // 2 in [1, 2, 3]
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'in' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.IN,
|
||||
value: ["1", "3", "4"], // 2 in [1, 3, 4]
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'nin' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.NIN,
|
||||
value: ["1", "3", "4"], // 2 not in [1, 3, 4]
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'nin' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.NIN,
|
||||
value: ["1", "2", "3"], // 2 not in [1, 2, 3]
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'ne' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.NE,
|
||||
value: "1", // 2 != 1
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'ne' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.NE,
|
||||
value: "2", // 2 != 2
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
|
||||
it("returns true when the 'eq' operator is valid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.EQ,
|
||||
value: "2", // 2 == 2
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(true)
|
||||
})
|
||||
|
||||
it("returns false when the 'eq' operator is invalid", () => {
|
||||
const rules = [
|
||||
{
|
||||
attribute: "attribute1",
|
||||
operator: RuleOperator.EQ,
|
||||
value: "1", // 2 == 1
|
||||
},
|
||||
]
|
||||
const context = { attribute1: "2" }
|
||||
expect(isContextValidForRules(context, rules)).toBe(false)
|
||||
})
|
||||
})
|
||||
1
packages/fulfillment/src/utils/index.ts
Normal file
1
packages/fulfillment/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './utils'
|
||||
152
packages/fulfillment/src/utils/utils.ts
Normal file
152
packages/fulfillment/src/utils/utils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { isString, MedusaError, pickValueFromObject } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* The rule engine here is kept inside the module as of now, but it could be moved
|
||||
* to the utils package and be used across the different modules that provides context
|
||||
* based rule filtering.
|
||||
*
|
||||
* TODO: discussion around that should happen at some point
|
||||
*/
|
||||
|
||||
export type Rule = {
|
||||
attribute: string
|
||||
operator: RuleOperator
|
||||
value: string | string[]
|
||||
}
|
||||
|
||||
export enum RuleOperator {
|
||||
IN = "in",
|
||||
EQ = "eq",
|
||||
NE = "ne",
|
||||
GT = "gt",
|
||||
GTE = "gte",
|
||||
LT = "lt",
|
||||
LTE = "lte",
|
||||
NIN = "nin",
|
||||
}
|
||||
|
||||
export const availableOperators = Object.values(RuleOperator)
|
||||
|
||||
const isDate = (str: string) => {
|
||||
return !isNaN(Date.parse(str))
|
||||
}
|
||||
|
||||
const operatorsPredicate = {
|
||||
in: (contextValue: string, ruleValue: string[]) =>
|
||||
ruleValue.includes(contextValue),
|
||||
nin: (contextValue: string, ruleValue: string[]) =>
|
||||
!ruleValue.includes(contextValue),
|
||||
eq: (contextValue: string, ruleValue: string) => contextValue === ruleValue,
|
||||
ne: (contextValue: string, ruleValue: string) => contextValue !== ruleValue,
|
||||
gt: (contextValue: string, ruleValue: string) => {
|
||||
if (isDate(contextValue) && isDate(ruleValue)) {
|
||||
return new Date(contextValue) > new Date(ruleValue)
|
||||
}
|
||||
return Number(contextValue) > Number(ruleValue)
|
||||
},
|
||||
gte: (contextValue: string, ruleValue: string) => {
|
||||
if (isDate(contextValue) && isDate(ruleValue)) {
|
||||
return new Date(contextValue) >= new Date(ruleValue)
|
||||
}
|
||||
return Number(contextValue) >= Number(ruleValue)
|
||||
},
|
||||
lt: (contextValue: string, ruleValue: string) => {
|
||||
if (isDate(contextValue) && isDate(ruleValue)) {
|
||||
return new Date(contextValue) < new Date(ruleValue)
|
||||
}
|
||||
return Number(contextValue) < Number(ruleValue)
|
||||
},
|
||||
lte: (contextValue: string, ruleValue: string) => {
|
||||
if (isDate(contextValue) && isDate(ruleValue)) {
|
||||
return new Date(contextValue) <= new Date(ruleValue)
|
||||
}
|
||||
return Number(contextValue) <= Number(ruleValue)
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate contextValue context object from contextValue set of rules.
|
||||
* By default, all rules must be valid to return true unless the option atLeastOneValidRule is set to true.
|
||||
* @param context
|
||||
* @param rules
|
||||
* @param options
|
||||
*/
|
||||
export function isContextValidForRules(
|
||||
context: Record<string, any>,
|
||||
rules: Rule[],
|
||||
options: {
|
||||
atLeastOneValidRule: boolean
|
||||
} = {
|
||||
atLeastOneValidRule: false,
|
||||
}
|
||||
) {
|
||||
const { atLeastOneValidRule } = options
|
||||
|
||||
const loopComparator = atLeastOneValidRule ? rules.some : rules.every
|
||||
const predicate = (rule) => {
|
||||
const { attribute, operator, value } = rule
|
||||
const contextValue = pickValueFromObject(attribute, context)
|
||||
return operatorsPredicate[operator](
|
||||
contextValue,
|
||||
value as string & string[]
|
||||
)
|
||||
}
|
||||
|
||||
return loopComparator.apply(rules, [predicate])
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate contextValue rule object
|
||||
* @param rule
|
||||
*/
|
||||
export function validateRule(rule: Record<string, unknown>): boolean {
|
||||
if (!rule.attribute || !rule.operator || !rule.value) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Rule must have an attribute, an operator and contextValue value"
|
||||
)
|
||||
}
|
||||
|
||||
if (!isString(rule.attribute)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Rule attribute must be contextValue string"
|
||||
)
|
||||
}
|
||||
|
||||
if (!isString(rule.operator)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Rule operator must be contextValue string"
|
||||
)
|
||||
}
|
||||
|
||||
if (!availableOperators.includes(rule.operator as RuleOperator)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Rule operator ${
|
||||
rule.operator
|
||||
} is not supported. Must be one of ${availableOperators.join(", ")}`
|
||||
)
|
||||
}
|
||||
|
||||
if (rule.operator === RuleOperator.IN || rule.operator === RuleOperator.NIN) {
|
||||
if (!Array.isArray(rule.value)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Rule value must be an array for in/nin operators"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate contextValue set of rules
|
||||
* @param rules
|
||||
*/
|
||||
export function validateRules(rules: Record<string, unknown>[]): boolean {
|
||||
rules.forEach(validateRule)
|
||||
return true
|
||||
}
|
||||
@@ -23,7 +23,8 @@
|
||||
"@models": ["./src/models"],
|
||||
"@services": ["./src/services"],
|
||||
"@repositories": ["./src/repositories"],
|
||||
"@types": ["./src/types"]
|
||||
"@types": ["./src/types"],
|
||||
"@utils": ["./src/utils"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextFunction, Request, Response } from "express"
|
||||
|
||||
import type { Customer, User } from "../models"
|
||||
import type { MedusaContainer } from "./global"
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
|
||||
export interface MedusaRequest extends Request {
|
||||
user?: (User | Customer) & { customer_id?: string; userId?: string }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface CreateShippingOptionRuleDTO {
|
||||
attribute: string
|
||||
operator: string
|
||||
operator: "in" | "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "nin"
|
||||
value: string | string[]
|
||||
shipping_option_id: string
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ import {
|
||||
FilterableGeoZoneProps,
|
||||
FilterableServiceZoneProps,
|
||||
FilterableShippingOptionProps,
|
||||
FilterableShippingOptionRuleProps,
|
||||
FilterableShippingOptionTypeProps,
|
||||
FilterableShippingProfileProps,
|
||||
FulfillmentSetDTO,
|
||||
GeoZoneDTO,
|
||||
ServiceZoneDTO,
|
||||
ShippingOptionDTO,
|
||||
ShippingOptionRuleDTO,
|
||||
ShippingOptionTypeDTO,
|
||||
ShippingProfileDTO,
|
||||
} from "./common"
|
||||
import { FindConfig } from "../common"
|
||||
@@ -19,10 +23,12 @@ import {
|
||||
CreateGeoZoneDTO,
|
||||
CreateServiceZoneDTO,
|
||||
CreateShippingOptionDTO,
|
||||
CreateShippingOptionRuleDTO,
|
||||
UpdateFulfillmentSetDTO,
|
||||
UpdateGeoZoneDTO,
|
||||
UpdateServiceZoneDTO,
|
||||
UpdateShippingOptionDTO,
|
||||
UpdateShippingOptionRuleDTO,
|
||||
} from "./mutations"
|
||||
import { CreateShippingProfileDTO } from "./mutations/shipping-profile"
|
||||
|
||||
@@ -98,6 +104,20 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<GeoZoneDTO>
|
||||
|
||||
/**
|
||||
* Create a new shipping option rules
|
||||
* @param data
|
||||
* @param sharedContext
|
||||
*/
|
||||
createShippingOptionRules(
|
||||
data: CreateShippingOptionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO[]>
|
||||
createShippingOptionRules(
|
||||
data: CreateShippingOptionRuleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO>
|
||||
|
||||
/**
|
||||
* Update a fulfillment set
|
||||
* @param data
|
||||
@@ -168,6 +188,20 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<GeoZoneDTO>
|
||||
|
||||
/**
|
||||
* Update a shipping option rule
|
||||
* @param data
|
||||
* @param sharedContext
|
||||
*/
|
||||
updateShippingOptionRules(
|
||||
data: UpdateShippingOptionRuleDTO[],
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO[]>
|
||||
updateShippingOptionRules(
|
||||
data: UpdateShippingOptionRuleDTO,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO>
|
||||
|
||||
/**
|
||||
* Delete a fulfillment set
|
||||
* @param ids
|
||||
@@ -208,6 +242,17 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
deleteGeoZones(ids: string[], sharedContext?: Context): Promise<void>
|
||||
deleteGeoZones(id: string, sharedContext?: Context): Promise<void>
|
||||
|
||||
/**
|
||||
* Delete a shipping option rule
|
||||
* @param ids
|
||||
* @param sharedContext
|
||||
*/
|
||||
deleteShippingOptionRules(
|
||||
ids: string[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
deleteShippingOptionRules(id: string, sharedContext?: Context): Promise<void>
|
||||
|
||||
/**
|
||||
* Retrieve a fulfillment set
|
||||
* @param id
|
||||
@@ -268,6 +313,30 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<GeoZoneDTO>
|
||||
|
||||
/**
|
||||
* Retrieve a shipping option rule
|
||||
* @param id
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
retrieveShippingOptionRule(
|
||||
id: string,
|
||||
config?: FindConfig<ShippingOptionRuleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO>
|
||||
|
||||
/**
|
||||
* Retrieve a shipping option type
|
||||
* @param id
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
retrieveShippingOptionType(
|
||||
id: string,
|
||||
config?: FindConfig<ShippingOptionTypeDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionTypeDTO>
|
||||
|
||||
/**
|
||||
* List fulfillment sets
|
||||
* @param filters
|
||||
@@ -328,6 +397,30 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<GeoZoneDTO[]>
|
||||
|
||||
/**
|
||||
* List shipping option rules
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listShippingOptionRules(
|
||||
filters?: FilterableShippingOptionRuleProps,
|
||||
config?: FindConfig<ShippingOptionRuleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionRuleDTO[]>
|
||||
|
||||
/**
|
||||
* List shipping option types
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listShippingOptionTypes(
|
||||
filters?: FilterableShippingOptionTypeProps,
|
||||
config?: FindConfig<ShippingOptionTypeDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<ShippingOptionTypeDTO[]>
|
||||
|
||||
/**
|
||||
* List and count fulfillment sets
|
||||
* @param filters
|
||||
@@ -388,6 +481,30 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<[GeoZoneDTO[], number]>
|
||||
|
||||
/**
|
||||
* List and count shipping option rules
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listAndCountShippingOptionRules(
|
||||
filters?: FilterableShippingOptionRuleProps,
|
||||
config?: FindConfig<ShippingOptionRuleDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ShippingOptionRuleDTO[], number]>
|
||||
|
||||
/**
|
||||
* List and count shipping options types
|
||||
* @param filters
|
||||
* @param config
|
||||
* @param sharedContext
|
||||
*/
|
||||
listAndCountShippingOptionTypes(
|
||||
filters?: FilterableShippingOptionTypeProps,
|
||||
config?: FindConfig<ShippingOptionTypeDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<[ShippingOptionTypeDTO[], number]>
|
||||
|
||||
/**
|
||||
* Soft delete fulfillment sets
|
||||
* @param fulfillmentIds
|
||||
@@ -454,5 +571,5 @@ export interface IFulfillmentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
// TODO defined the other restore methods
|
||||
// TODO define needed soft delete/delete/restore methods
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function internalModuleServiceFactory<
|
||||
}
|
||||
|
||||
static buildUniqueCompositeKeyValue(keys: string[], data: object) {
|
||||
return keys.map((k) => data[k]).join("_")
|
||||
return keys.map((k) => data[k]).join(":")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,7 +287,7 @@ export function internalModuleServiceFactory<
|
||||
|
||||
;[...keySelectorDataMap.keys()].filter((key) => {
|
||||
if (!compositeKeysValuesForFoundEntities.has(key)) {
|
||||
const value = key.replace(/_/gi, " - ")
|
||||
const value = key.replace(/:/gi, " - ")
|
||||
missingEntityValues.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user