Merge branch 'develop' into feat/payment-module-models

This commit is contained in:
Frane Polić authored and GitHub committed 2024-01-15 11:19:55 +01:00
commit efdc59fd53
101 files changed
+4526 -208

No files matched your search

+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/authentication": patch
"@medusajs/pricing": patch
"@medusajs/utils": patch
---
chore(utils): Update base repository to infer primary keys and support composite
+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(utils,types): added item/shipping adjustments for order/items/shipping_methods
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/inventory": patch
"@medusajs/stock-location": patch
"@medusajs/types": patch
---
feat(cart): Partial module service implementation
+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(utils,types): campaigns and campaign budgets + services CRUD
+3 -2
View File
@@ -72,7 +72,9 @@ module.exports = {
node: true, node: true,
jest: true, jest: true,
}, },
ignorePatterns: [], ignorePatterns: [
"packages/admin-next/dashboard/**/dist"
],
overrides: [ overrides: [
{ {
files: ["*.ts"], files: ["*.ts"],
@@ -232,7 +234,6 @@ module.exports = {
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended", "plugin:react-hooks/recommended",
], ],
ignorePatterns: ["dist"],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
project: "./packages/admin-next/dashboard/tsconfig.json", project: "./packages/admin-next/dashboard/tsconfig.json",
@@ -998,7 +998,7 @@
}, },
{ {
"kind": "code", "kind": "code",
"text": "```ts\nclass MyFulfillmentService extends AbstractFulfillmentService {\n // ...\n async createFulfillment(\n data: Record<string, unknown>,\n items: LineItem,\n order: Order,\n fulfillment: Fulfillment\n ) {\n // No data is being sent anywhere\n // No data to be stored in the fulfillment's data object\n return {}\n }\n}\n```" "text": "```ts\nclass MyFulfillmentService extends AbstractFulfillmentService {\n // ...\n async createFulfillment(\n data: Record<string, unknown>,\n items: LineItem[],\n order: Order,\n fulfillment: Fulfillment\n ) {\n // No data is being sent anywhere\n // No data to be stored in the fulfillment's data object\n return {}\n }\n}\n```"
} }
] ]
} }
@@ -3356,7 +3356,7 @@
}, },
{ {
"kind": "code", "kind": "code",
"text": "```ts\nclass MyFulfillmentService extends AbstractFulfillmentService {\n // ...\n async createFulfillment(\n data: Record<string, unknown>,\n items: LineItem,\n order: Order,\n fulfillment: Fulfillment\n ) {\n // No data is being sent anywhere\n // No data to be stored in the fulfillment's data object\n return {}\n }\n}\n```" "text": "```ts\nclass MyFulfillmentService extends AbstractFulfillmentService {\n // ...\n async createFulfillment(\n data: Record<string, unknown>,\n items: LineItem[],\n order: Order,\n fulfillment: Fulfillment\n ) {\n // No data is being sent anywhere\n // No data to be stored in the fulfillment's data object\n return {}\n }\n}\n```"
} }
] ]
} }
@@ -10,7 +10,12 @@ export class AuthProviderRepository extends DALUtils.mikroOrmBaseRepositoryFacto
{ {
create: RepositoryTypes.CreateAuthProviderDTO create: RepositoryTypes.CreateAuthProviderDTO
} }
>(AuthProvider, "provider") { >(AuthProvider) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async update( async update(
data: RepositoryTypes.UpdateAuthProviderDTO[], data: RepositoryTypes.UpdateAuthProviderDTO[],
context: Context = {} context: Context = {}
@@ -8,6 +8,11 @@ import { SqlEntityManager } from "@mikro-orm/postgresql"
export class AuthUserRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class AuthUserRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
AuthUser AuthUser
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: RepositoryTypes.CreateAuthUserDTO[], data: RepositoryTypes.CreateAuthUserDTO[],
context: Context = {} context: Context = {}
@@ -14,7 +14,7 @@ export const defaultCartsData = [
first_name: "Tony", first_name: "Tony",
last_name: "Stark", last_name: "Stark",
}, },
billing_address_id: { billing_address: {
address_1: "Stark Industries", address_1: "Stark Industries",
city: "New York", city: "New York",
}, },
@@ -68,7 +68,7 @@ describe("Address Service", () => {
.catch((e) => e) .catch((e) => e)
expect(error.message).toContain( expect(error.message).toContain(
"Address with id \"none-existing\" not found" 'Address with id "none-existing" not found'
) )
}) })
@@ -0,0 +1,245 @@
import { ICartModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src/initialize"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
jest.setTimeout(30000)
describe("Cart Module Service", () => {
let service: ICartModuleService
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = await MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_CART_DB_SCHEMA,
},
})
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("create", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.create([
{
email: "test@email.com",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for Cart.currency_code is required, 'undefined' found"
)
})
it("should create a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
})
)
})
it("should create a cart with billing + shipping address successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
billing_address: {
first_name: "John",
last_name: "Doe",
},
shipping_address: {
first_name: "John",
last_name: "Doe",
},
},
])
const [cart] = await service.list(
{ id: [createdCart.id] },
{ relations: ["billing_address", "shipping_address"] }
)
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
billing_address: expect.objectContaining({
first_name: "John",
last_name: "Doe",
}),
shipping_address: expect.objectContaining({
first_name: "John",
last_name: "Doe",
}),
})
)
})
it("should create a cart with billing id + shipping id successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
last_name: "Doe",
},
])
const [createdCart] = await service.create([
{
currency_code: "eur",
billing_address_id: createdAddress.id,
shipping_address_id: createdAddress.id,
},
])
expect(createdCart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
billing_address: expect.objectContaining({
id: createdAddress.id,
first_name: "John",
last_name: "Doe",
}),
shipping_address: expect.objectContaining({
id: createdAddress.id,
first_name: "John",
last_name: "Doe",
}),
})
)
})
})
describe("update", () => {
it("should throw an error if cart does not exist", async () => {
const error = await service
.update([
{
id: "none-existing",
},
])
.catch((e) => e)
expect(error.message).toContain('Cart with id "none-existing" not found')
})
it("should update a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
const [updatedCart] = await service.update([
{
id: createdCart.id,
email: "test@email.com",
},
])
const [cart] = await service.list({ id: [createdCart.id] })
expect(cart).toEqual(
expect.objectContaining({
id: createdCart.id,
currency_code: "eur",
email: updatedCart.email,
})
)
})
})
describe("delete", () => {
it("should delete a cart successfully", async () => {
const [createdCart] = await service.create([
{
currency_code: "eur",
},
])
await service.delete([createdCart.id])
const carts = await service.list({ id: [createdCart.id] })
expect(carts.length).toEqual(0)
})
})
describe("createAddresses", () => {
it("should create an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
const [address] = await service.listAddresses({
id: [createdAddress.id!],
})
expect(address).toEqual(
expect.objectContaining({
id: createdAddress.id,
first_name: "John",
})
)
})
})
describe("updateAddresses", () => {
it("should update an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
const [updatedAddress] = await service.updateAddresses([
{ id: createdAddress.id!, first_name: "Jane" },
])
expect(updatedAddress).toEqual(
expect.objectContaining({
id: createdAddress.id,
first_name: "Jane",
})
)
})
})
describe("deleteAddresses", () => {
it("should delete an address successfully", async () => {
const [createdAddress] = await service.createAddresses([
{
first_name: "John",
},
])
await service.deleteAddresses([createdAddress.id!])
const [address] = await service.listAddresses({
id: [createdAddress.id!],
})
expect(address).toBe(undefined)
})
})
})
@@ -17,7 +17,7 @@ describe("Cart Service", () => {
testManager = await MikroOrmWrapper.forkManager() testManager = await MikroOrmWrapper.forkManager()
const cartRepository = new CartRepository({ const cartRepository = new CartRepository({
manager: repositoryManager, manager: repositoryManager
}) })
service = new CartService({ service = new CartService({
@@ -74,9 +74,7 @@ describe("Cart Service", () => {
]) ])
.catch((e) => e) .catch((e) => e)
expect(error.message).toContain( expect(error.message).toContain('Cart with id "none-existing" not found')
"Cart with id \"none-existing\" not found"
)
}) })
it("should update a cart successfully", async () => { it("should update a cart successfully", async () => {
@@ -87,7 +85,6 @@ describe("Cart Service", () => {
]) ])
const [updatedCart] = await service.update([ const [updatedCart] = await service.update([
{ {
id: createdCart.id, id: createdCart.id,
email: "test@email.com", email: "test@email.com",
+1 -1
View File
@@ -39,6 +39,7 @@
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear"
}, },
"devDependencies": { "devDependencies": {
"@medusajs/types": "workspace:^",
"@mikro-orm/cli": "5.9.7", "@mikro-orm/cli": "5.9.7",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"jest": "^29.6.3", "jest": "^29.6.3",
@@ -51,7 +52,6 @@
}, },
"dependencies": { "dependencies": {
"@medusajs/modules-sdk": "^1.12.5", "@medusajs/modules-sdk": "^1.12.5",
"@medusajs/types": "^1.11.9",
"@medusajs/utils": "^1.11.2", "@medusajs/utils": "^1.11.2",
"@mikro-orm/core": "5.9.7", "@mikro-orm/core": "5.9.7",
"@mikro-orm/migrations": "5.9.7", "@mikro-orm/migrations": "5.9.7",
+5 -1
View File
@@ -10,7 +10,11 @@ import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types" import { InitializeModuleInjectableDependencies } from "../types"
export const initialize = async ( export const initialize = async (
options?: ModulesSdkTypes.ModuleBootstrapDeclaration, options?:
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
| ExternalModuleDeclaration
| InternalModuleDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<ICartModuleService> => { ): Promise<ICartModuleService> => {
const loaded = await MedusaModule.bootstrap<ICartModuleService>({ const loaded = await MedusaModule.bootstrap<ICartModuleService>({
+5 -1
View File
@@ -3,6 +3,7 @@ import * as defaultRepositories from "@repositories"
import { LoaderOptions } from "@medusajs/modules-sdk" import { LoaderOptions } from "@medusajs/modules-sdk"
import { ModulesSdkTypes } from "@medusajs/types" import { ModulesSdkTypes } from "@medusajs/types"
import { loadCustomRepositories } from "@medusajs/utils" import { loadCustomRepositories } from "@medusajs/utils"
import * as defaultServices from "@services"
import { asClass } from "awilix" import { asClass } from "awilix"
export default async ({ export default async ({
@@ -17,7 +18,8 @@ export default async ({
)?.repositories )?.repositories
container.register({ container.register({
// cartService: asClass(defaultServices.CartService).singleton(), cartService: asClass(defaultServices.CartService).singleton(),
addressService: asClass(defaultServices.AddressService).singleton(),
}) })
if (customRepositories) { if (customRepositories) {
@@ -34,5 +36,7 @@ export default async ({
function loadDefaultRepositories({ container }) { function loadDefaultRepositories({ container }) {
container.register({ container.register({
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
cartRepository: asClass(defaultRepositories.CartRepository).singleton(),
addressRepository: asClass(defaultRepositories.AddressRepository).singleton(),
}) })
} }
+20 -12
View File
@@ -5,9 +5,10 @@ import {
Cascade, Cascade,
Collection, Collection,
Entity, Entity,
Index,
ManyToOne,
OnInit, OnInit,
OneToMany, OneToMany,
OneToOne,
OptionalProps, OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
@@ -19,7 +20,7 @@ import ShippingMethod from "./shipping-method"
type OptionalCartProps = type OptionalCartProps =
| "shipping_address" | "shipping_address"
| "billing_address" | "billing_address"
| DAL.EntityDateColumns // TODO: To be revisited when more clear | DAL.EntityDateColumns
@Entity({ tableName: "cart" }) @Entity({ tableName: "cart" })
export default class Cart { export default class Cart {
@@ -47,18 +48,22 @@ export default class Cart {
@Property({ columnType: "text" }) @Property({ columnType: "text" })
currency_code: string currency_code: string
@OneToOne({ @Index({ name: "IDX_cart_shipping_address_id" })
entity: () => Address, @Property({ columnType: "text", nullable: true })
joinColumn: "shipping_address_id", shipping_address_id?: string | null
cascade: [Cascade.REMOVE],
@ManyToOne(() => Address, {
fieldName: "shipping_address_id",
nullable: true, nullable: true,
}) })
shipping_address?: Address | null shipping_address?: Address | null
@OneToOne({ @Index({ name: "IDX_cart_billing_address_id" })
entity: () => Address, @Property({ columnType: "text", nullable: true })
joinColumn: "billing_address_id", billing_address_id?: string | null
cascade: [Cascade.REMOVE],
@ManyToOne(() => Address, {
fieldName: "billing_address_id",
nullable: true, nullable: true,
}) })
billing_address?: Address | null billing_address?: Address | null
@@ -116,7 +121,7 @@ export default class Cart {
columnType: "timestamptz", columnType: "timestamptz",
defaultRaw: "now()", defaultRaw: "now()",
}) })
created_at: Date created_at?: Date
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
@@ -124,7 +129,10 @@ export default class Cart {
columnType: "timestamptz", columnType: "timestamptz",
defaultRaw: "now()", defaultRaw: "now()",
}) })
updated_at: Date updated_at?: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date
@BeforeCreate() @BeforeCreate()
onCreate() { onCreate() {
+1
View File
@@ -6,3 +6,4 @@ export { default as LineItemTaxLine } from "./line-item-tax-line"
export { default as ShippingMethod } from "./shipping-method" export { default as ShippingMethod } from "./shipping-method"
export { default as ShippingMethodAdjustmentLine } from "./shipping-method-adjustment-line" export { default as ShippingMethodAdjustmentLine } from "./shipping-method-adjustment-line"
export { default as ShippingMethodTaxLine } from "./shipping-method-tax-line" export { default as ShippingMethodTaxLine } from "./shipping-method-tax-line"
@@ -10,6 +10,11 @@ export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: CreateAddressDTO create: CreateAddressDTO
} }
>(Address) { >(Address) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async update( async update(
data: { address: Address; update: UpdateAddressDTO }[], data: { address: Address; update: UpdateAddressDTO }[],
context: Context = {} context: Context = {}
+5
View File
@@ -10,6 +10,11 @@ export class CartRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: CreateCartDTO create: CreateCartDTO
} }
>(Cart) { >(Cart) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async update( async update(
data: { cart: Cart; update: UpdateCartDTO }[], data: { cart: Cart; update: UpdateCartDTO }[],
context: Context = {} context: Context = {}
+98 -9
View File
@@ -1,4 +1,6 @@
import { import {
AddressDTO,
CartAddressDTO,
CartDTO, CartDTO,
Context, Context,
CreateCartDTO, CreateCartDTO,
@@ -11,11 +13,13 @@ import {
UpdateCartDTO, UpdateCartDTO,
} from "@medusajs/types" } from "@medusajs/types"
import { FilterableAddressProps } from "@medusajs/types"
import { import {
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
} from "@medusajs/utils" } from "@medusajs/utils"
import { CreateAddressDTO, UpdateAddressDTO } from "@types"
import { joinerConfig } from "../joiner-config" import { joinerConfig } from "../joiner-config"
import AddressService from "./address" import AddressService from "./address"
import CartService from "./cart" import CartService from "./cart"
@@ -32,10 +36,12 @@ export default class CartModuleService implements ICartModuleService {
protected addressService_: AddressService protected addressService_: AddressService
constructor( constructor(
{ baseRepository }: InjectedDependencies, { baseRepository, cartService, addressService }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
this.baseRepository_ = baseRepository this.baseRepository_ = baseRepository
this.cartService_ = cartService
this.addressService_ = addressService
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
@@ -157,15 +163,9 @@ export default class CartModuleService implements ICartModuleService {
return await this.cartService_.update(data, sharedContext) return await this.cartService_.update(data, sharedContext)
} }
async delete( async delete(ids: string[], sharedContext?: Context): Promise<void>
ids: string[],
sharedContext?: Context
): Promise<void>
async delete( async delete(ids: string, sharedContext?: Context): Promise<void>
ids: string,
sharedContext?: Context
): Promise<void>
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async delete( async delete(
@@ -175,4 +175,93 @@ export default class CartModuleService implements ICartModuleService {
const cartIds = Array.isArray(ids) ? ids : [ids] const cartIds = Array.isArray(ids) ? ids : [ids]
await this.cartService_.delete(cartIds, sharedContext) await this.cartService_.delete(cartIds, sharedContext)
} }
@InjectManager("baseRepository_")
async listAddresses(
filters: FilterableAddressProps = {},
config: FindConfig<AddressDTO> = {},
@MedusaContext() sharedContext: Context = {}
) {
const addresses = await this.addressService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<CartAddressDTO[]>(addresses, {
populate: true,
})
}
async createAddresses(data: CreateAddressDTO, sharedContext?: Context)
async createAddresses(data: CreateAddressDTO[], sharedContext?: Context)
@InjectManager("baseRepository_")
async createAddresses(
data: CreateAddressDTO[] | CreateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.createAddresses_(input, sharedContext)
const result = await this.listAddresses(
{ id: addresses.map((p) => p.id) },
{},
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async createAddresses_(
data: CreateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.create(data, sharedContext)
}
async updateAddresses(data: UpdateAddressDTO, sharedContext?: Context)
async updateAddresses(data: UpdateAddressDTO[], sharedContext?: Context)
@InjectManager("baseRepository_")
async updateAddresses(
data: UpdateAddressDTO[] | UpdateAddressDTO,
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const addresses = await this.updateAddresses_(input, sharedContext)
const result = await this.listAddresses(
{ id: addresses.map((p) => p.id) },
{},
sharedContext
)
return (Array.isArray(data) ? result : result[0]) as
| AddressDTO
| AddressDTO[]
}
@InjectTransactionManager("baseRepository_")
protected async updateAddresses_(
data: UpdateAddressDTO[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.addressService_.update(data, sharedContext)
}
async deleteAddresses(ids: string[], sharedContext?: Context)
async deleteAddresses(ids: string, sharedContext?: Context)
@InjectTransactionManager("baseRepository_")
async deleteAddresses(
ids: string[] | string,
@MedusaContext() sharedContext: Context = {}
) {
const addressIds = Array.isArray(ids) ? ids : [ids]
await this.addressService_.delete(addressIds, sharedContext)
}
} }
+1 -1
View File
@@ -20,7 +20,7 @@
"author": "Medusa", "author": "Medusa",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@medusajs/types": "^1.11.8", "@medusajs/types": "^1.11.9",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"jest": "^29.6.3", "jest": "^29.6.3",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
@@ -238,7 +238,7 @@ export interface FulfillmentService extends TransactionBaseService {
* // ... * // ...
* async createFulfillment( * async createFulfillment(
* data: Record<string, unknown>, * data: Record<string, unknown>,
* items: LineItem, * items: LineItem[],
* order: Order, * order: Order,
* fulfillment: Fulfillment * fulfillment: Fulfillment
* ) { * ) {
@@ -9,4 +9,9 @@ export class CurrencyRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: RepositoryTypes.CreateCurrencyDTO create: RepositoryTypes.CreateCurrencyDTO
update: RepositoryTypes.UpdateCurrencyDTO update: RepositoryTypes.UpdateCurrencyDTO
} }
>(Currency, "code") {} >(Currency) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -9,4 +9,9 @@ export class MoneyAmountRepository extends DALUtils.mikroOrmBaseRepositoryFactor
create: RepositoryTypes.CreateMoneyAmountDTO create: RepositoryTypes.CreateMoneyAmountDTO
update: RepositoryTypes.UpdateMoneyAmountDTO update: RepositoryTypes.UpdateMoneyAmountDTO
} }
>(MoneyAmount) {} >(MoneyAmount) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -10,6 +10,11 @@ export class PriceListRuleValueRepository extends DALUtils.mikroOrmBaseRepositor
update: RepositoryTypes.UpdatePriceListRuleValueDTO update: RepositoryTypes.UpdatePriceListRuleValueDTO
} }
>(PriceListRuleValue) { >(PriceListRuleValue) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: RepositoryTypes.CreatePriceListRuleValueDTO[], data: RepositoryTypes.CreatePriceListRuleValueDTO[],
context: Context = {} context: Context = {}
@@ -7,6 +7,11 @@ import { RepositoryTypes } from "@types"
export class PriceListRuleRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class PriceListRuleRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
PriceListRule PriceListRule
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: RepositoryTypes.CreatePriceListRuleDTO[], data: RepositoryTypes.CreatePriceListRuleDTO[],
context: Context = {} context: Context = {}
@@ -7,6 +7,11 @@ import { RepositoryTypes } from "@types"
export class PriceListRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class PriceListRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
PriceList PriceList
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: RepositoryTypes.CreatePriceListDTO[], data: RepositoryTypes.CreatePriceListDTO[],
context: Context = {} context: Context = {}
@@ -10,6 +10,11 @@ export class PriceRuleRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
update: RepositoryTypes.UpdatePriceRuleDTO update: RepositoryTypes.UpdatePriceRuleDTO
} }
>(PriceRule) { >(PriceRule) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: RepositoryTypes.CreatePriceRuleDTO[], data: RepositoryTypes.CreatePriceRuleDTO[],
context: Context = {} context: Context = {}
@@ -9,4 +9,9 @@ export class PriceSetMoneyAmountRulesRepository extends DALUtils.mikroOrmBaseRep
create: RepositoryTypes.CreatePriceSetMoneyAmountRulesDTO create: RepositoryTypes.CreatePriceSetMoneyAmountRulesDTO
update: RepositoryTypes.UpdatePriceSetMoneyAmountRulesDTO update: RepositoryTypes.UpdatePriceSetMoneyAmountRulesDTO
} }
>(PriceSetMoneyAmountRules) {} >(PriceSetMoneyAmountRules) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -9,4 +9,9 @@ export class PriceSetMoneyAmountRepository extends DALUtils.mikroOrmBaseReposito
create: RepositoryTypes.CreatePriceSetMoneyAmountDTO create: RepositoryTypes.CreatePriceSetMoneyAmountDTO
update: RepositoryTypes.UpdatePriceSetMoneyAmountDTO update: RepositoryTypes.UpdatePriceSetMoneyAmountDTO
} }
>(PriceSetMoneyAmount) {} >(PriceSetMoneyAmount) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -9,4 +9,9 @@ export class PriceSetRuleTypeRepository extends DALUtils.mikroOrmBaseRepositoryF
create: RepositoryTypes.CreatePriceSetRuleTypeDTO create: RepositoryTypes.CreatePriceSetRuleTypeDTO
update: RepositoryTypes.UpdatePriceSetRuleTypeDTO update: RepositoryTypes.UpdatePriceSetRuleTypeDTO
} }
>(PriceSetRuleType) {} >(PriceSetRuleType) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -9,4 +9,9 @@ export class PriceSetRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: RepositoryTypes.CreatePriceSetDTO create: RepositoryTypes.CreatePriceSetDTO
update: RepositoryTypes.UpdatePriceSetDTO update: RepositoryTypes.UpdatePriceSetDTO
} }
>(PriceSet) {} >(PriceSet) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -8,4 +8,9 @@ export class RuleTypeRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: RepositoryTypes.CreateRuleTypeDTO create: RepositoryTypes.CreateRuleTypeDTO
update: RepositoryTypes.UpdateRuleTypeDTO update: RepositoryTypes.UpdateRuleTypeDTO
} }
>(RuleType) {} >(RuleType) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -14,6 +14,11 @@ type CreateProductCollection = ProductTypes.CreateProductCollectionDTO & {
export class ProductCollectionRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class ProductCollectionRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
ProductCollection ProductCollection
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: CreateProductCollection[], data: CreateProductCollection[],
context: Context = {} context: Context = {}
@@ -7,6 +7,11 @@ import { DALUtils } from "@medusajs/utils"
export class ProductImageRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class ProductImageRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
Image Image
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async upsert(urls: string[], context: Context = {}): Promise<Image[]> { async upsert(urls: string[], context: Context = {}): Promise<Image[]> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -11,6 +11,11 @@ import { SqlEntityManager } from "@mikro-orm/postgresql"
export class ProductOptionValueRepository extends DALUtils.mikroOrmBaseRepositoryFactory( export class ProductOptionValueRepository extends DALUtils.mikroOrmBaseRepositoryFactory(
ProductOptionValue ProductOptionValue
) { ) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async upsert( async upsert(
optionValues: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[], optionValues: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[],
context: Context = {} context: Context = {}
@@ -10,6 +10,11 @@ export class ProductOptionRepository extends DALUtils.mikroOrmBaseRepositoryFact
update: ProductTypes.UpdateProductOptionDTO update: ProductTypes.UpdateProductOptionDTO
} }
>(ProductOption) { >(ProductOption) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async create( async create(
data: ProductTypes.CreateProductOptionDTO[], data: ProductTypes.CreateProductOptionDTO[],
context: Context = {} context: Context = {}
@@ -15,6 +15,11 @@ export class ProductTagRepository extends DALUtils.mikroOrmBaseRepositoryFactory
update: UpdateProductTagDTO update: UpdateProductTagDTO
} }
>(ProductTag) { >(ProductTag) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async upsert( async upsert(
tags: UpsertProductTagDTO[], tags: UpsertProductTagDTO[],
context: Context = {} context: Context = {}
@@ -14,6 +14,11 @@ export class ProductTypeRepository extends DALUtils.mikroOrmBaseRepositoryFactor
update: UpdateProductTypeDTO update: UpdateProductTypeDTO
} }
>(ProductType) { >(ProductType) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async upsert( async upsert(
types: CreateProductTypeDTO[], types: CreateProductTypeDTO[],
context: Context = {} context: Context = {}
@@ -14,4 +14,9 @@ export class ProductVariantRepository extends DALUtils.mikroOrmBaseRepositoryFac
"id" "id"
> >
} }
>(ProductVariant) {} >(ProductVariant) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -24,6 +24,11 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: WithRequiredProperty<ProductTypes.CreateProductOnlyDTO, "status"> create: WithRequiredProperty<ProductTypes.CreateProductOnlyDTO, "status">
} }
>(Product) { >(Product) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
async find( async find(
findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} }, findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} },
context: Context = {} context: Context = {}
@@ -0,0 +1,32 @@
import { CampaignBudgetType } from "@medusajs/utils"
export const defaultCampaignsData = [
{
id: "campaign-id-1",
name: "campaign 1",
description: "test description",
currency: "USD",
campaign_identifier: "test-1",
starts_at: new Date("01/01/2023"),
ends_at: new Date("01/01/2024"),
budget: {
type: CampaignBudgetType.SPEND,
limit: 1000,
used: 0,
},
},
{
id: "campaign-id-2",
name: "campaign 1",
description: "test description",
currency: "USD",
campaign_identifier: "test-2",
starts_at: new Date("01/01/2023"),
ends_at: new Date("01/01/2024"),
budget: {
type: CampaignBudgetType.USAGE,
limit: 1000,
used: 0,
},
},
]
@@ -0,0 +1,23 @@
import { CreateCampaignDTO } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { Campaign } from "@models"
import { defaultCampaignsData } from "./data"
export * from "./data"
export async function createCampaigns(
manager: SqlEntityManager,
campaignsData: CreateCampaignDTO[] = defaultCampaignsData
): Promise<Campaign[]> {
const campaigns: Campaign[] = []
for (let campaignData of campaignsData) {
let campaign = manager.create(Campaign, campaignData)
manager.persist(campaign)
await manager.flush()
}
return campaigns
}
@@ -0,0 +1,241 @@
import { IPromotionModuleService } from "@medusajs/types"
import { SqlEntityManager } from "@mikro-orm/postgresql"
import { initialize } from "../../../../src"
import { createCampaigns } from "../../../__fixtures__/campaigns"
import { DB_URL, MikroOrmWrapper } from "../../../utils"
jest.setTimeout(30000)
describe("Promotion Module Service: Campaigns", () => {
let service: IPromotionModuleService
let repositoryManager: SqlEntityManager
beforeEach(async () => {
await MikroOrmWrapper.setupDatabase()
repositoryManager = MikroOrmWrapper.forkManager()
service = await initialize({
database: {
clientUrl: DB_URL,
schema: process.env.MEDUSA_PROMOTION_DB_SCHEMA,
},
})
})
afterEach(async () => {
await MikroOrmWrapper.clearDatabase()
})
describe("createCampaigns", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.createCampaigns([
{
name: "test",
} as any,
])
.catch((e) => e)
expect(error.message).toContain(
"Value for Campaign.campaign_identifier is required, 'undefined' found"
)
})
it("should create a basic campaign successfully", async () => {
const startsAt = new Date("01/01/2024")
const endsAt = new Date("01/01/2025")
const [createdCampaign] = await service.createCampaigns([
{
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
},
])
const campaign = await service.retrieveCampaign(createdCampaign.id)
expect(campaign).toEqual(
expect.objectContaining({
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
})
)
})
it("should create a campaign with campaign budget successfully", async () => {
const startsAt = new Date("01/01/2024")
const endsAt = new Date("01/01/2025")
const [createdPromotion] = await service.createCampaigns([
{
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
budget: {
limit: 1000,
type: "usage",
used: 10,
},
},
])
const campaign = await service.retrieveCampaign(createdPromotion.id, {
relations: ["budget"],
})
expect(campaign).toEqual(
expect.objectContaining({
name: "test",
campaign_identifier: "test",
starts_at: startsAt,
ends_at: endsAt,
budget: expect.objectContaining({
limit: 1000,
type: "usage",
used: 10,
}),
})
)
})
})
describe("updateCampaigns", () => {
it("should throw an error when required params are not passed", async () => {
const error = await service
.updateCampaigns([
{
name: "test",
} as any,
])
.catch((e) => e)
expect(error.message).toContain('Campaign with id "" not found')
})
it("should update the attributes of a campaign successfully", async () => {
await createCampaigns(repositoryManager)
const [updatedCampaign] = await service.updateCampaigns([
{
id: "campaign-id-1",
description: "test description 1",
currency: "EUR",
campaign_identifier: "new",
starts_at: new Date("01/01/2024"),
ends_at: new Date("01/01/2025"),
},
])
expect(updatedCampaign).toEqual(
expect.objectContaining({
description: "test description 1",
currency: "EUR",
campaign_identifier: "new",
starts_at: new Date("01/01/2024"),
ends_at: new Date("01/01/2025"),
})
)
})
it("should update the attributes of a campaign budget successfully", async () => {
await createCampaigns(repositoryManager)
const [updatedCampaign] = await service.updateCampaigns([
{
id: "campaign-id-1",
budget: {
limit: 100,
used: 100,
},
},
])
expect(updatedCampaign).toEqual(
expect.objectContaining({
budget: expect.objectContaining({
limit: 100,
used: 100,
}),
})
)
})
})
describe("retrieveCampaign", () => {
beforeEach(async () => {
await createCampaigns(repositoryManager)
})
const id = "campaign-id-1"
it("should return campaign for the given id", async () => {
const campaign = await service.retrieveCampaign(id)
expect(campaign).toEqual(
expect.objectContaining({
id,
})
)
})
it("should throw an error when campaign with id does not exist", async () => {
let error
try {
await service.retrieveCampaign("does-not-exist")
} catch (e) {
error = e
}
expect(error.message).toEqual(
"Campaign with id: does-not-exist was not found"
)
})
it("should throw an error when a id is not provided", async () => {
let error
try {
await service.retrieveCampaign(undefined as unknown as string)
} catch (e) {
error = e
}
expect(error.message).toEqual('"campaignId" must be defined')
})
it("should return campaign based on config select param", async () => {
const campaign = await service.retrieveCampaign(id, {
select: ["id"],
})
const serialized = JSON.parse(JSON.stringify(campaign))
expect(serialized).toEqual({
id,
})
})
})
describe("deleteCampaigns", () => {
beforeEach(async () => {
await createCampaigns(repositoryManager)
})
const id = "campaign-id-1"
it("should delete the campaigns given an id successfully", async () => {
await service.deleteCampaigns([id])
const campaigns = await service.list({
id: [id],
})
expect(campaigns).toHaveLength(0)
})
})
})
@@ -99,63 +99,6 @@ describe("Promotion Service", () => {
) )
}) })
it("should create a promotion with order application method with rules successfully", async () => {
const [createdPromotion] = await service.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "order",
value: "100",
target_rules: [
{
attribute: "product_id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
},
])
const [promotion] = await service.list(
{
id: [createdPromotion.id],
},
{
relations: [
"application_method",
"application_method.target_rules.values",
],
}
)
expect(promotion).toEqual(
expect.objectContaining({
code: "PROMOTION_TEST",
is_automatic: false,
type: "standard",
application_method: expect.objectContaining({
type: "fixed",
target_type: "order",
value: 100,
target_rules: [
expect.objectContaining({
attribute: "product_id",
operator: "eq",
values: expect.arrayContaining([
expect.objectContaining({
value: "prod_tshirt",
}),
]),
}),
],
}),
})
)
})
it("should throw error when creating an item application method without allocation", async () => { it("should throw error when creating an item application method without allocation", async () => {
const error = await service const error = await service
.create([ .create([
@@ -164,7 +107,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
value: "100", value: "100",
}, },
}, },
@@ -172,7 +115,7 @@ describe("Promotion Service", () => {
.catch((e) => e) .catch((e) => e)
expect(error.message).toContain( expect(error.message).toContain(
"application_method.allocation should be either 'across OR each' when application_method.target_type is either 'shipping OR item'" "application_method.allocation should be either 'across OR each' when application_method.target_type is either 'shipping_methods OR items'"
) )
}) })
@@ -185,7 +128,7 @@ describe("Promotion Service", () => {
application_method: { application_method: {
type: "fixed", type: "fixed",
allocation: "each", allocation: "each",
target_type: "shipping", target_type: "shipping_methods",
value: "100", value: "100",
}, },
}, },
@@ -197,6 +140,33 @@ describe("Promotion Service", () => {
) )
}) })
it("should throw error when creating an order application method with rules", async () => {
const error = await service
.create([
{
code: "PROMOTION_TEST",
type: PromotionType.STANDARD,
application_method: {
type: "fixed",
target_type: "order",
value: "100",
target_rules: [
{
attribute: "product_id",
operator: "eq",
values: ["prod_tshirt"],
},
],
},
},
])
.catch((e) => e)
expect(error.message).toContain(
"Target rules for application method with target type (order) is not allowed"
)
})
it("should create a promotion with rules successfully", async () => { it("should create a promotion with rules successfully", async () => {
const [createdPromotion] = await service.create([ const [createdPromotion] = await service.create([
{ {
@@ -390,7 +360,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "across", allocation: "across",
value: "100", value: "100",
}, },
@@ -424,7 +394,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "each", allocation: "each",
value: "100", value: "100",
max_quantity: 500, max_quantity: 500,
@@ -483,7 +453,7 @@ describe("Promotion Service", () => {
.catch((e) => e) .catch((e) => e)
expect(error.message).toContain( expect(error.message).toContain(
`application_method.target_type should be one of order, shipping, item` `application_method.target_type should be one of order, shipping_methods, items`
) )
error = await service error = await service
@@ -604,7 +574,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "each", allocation: "each",
value: "100", value: "100",
max_quantity: 500, max_quantity: 500,
@@ -676,7 +646,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "each", allocation: "each",
value: "100", value: "100",
max_quantity: 500, max_quantity: 500,
@@ -760,7 +730,7 @@ describe("Promotion Service", () => {
], ],
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "each", allocation: "each",
value: "100", value: "100",
max_quantity: 500, max_quantity: 500,
@@ -821,7 +791,7 @@ describe("Promotion Service", () => {
type: PromotionType.STANDARD, type: PromotionType.STANDARD,
application_method: { application_method: {
type: "fixed", type: "fixed",
target_type: "item", target_type: "items",
allocation: "each", allocation: "each",
value: "100", value: "100",
max_quantity: 500, max_quantity: 500,
+2
View File
@@ -3,6 +3,8 @@ module.exports = {
"^@models": "<rootDir>/src/models", "^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services", "^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories", "^@repositories": "<rootDir>/src/repositories",
"^@utils": "<rootDir>/src/utils",
"^@types": "<rootDir>/src/types",
}, },
transform: { transform: {
"^.+\\.[jt]s?$": [ "^.+\\.[jt]s?$": [
@@ -28,6 +28,10 @@ export default async ({
applicationMethodService: asClass( applicationMethodService: asClass(
defaultServices.ApplicationMethodService defaultServices.ApplicationMethodService
).singleton(), ).singleton(),
campaignService: asClass(defaultServices.CampaignService).singleton(),
campaignBudgetService: asClass(
defaultServices.CampaignBudgetService
).singleton(),
}) })
if (customRepositories) { if (customRepositories) {
@@ -56,5 +60,11 @@ function loadDefaultRepositories({ container }) {
promotionRuleValueRepository: asClass( promotionRuleValueRepository: asClass(
defaultRepositories.PromotionRuleValueRepository defaultRepositories.PromotionRuleValueRepository
).singleton(), ).singleton(),
campaignRepository: asClass(
defaultRepositories.CampaignRepository
).singleton(),
campaignBudgetRepository: asClass(
defaultRepositories.CampaignBudgetRepository
).singleton(),
}) })
} }
@@ -2,6 +2,7 @@ import {
ApplicationMethodAllocationValues, ApplicationMethodAllocationValues,
ApplicationMethodTargetTypeValues, ApplicationMethodTargetTypeValues,
ApplicationMethodTypeValues, ApplicationMethodTypeValues,
DAL,
} from "@medusajs/types" } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import { import {
@@ -24,9 +25,8 @@ type OptionalFields =
| "value" | "value"
| "max_quantity" | "max_quantity"
| "allocation" | "allocation"
| "created_at"
| "updated_at"
| "deleted_at" | "deleted_at"
| DAL.EntityDateColumns
@Entity() @Entity()
export default class ApplicationMethod { export default class ApplicationMethod {
@@ -0,0 +1,81 @@
import { CampaignBudgetTypeValues, DAL } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Entity,
Enum,
Index,
OnInit,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
} from "@mikro-orm/core"
import Campaign from "./campaign"
type OptionalFields =
| "description"
| "limit"
| "used"
| "deleted_at"
| DAL.EntityDateColumns
@Entity()
export default class CampaignBudget {
[OptionalProps]?: OptionalFields
@PrimaryKey({ columnType: "text" })
id!: string
@Index({ name: "IDX_campaign_budget_type" })
@Enum(() => PromotionUtils.CampaignBudgetType)
type: CampaignBudgetTypeValues
@OneToOne({
entity: () => Campaign,
})
campaign?: Campaign
@Property({
columnType: "numeric",
nullable: true,
serializer: Number,
default: null,
})
limit: number | null
@Property({
columnType: "numeric",
serializer: Number,
default: 0,
})
used: number
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "probudg")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "probudg")
}
}
+103
View File
@@ -0,0 +1,103 @@
import { DAL } from "@medusajs/types"
import { generateEntityId } from "@medusajs/utils"
import {
BeforeCreate,
Collection,
Entity,
OnInit,
OneToMany,
OneToOne,
OptionalProps,
PrimaryKey,
Property,
Unique,
} from "@mikro-orm/core"
import CampaignBudget from "./campaign-budget"
import Promotion from "./promotion"
type OptionalFields =
| "description"
| "currency"
| "starts_at"
| "ends_at"
| "deleted_at"
| DAL.EntityDateColumns
type OptionalRelations = "budget"
@Entity()
export default class Campaign {
[OptionalProps]?: OptionalFields | OptionalRelations
@PrimaryKey({ columnType: "text" })
id!: string
@Property({ columnType: "text" })
name: string
@Property({ columnType: "text", nullable: true })
description: string | null
@Property({ columnType: "text", nullable: true })
currency: string | null
@Property({ columnType: "text" })
@Unique({
name: "IDX_campaign_identifier_unique",
properties: ["campaign_identifier"],
})
campaign_identifier: string
@Property({
columnType: "timestamptz",
nullable: true,
})
starts_at: Date | null
@Property({
columnType: "timestamptz",
nullable: true,
})
ends_at: Date | null
@OneToOne({
entity: () => CampaignBudget,
mappedBy: (cb) => cb.campaign,
cascade: ["soft-remove"] as any,
nullable: true,
})
budget: CampaignBudget | null
@OneToMany(() => Promotion, (promotion) => promotion.campaign, {
orphanRemoval: true,
})
promotions = new Collection<Promotion>(this)
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Property({ columnType: "timestamptz", nullable: true })
deleted_at: Date | null
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "procamp")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "procamp")
}
}
+2
View File
@@ -1,4 +1,6 @@
export { default as ApplicationMethod } from "./application-method" export { default as ApplicationMethod } from "./application-method"
export { default as Campaign } from "./campaign"
export { default as CampaignBudget } from "./campaign-budget"
export { default as Promotion } from "./promotion" export { default as Promotion } from "./promotion"
export { default as PromotionRule } from "./promotion-rule" export { default as PromotionRule } from "./promotion-rule"
export { default as PromotionRuleValue } from "./promotion-rule-value" export { default as PromotionRuleValue } from "./promotion-rule-value"
@@ -1,4 +1,4 @@
import { PromotionRuleOperatorValues } from "@medusajs/types" import { DAL, PromotionRuleOperatorValues } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
@@ -18,7 +18,7 @@ import ApplicationMethod from "./application-method"
import Promotion from "./promotion" import Promotion from "./promotion"
import PromotionRuleValue from "./promotion-rule-value" import PromotionRuleValue from "./promotion-rule-value"
type OptionalFields = "description" | "created_at" | "updated_at" | "deleted_at" type OptionalFields = "description" | "deleted_at" | DAL.EntityDateColumns
type OptionalRelations = "values" | "promotions" type OptionalRelations = "values" | "promotions"
@Entity() @Entity()
+13 -7
View File
@@ -1,4 +1,4 @@
import { PromotionType } from "@medusajs/types" import { DAL, PromotionType } from "@medusajs/types"
import { PromotionUtils, generateEntityId } from "@medusajs/utils" import { PromotionUtils, generateEntityId } from "@medusajs/utils"
import { import {
BeforeCreate, BeforeCreate,
@@ -7,6 +7,7 @@ import {
Enum, Enum,
Index, Index,
ManyToMany, ManyToMany,
ManyToOne,
OnInit, OnInit,
OneToOne, OneToOne,
OptionalProps, OptionalProps,
@@ -15,14 +16,12 @@ import {
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import ApplicationMethod from "./application-method" import ApplicationMethod from "./application-method"
import Campaign from "./campaign"
import PromotionRule from "./promotion-rule" import PromotionRule from "./promotion-rule"
type OptionalFields = type OptionalFields = "is_automatic" | "deleted_at" | DAL.EntityDateColumns
| "is_automatic" type OptionalRelations = "application_method" | "campaign"
| "created_at"
| "updated_at"
| "deleted_at"
type OptionalRelations = "application_method"
@Entity() @Entity()
export default class Promotion { export default class Promotion {
[OptionalProps]?: OptionalFields | OptionalRelations [OptionalProps]?: OptionalFields | OptionalRelations
@@ -38,6 +37,13 @@ export default class Promotion {
}) })
code: string code: string
@ManyToOne(() => Campaign, {
joinColumn: "campaign",
fieldName: "campaign_id",
nullable: true,
})
campaign: Campaign
@Property({ columnType: "boolean", default: false }) @Property({ columnType: "boolean", default: false })
is_automatic?: boolean = false is_automatic?: boolean = false
@@ -8,4 +8,9 @@ export class ApplicationMethodRepository extends DALUtils.mikroOrmBaseRepository
create: CreateApplicationMethodDTO create: CreateApplicationMethodDTO
update: UpdateApplicationMethodDTO update: UpdateApplicationMethodDTO
} }
>(ApplicationMethod) {} >(ApplicationMethod) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -0,0 +1,11 @@
import { DALUtils } from "@medusajs/utils"
import { CampaignBudget } from "@models"
import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "@types"
export class CampaignBudgetRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
CampaignBudget,
{
create: CreateCampaignBudgetDTO
update: UpdateCampaignBudgetDTO
}
>(CampaignBudget) {}
@@ -0,0 +1,11 @@
import { DALUtils } from "@medusajs/utils"
import { Campaign } from "@models"
import { CreateCampaignDTO, UpdateCampaignDTO } from "@types"
export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
Campaign,
{
create: CreateCampaignDTO
update: UpdateCampaignDTO
}
>(Campaign) {}
@@ -1,5 +1,7 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
export { ApplicationMethodRepository } from "./application-method" export { ApplicationMethodRepository } from "./application-method"
export { CampaignRepository } from "./campaign"
export { CampaignBudgetRepository } from "./campaign-budget"
export { PromotionRepository } from "./promotion" export { PromotionRepository } from "./promotion"
export { PromotionRuleRepository } from "./promotion-rule" export { PromotionRuleRepository } from "./promotion-rule"
export { PromotionRuleValueRepository } from "./promotion-rule-value" export { PromotionRuleValueRepository } from "./promotion-rule-value"
@@ -11,4 +11,9 @@ export class PromotionRuleValueRepository extends DALUtils.mikroOrmBaseRepositor
create: CreatePromotionRuleValueDTO create: CreatePromotionRuleValueDTO
update: UpdatePromotionRuleValueDTO update: UpdatePromotionRuleValueDTO
} }
>(PromotionRuleValue) {} >(PromotionRuleValue) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -8,4 +8,9 @@ export class PromotionRuleRepository extends DALUtils.mikroOrmBaseRepositoryFact
create: CreatePromotionRuleDTO create: CreatePromotionRuleDTO
update: UpdatePromotionRuleDTO update: UpdatePromotionRuleDTO
} }
>(PromotionRule) {} >(PromotionRule) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -8,4 +8,9 @@ export class PromotionRepository extends DALUtils.mikroOrmBaseRepositoryFactory<
create: CreatePromotionDTO create: CreatePromotionDTO
Update: UpdatePromotionDTO Update: UpdatePromotionDTO
} }
>(Promotion) {} >(Promotion) {
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
}
@@ -0,0 +1,105 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { CampaignBudget } from "@models"
import { CampaignBudgetRepository } from "@repositories"
import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "../types"
type InjectedDependencies = {
campaignBudgetRepository: DAL.RepositoryService
}
export default class CampaignBudgetService<
TEntity extends CampaignBudget = CampaignBudget
> {
protected readonly campaignBudgetRepository_: DAL.RepositoryService
constructor({ campaignBudgetRepository }: InjectedDependencies) {
this.campaignBudgetRepository_ = campaignBudgetRepository
}
@InjectManager("campaignBudgetRepository_")
async retrieve(
campaignBudgetId: string,
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<
CampaignBudget,
PromotionTypes.CampaignBudgetDTO
>({
id: campaignBudgetId,
entityName: CampaignBudget.name,
repository: this.campaignBudgetRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("campaignBudgetRepository_")
async list(
filters: PromotionTypes.FilterableCampaignBudgetProps = {},
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<CampaignBudget>(
filters,
config
)
return (await this.campaignBudgetRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("campaignBudgetRepository_")
async listAndCount(
filters: PromotionTypes.FilterableCampaignBudgetProps = {},
config: FindConfig<PromotionTypes.CampaignBudgetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<CampaignBudget>(
filters,
config
)
return (await this.campaignBudgetRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("campaignBudgetRepository_")
async create(
data: CreateCampaignBudgetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.campaignBudgetRepository_ as CampaignBudgetRepository
).create(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("campaignBudgetRepository_")
async update(
data: UpdateCampaignBudgetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (
this.campaignBudgetRepository_ as CampaignBudgetRepository
).update(data, sharedContext)) as TEntity[]
}
@InjectTransactionManager("campaignBudgetRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.campaignBudgetRepository_.delete(ids, sharedContext)
}
}
@@ -0,0 +1,96 @@
import { Context, DAL, FindConfig, PromotionTypes } from "@medusajs/types"
import {
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
retrieveEntity,
} from "@medusajs/utils"
import { Campaign } from "@models"
import { CampaignRepository } from "@repositories"
import { CreateCampaignDTO, UpdateCampaignDTO } from "../types"
type InjectedDependencies = {
campaignRepository: DAL.RepositoryService
}
export default class CampaignService<TEntity extends Campaign = Campaign> {
protected readonly campaignRepository_: DAL.RepositoryService
constructor({ campaignRepository }: InjectedDependencies) {
this.campaignRepository_ = campaignRepository
}
@InjectManager("campaignRepository_")
async retrieve(
campaignId: string,
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity> {
return (await retrieveEntity<Campaign, PromotionTypes.CampaignDTO>({
id: campaignId,
entityName: Campaign.name,
repository: this.campaignRepository_,
config,
sharedContext,
})) as TEntity
}
@InjectManager("campaignRepository_")
async list(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
const queryOptions = ModulesSdkUtils.buildQuery<Campaign>(filters, config)
return (await this.campaignRepository_.find(
queryOptions,
sharedContext
)) as TEntity[]
}
@InjectManager("campaignRepository_")
async listAndCount(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[TEntity[], number]> {
const queryOptions = ModulesSdkUtils.buildQuery<Campaign>(filters, config)
return (await this.campaignRepository_.findAndCount(
queryOptions,
sharedContext
)) as [TEntity[], number]
}
@InjectTransactionManager("campaignRepository_")
async create(
data: CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.campaignRepository_ as CampaignRepository).create(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("campaignRepository_")
async update(
data: UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TEntity[]> {
return (await (this.campaignRepository_ as CampaignRepository).update(
data,
sharedContext
)) as TEntity[]
}
@InjectTransactionManager("campaignRepository_")
async delete(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
await this.campaignRepository_.delete(ids, sharedContext)
}
}
+2
View File
@@ -1,4 +1,6 @@
export { default as ApplicationMethodService } from "./application-method" export { default as ApplicationMethodService } from "./application-method"
export { default as CampaignService } from "./campaign"
export { default as CampaignBudgetService } from "./campaign-budget"
export { default as PromotionService } from "./promotion" export { default as PromotionService } from "./promotion"
export { default as PromotionModuleService } from "./promotion-module" export { default as PromotionModuleService } from "./promotion-module"
export { default as PromotionRuleService } from "./promotion-rule" export { default as PromotionRuleService } from "./promotion-rule"
@@ -7,30 +7,41 @@ import {
PromotionTypes, PromotionTypes,
} from "@medusajs/types" } from "@medusajs/types"
import { import {
ApplicationMethodTargetType,
InjectManager, InjectManager,
InjectTransactionManager, InjectTransactionManager,
MedusaContext, MedusaContext,
MedusaError, MedusaError,
isString,
} from "@medusajs/utils" } from "@medusajs/utils"
import { ApplicationMethod, Promotion } from "@models" import { ApplicationMethod, Promotion } from "@models"
import { import {
ApplicationMethodService, ApplicationMethodService,
CampaignBudgetService,
CampaignService,
PromotionRuleService, PromotionRuleService,
PromotionRuleValueService, PromotionRuleValueService,
PromotionService, PromotionService,
} from "@services" } from "@services"
import { joinerConfig } from "../joiner-config"
import { import {
CreateApplicationMethodDTO, CreateApplicationMethodDTO,
CreateCampaignBudgetDTO,
CreateCampaignDTO,
CreatePromotionDTO, CreatePromotionDTO,
CreatePromotionRuleDTO,
UpdateApplicationMethodDTO, UpdateApplicationMethodDTO,
UpdateCampaignBudgetDTO,
UpdateCampaignDTO,
UpdatePromotionDTO, UpdatePromotionDTO,
} from "../types" } from "@types"
import { import {
ComputeActionUtils,
allowedAllocationForQuantity, allowedAllocationForQuantity,
areRulesValidForContext,
validateApplicationMethodAttributes, validateApplicationMethodAttributes,
validatePromotionRuleAttributes, validatePromotionRuleAttributes,
} from "../utils" } from "@utils"
import { joinerConfig } from "../joiner-config"
type InjectedDependencies = { type InjectedDependencies = {
baseRepository: DAL.RepositoryService baseRepository: DAL.RepositoryService
@@ -38,6 +49,8 @@ type InjectedDependencies = {
applicationMethodService: ApplicationMethodService applicationMethodService: ApplicationMethodService
promotionRuleService: PromotionRuleService promotionRuleService: PromotionRuleService
promotionRuleValueService: PromotionRuleValueService promotionRuleValueService: PromotionRuleValueService
campaignService: CampaignService
campaignBudgetService: CampaignBudgetService
} }
export default class PromotionModuleService< export default class PromotionModuleService<
@@ -49,6 +62,8 @@ export default class PromotionModuleService<
protected applicationMethodService_: ApplicationMethodService protected applicationMethodService_: ApplicationMethodService
protected promotionRuleService_: PromotionRuleService protected promotionRuleService_: PromotionRuleService
protected promotionRuleValueService_: PromotionRuleValueService protected promotionRuleValueService_: PromotionRuleValueService
protected campaignService_: CampaignService
protected campaignBudgetService_: CampaignBudgetService
constructor( constructor(
{ {
@@ -57,6 +72,8 @@ export default class PromotionModuleService<
applicationMethodService, applicationMethodService,
promotionRuleService, promotionRuleService,
promotionRuleValueService, promotionRuleValueService,
campaignService,
campaignBudgetService,
}: InjectedDependencies, }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration protected readonly moduleDeclaration: InternalModuleDeclaration
) { ) {
@@ -65,12 +82,169 @@ export default class PromotionModuleService<
this.applicationMethodService_ = applicationMethodService this.applicationMethodService_ = applicationMethodService
this.promotionRuleService_ = promotionRuleService this.promotionRuleService_ = promotionRuleService
this.promotionRuleValueService_ = promotionRuleValueService this.promotionRuleValueService_ = promotionRuleValueService
this.campaignService_ = campaignService
this.campaignBudgetService_ = campaignBudgetService
} }
__joinerConfig(): ModuleJoinerConfig { __joinerConfig(): ModuleJoinerConfig {
return joinerConfig return joinerConfig
} }
async computeActions(
promotionCodesToApply: string[],
applicationContext: PromotionTypes.ComputeActionContext,
// TODO: specify correct type with options
options: Record<string, any> = {}
): Promise<PromotionTypes.ComputeActions[]> {
const computedActions: PromotionTypes.ComputeActions[] = []
const { items = [], shipping_methods: shippingMethods = [] } =
applicationContext
const appliedItemCodes: string[] = []
const appliedShippingCodes: string[] = []
const codeAdjustmentMap = new Map<
string,
PromotionTypes.ComputeActionAdjustmentLine
>()
const methodIdPromoValueMap = new Map<string, number>()
items.forEach((item) => {
item.adjustments?.forEach((adjustment) => {
if (isString(adjustment.code)) {
codeAdjustmentMap.set(adjustment.code, adjustment)
appliedItemCodes.push(adjustment.code)
}
})
})
shippingMethods.forEach((shippingMethod) => {
shippingMethod.adjustments?.forEach((adjustment) => {
if (isString(adjustment.code)) {
codeAdjustmentMap.set(adjustment.code, adjustment)
appliedShippingCodes.push(adjustment.code)
}
})
})
const promotions = await this.list(
{
code: [
...promotionCodesToApply,
...appliedItemCodes,
...appliedShippingCodes,
],
},
{
relations: [
"application_method",
"application_method.target_rules",
"application_method.target_rules.values",
"rules",
"rules.values",
],
}
)
const existingPromotionsMap = new Map<string, PromotionTypes.PromotionDTO>(
promotions.map((promotion) => [promotion.code!, promotion])
)
for (const appliedCode of [...appliedShippingCodes, ...appliedItemCodes]) {
const promotion = existingPromotionsMap.get(appliedCode)
if (!promotion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Applied Promotion for code (${appliedCode}) not found`
)
}
if (promotionCodesToApply.includes(appliedCode)) {
continue
}
if (appliedItemCodes.includes(appliedCode)) {
computedActions.push({
action: "removeItemAdjustment",
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
})
}
if (appliedShippingCodes.includes(appliedCode)) {
computedActions.push({
action: "removeShippingMethodAdjustment",
adjustment_id: codeAdjustmentMap.get(appliedCode)!.id,
})
}
}
for (const promotionCode of promotionCodesToApply) {
const promotion = existingPromotionsMap.get(promotionCode)
if (!promotion) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Promotion for code (${promotionCode}) not found`
)
}
const {
application_method: applicationMethod,
rules: promotionRules = [],
} = promotion
if (!applicationMethod) {
continue
}
const isPromotionApplicable = areRulesValidForContext(
promotionRules,
applicationContext
)
if (!isPromotionApplicable) {
continue
}
if (applicationMethod.target_type === ApplicationMethodTargetType.ORDER) {
const computedActionsForItems =
ComputeActionUtils.getComputedActionsForOrder(
promotion,
applicationContext,
methodIdPromoValueMap
)
computedActions.push(...computedActionsForItems)
}
if (applicationMethod.target_type === ApplicationMethodTargetType.ITEMS) {
const computedActionsForItems =
ComputeActionUtils.getComputedActionsForItems(
promotion,
applicationContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap
)
computedActions.push(...computedActionsForItems)
}
if (
applicationMethod.target_type ===
ApplicationMethodTargetType.SHIPPING_METHODS
) {
const computedActionsForShippingMethods =
ComputeActionUtils.getComputedActionsForShippingMethods(
promotion,
applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap
)
computedActions.push(...computedActionsForShippingMethods)
}
}
return computedActions
}
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
async retrieve( async retrieve(
id: string, id: string,
@@ -111,15 +285,28 @@ export default class PromotionModuleService<
) )
} }
@InjectManager("baseRepository_") async create(
data: PromotionTypes.CreatePromotionDTO,
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO>
async create( async create(
data: PromotionTypes.CreatePromotionDTO[], data: PromotionTypes.CreatePromotionDTO[],
@MedusaContext() sharedContext: Context = {} sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO[]> { ): Promise<PromotionTypes.PromotionDTO[]>
const promotions = await this.create_(data, sharedContext)
return await this.list( @InjectManager("baseRepository_")
{ id: promotions.map((p) => p!.id) }, async create(
data:
| PromotionTypes.CreatePromotionDTO
| PromotionTypes.CreatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO | PromotionTypes.PromotionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const createdPromotions = await this.create_(input, sharedContext)
const promotions = await this.list(
{ id: createdPromotions.map((p) => p!.id) },
{ {
relations: [ relations: [
"application_method", "application_method",
@@ -131,6 +318,8 @@ export default class PromotionModuleService<
}, },
sharedContext sharedContext
) )
return Array.isArray(data) ? promotions : promotions[0]
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -194,6 +383,17 @@ export default class PromotionModuleService<
promotion, promotion,
} }
if (
applicationMethodData.target_type ===
ApplicationMethodTargetType.ORDER &&
targetRulesData.length
) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Target rules for application method with target type (${ApplicationMethodTargetType.ORDER}) is not allowed`
)
}
validateApplicationMethodAttributes(applicationMethodData) validateApplicationMethodAttributes(applicationMethodData)
applicationMethodsData.push(applicationMethodData) applicationMethodsData.push(applicationMethodData)
@@ -228,15 +428,28 @@ export default class PromotionModuleService<
return createdPromotions return createdPromotions
} }
@InjectManager("baseRepository_") async update(
data: PromotionTypes.UpdatePromotionDTO,
sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO>
async update( async update(
data: PromotionTypes.UpdatePromotionDTO[], data: PromotionTypes.UpdatePromotionDTO[],
@MedusaContext() sharedContext: Context = {} sharedContext?: Context
): Promise<PromotionTypes.PromotionDTO[]> { ): Promise<PromotionTypes.PromotionDTO[]>
const promotions = await this.update_(data, sharedContext)
return await this.list( @InjectManager("baseRepository_")
{ id: promotions.map((p) => p!.id) }, async update(
data:
| PromotionTypes.UpdatePromotionDTO
| PromotionTypes.UpdatePromotionDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.PromotionDTO | PromotionTypes.PromotionDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedPromotions = await this.update_(input, sharedContext)
const promotions = await this.list(
{ id: updatedPromotions.map((p) => p!.id) },
{ {
relations: [ relations: [
"application_method", "application_method",
@@ -247,6 +460,8 @@ export default class PromotionModuleService<
}, },
sharedContext sharedContext
) )
return Array.isArray(data) ? promotions : promotions[0]
} }
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
@@ -394,7 +609,7 @@ export default class PromotionModuleService<
for (const ruleData of rulesData) { for (const ruleData of rulesData) {
const { values, ...rest } = ruleData const { values, ...rest } = ruleData
const promotionRuleData = { const promotionRuleData: CreatePromotionRuleDTO = {
...rest, ...rest,
[relationName]: [relation], [relationName]: [relation],
} }
@@ -416,10 +631,12 @@ export default class PromotionModuleService<
@InjectTransactionManager("baseRepository_") @InjectTransactionManager("baseRepository_")
async delete( async delete(
ids: string[], ids: string | string[],
@MedusaContext() sharedContext: Context = {} @MedusaContext() sharedContext: Context = {}
): Promise<void> { ): Promise<void> {
await this.promotionService_.delete(ids, sharedContext) const promotionIds = Array.isArray(ids) ? ids : [ids]
await this.promotionService_.delete(promotionIds, sharedContext)
} }
@InjectManager("baseRepository_") @InjectManager("baseRepository_")
@@ -520,4 +737,214 @@ export default class PromotionModuleService<
sharedContext sharedContext
) )
} }
@InjectManager("baseRepository_")
async retrieveCampaign(
id: string,
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO> {
const campaign = await this.campaignService_.retrieve(
id,
config,
sharedContext
)
return await this.baseRepository_.serialize<PromotionTypes.CampaignDTO>(
campaign,
{
populate: true,
}
)
}
@InjectManager("baseRepository_")
async listCampaigns(
filters: PromotionTypes.FilterableCampaignProps = {},
config: FindConfig<PromotionTypes.CampaignDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO[]> {
const campaigns = await this.campaignService_.list(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<PromotionTypes.CampaignDTO[]>(
campaigns,
{
populate: true,
}
)
}
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO,
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO>
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO[],
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager("baseRepository_")
async createCampaigns(
data: PromotionTypes.CreateCampaignDTO | PromotionTypes.CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO | PromotionTypes.CampaignDTO[]> {
const input = Array.isArray(data) ? data : [data]
const createdCampaigns = await this.createCampaigns_(input, sharedContext)
const campaigns = await this.listCampaigns(
{ id: createdCampaigns.map((p) => p!.id) },
{
relations: ["budget"],
},
sharedContext
)
return Array.isArray(data) ? campaigns : campaigns[0]
}
@InjectTransactionManager("baseRepository_")
protected async createCampaigns_(
data: PromotionTypes.CreateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const campaignsData: CreateCampaignDTO[] = []
const campaignBudgetsData: CreateCampaignBudgetDTO[] = []
const campaignIdentifierBudgetMap = new Map<
string,
CreateCampaignBudgetDTO
>()
for (const createCampaignData of data) {
const { budget: campaignBudgetData, ...campaignData } = createCampaignData
if (campaignBudgetData) {
campaignIdentifierBudgetMap.set(
campaignData.campaign_identifier,
campaignBudgetData
)
}
campaignsData.push(campaignData)
}
const createdCampaigns = await this.campaignService_.create(
campaignsData,
sharedContext
)
for (const createdCampaign of createdCampaigns) {
const campaignBudgetData = campaignIdentifierBudgetMap.get(
createdCampaign.campaign_identifier
)
if (campaignBudgetData) {
campaignBudgetsData.push({
...campaignBudgetData,
campaign: createdCampaign.id,
})
}
}
if (campaignBudgetsData.length) {
await this.campaignBudgetService_.create(
campaignBudgetsData,
sharedContext
)
}
return createdCampaigns
}
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO,
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO>
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO[],
sharedContext?: Context
): Promise<PromotionTypes.CampaignDTO[]>
@InjectManager("baseRepository_")
async updateCampaigns(
data: PromotionTypes.UpdateCampaignDTO | PromotionTypes.UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PromotionTypes.CampaignDTO | PromotionTypes.CampaignDTO[]> {
const input = Array.isArray(data) ? data : [data]
const updatedCampaigns = await this.updateCampaigns_(input, sharedContext)
const campaigns = await this.listCampaigns(
{ id: updatedCampaigns.map((p) => p!.id) },
{
relations: ["budget"],
},
sharedContext
)
return Array.isArray(data) ? campaigns : campaigns[0]
}
@InjectTransactionManager("baseRepository_")
protected async updateCampaigns_(
data: PromotionTypes.UpdateCampaignDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const campaignIds = data.map((d) => d.id)
const campaignsData: UpdateCampaignDTO[] = []
const campaignBudgetsData: UpdateCampaignBudgetDTO[] = []
const existingCampaigns = await this.listCampaigns(
{
id: campaignIds,
},
{
relations: ["budget"],
},
sharedContext
)
const existingCampaignsMap = new Map<string, PromotionTypes.CampaignDTO>(
existingCampaigns.map((campaign) => [campaign.id, campaign])
)
for (const updateCampaignData of data) {
const { budget: campaignBudgetData, ...campaignData } = updateCampaignData
const existingCampaign = existingCampaignsMap.get(campaignData.id)
const existingCampaignBudget = existingCampaign?.budget
campaignsData.push(campaignData)
if (existingCampaignBudget && campaignBudgetData) {
campaignBudgetsData.push({
id: existingCampaignBudget.id,
...campaignBudgetData,
})
}
}
const updatedCampaigns = await this.campaignService_.update(campaignsData)
if (campaignBudgetsData.length) {
await this.campaignBudgetService_.update(campaignBudgetsData)
}
return updatedCampaigns
}
@InjectTransactionManager("baseRepository_")
async deleteCampaigns(
ids: string | string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
const campaignIds = Array.isArray(ids) ? ids : [ids]
await this.promotionService_.delete(campaignIds, sharedContext)
}
} }
@@ -0,0 +1,16 @@
import { CampaignBudgetTypeValues } from "@medusajs/types"
import { Campaign } from "@models"
export interface CreateCampaignBudgetDTO {
type: CampaignBudgetTypeValues
limit: number | null
used?: number
campaign?: Campaign | string
}
export interface UpdateCampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: number | null
used?: number
}
+18
View File
@@ -0,0 +1,18 @@
export interface CreateCampaignDTO {
name: string
description?: string
currency?: string
campaign_identifier: string
starts_at: Date
ends_at: Date
}
export interface UpdateCampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
}
+2
View File
@@ -5,6 +5,8 @@ export type InitializeModuleInjectableDependencies = {
} }
export * from "./application-method" export * from "./application-method"
export * from "./campaign"
export * from "./campaign-budget"
export * from "./promotion" export * from "./promotion"
export * from "./promotion-rule" export * from "./promotion-rule"
export * from "./promotion-rule-value" export * from "./promotion-rule-value"
@@ -1,12 +1,13 @@
import { PromotionRuleDTO } from "@medusajs/types" import { PromotionRuleDTO } from "@medusajs/types"
import { PromotionRule } from "@models"
export interface CreatePromotionRuleValueDTO { export interface CreatePromotionRuleValueDTO {
value: any value: any
promotion_rule: string | PromotionRuleDTO promotion_rule: string | PromotionRuleDTO | PromotionRule
} }
export interface UpdatePromotionRuleValueDTO { export interface UpdatePromotionRuleValueDTO {
id: string id: string
value: any value: any
promotion_rule: string | PromotionRuleDTO promotion_rule: string | PromotionRuleDTO | PromotionRule
} }
@@ -1,7 +1,7 @@
import { PromotionRuleOperatorValues } from "@medusajs/types" import { PromotionRuleOperatorValues } from "@medusajs/types"
export interface CreatePromotionRuleDTO { export interface CreatePromotionRuleDTO {
description?: string description?: string | null
attribute: string attribute: string
operator: PromotionRuleOperatorValues operator: PromotionRuleOperatorValues
} }
@@ -0,0 +1,3 @@
export * from "./items"
export * from "./order"
export * from "./shipping-methods"
@@ -0,0 +1,131 @@
import {
ApplicationMethodAllocationValues,
PromotionTypes,
} from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
MedusaError,
} from "@medusajs/utils"
import { areRulesValidForContext } from "../validations"
export function getComputedActionsForItems(
promotion: PromotionTypes.PromotionDTO,
itemApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues
): PromotionTypes.ComputeActions[] {
const applicableItems: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS] =
[]
if (!itemApplicationContext) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`"items" should be present as an array in the context for computeActions`
)
}
for (const itemContext of itemApplicationContext) {
const isPromotionApplicableToItem = areRulesValidForContext(
promotion?.application_method?.target_rules!,
itemContext
)
if (!isPromotionApplicableToItem) {
continue
}
applicableItems.push(itemContext)
}
return applyPromotionToItems(
promotion,
applicableItems,
methodIdPromoValueMap,
allocationOverride
)
}
export function applyPromotionToItems(
promotion: PromotionTypes.PromotionDTO,
items: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap: Map<string, number>,
allocationOverride?: ApplicationMethodAllocationValues
): PromotionTypes.ComputeActions[] {
const { application_method: applicationMethod } = promotion
const allocation = applicationMethod?.allocation!
const computedActions: PromotionTypes.ComputeActions[] = []
if (
[allocation, allocationOverride].includes(ApplicationMethodAllocation.EACH)
) {
for (const method of items!) {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal =
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
const amount = Math.min(promotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addItemAdjustment",
item_id: method.id,
amount,
code: promotion.code!,
})
}
}
if (
[allocation, allocationOverride].includes(
ApplicationMethodAllocation.ACROSS
)
) {
const totalApplicableValue = items!.reduce((acc, method) => {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
return (
acc +
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
)
}, 0)
for (const method of items!) {
const promotionValue = parseFloat(applicationMethod!.value!)
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const applicableTotal =
method.unit_price *
Math.min(method.quantity, applicationMethod?.max_quantity!) -
appliedPromoValue
// TODO: should we worry about precision here?
const applicablePromotionValue =
(applicableTotal / totalApplicableValue) * promotionValue
const amount = Math.min(applicablePromotionValue, applicableTotal)
if (amount <= 0) {
continue
}
computedActions.push({
action: "addItemAdjustment",
item_id: method.id,
amount,
code: promotion.code!,
})
}
}
return computedActions
}
@@ -0,0 +1,19 @@
import { PromotionTypes } from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
} from "@medusajs/utils"
import { getComputedActionsForItems } from "./items"
export function getComputedActionsForOrder(
promotion: PromotionTypes.PromotionDTO,
itemApplicationContext: PromotionTypes.ComputeActionContext,
methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] {
return getComputedActionsForItems(
promotion,
itemApplicationContext[ApplicationMethodTargetType.ITEMS],
methodIdPromoValueMap,
ApplicationMethodAllocation.ACROSS
)
}
@@ -0,0 +1,114 @@
import { PromotionTypes } from "@medusajs/types"
import {
ApplicationMethodAllocation,
ApplicationMethodTargetType,
MedusaError,
} from "@medusajs/utils"
import { areRulesValidForContext } from "../validations"
export function getComputedActionsForShippingMethods(
promotion: PromotionTypes.PromotionDTO,
shippingMethodApplicationContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] {
const applicableShippingItems: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS] =
[]
if (!shippingMethodApplicationContext) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`"shipping_methods" should be present as an array in the context for computeActions`
)
}
for (const shippingMethodContext of shippingMethodApplicationContext) {
const isPromotionApplicableToItem = areRulesValidForContext(
promotion.application_method?.target_rules!,
shippingMethodContext
)
if (!isPromotionApplicableToItem) {
continue
}
applicableShippingItems.push(shippingMethodContext)
}
return applyPromotionToShippingMethods(
promotion,
applicableShippingItems,
methodIdPromoValueMap
)
}
export function applyPromotionToShippingMethods(
promotion: PromotionTypes.PromotionDTO,
shippingMethods: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.SHIPPING_METHODS],
methodIdPromoValueMap: Map<string, number>
): PromotionTypes.ComputeActions[] {
const { application_method: applicationMethod } = promotion
const allocation = applicationMethod?.allocation!
const computedActions: PromotionTypes.ComputeActions[] = []
if (allocation === ApplicationMethodAllocation.EACH) {
for (const method of shippingMethods!) {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal = method.unit_price - appliedPromoValue
const amount = Math.min(promotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addShippingMethodAdjustment",
shipping_method_id: method.id,
amount,
code: promotion.code!,
})
}
}
if (allocation === ApplicationMethodAllocation.ACROSS) {
const totalApplicableValue = shippingMethods!.reduce((acc, method) => {
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
return acc + method.unit_price - appliedPromoValue
}, 0)
if (totalApplicableValue <= 0) {
return computedActions
}
for (const method of shippingMethods!) {
const promotionValue = parseFloat(applicationMethod!.value!)
const applicableTotal = method.unit_price
const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0
// TODO: should we worry about precision here?
const applicablePromotionValue =
(applicableTotal / totalApplicableValue) * promotionValue -
appliedPromoValue
const amount = Math.min(applicablePromotionValue, applicableTotal)
if (amount <= 0) {
continue
}
methodIdPromoValueMap.set(method.id, appliedPromoValue + amount)
computedActions.push({
action: "addShippingMethodAdjustment",
shipping_method_id: method.id,
amount,
code: promotion.code!,
})
}
}
return computedActions
}
+1
View File
@@ -1 +1,2 @@
export * as ComputeActionUtils from "./compute-actions"
export * from "./validations" export * from "./validations"
@@ -12,8 +12,8 @@ import {
} from "@medusajs/utils" } from "@medusajs/utils"
export const allowedAllocationTargetTypes: string[] = [ export const allowedAllocationTargetTypes: string[] = [
ApplicationMethodTargetType.SHIPPING, ApplicationMethodTargetType.SHIPPING_METHODS,
ApplicationMethodTargetType.ITEM, ApplicationMethodTargetType.ITEMS,
] ]
export const allowedAllocationTypes: string[] = [ export const allowedAllocationTypes: string[] = [
@@ -1,5 +1,11 @@
import { PromotionRuleOperatorValues } from "@medusajs/types" import { PromotionRuleDTO, PromotionRuleOperatorValues } from "@medusajs/types"
import { MedusaError, PromotionRuleOperator, isPresent } from "@medusajs/utils" import {
MedusaError,
PromotionRuleOperator,
isPresent,
isString,
pickValueFromObject,
} from "@medusajs/utils"
import { CreatePromotionRuleDTO } from "../../types" import { CreatePromotionRuleDTO } from "../../types"
export function validatePromotionRuleAttributes( export function validatePromotionRuleAttributes(
@@ -37,3 +43,62 @@ export function validatePromotionRuleAttributes(
throw new MedusaError(MedusaError.Types.INVALID_DATA, errors.join(", ")) throw new MedusaError(MedusaError.Types.INVALID_DATA, errors.join(", "))
} }
export function areRulesValidForContext(
rules: PromotionRuleDTO[],
context: Record<string, any>
): boolean {
return rules.every((rule) => {
const validRuleValues = rule.values?.map((ruleValue) => ruleValue.value)
if (!rule.attribute) {
return false
}
const valuesToCheck = pickValueFromObject(rule.attribute, context)
return evaluateRuleValueCondition(
validRuleValues.filter(isString),
rule.operator!,
valuesToCheck
)
})
}
export function evaluateRuleValueCondition(
ruleValues: string[],
operator: string,
ruleValuesToCheck: string[] | string
) {
if (!Array.isArray(ruleValuesToCheck)) {
ruleValuesToCheck = [ruleValuesToCheck]
}
return ruleValuesToCheck.every((ruleValueToCheck: string) => {
if (operator === "in" || operator === "eq") {
return ruleValues.some((ruleValue) => ruleValue === ruleValueToCheck)
}
if (operator === "ne") {
return ruleValues.some((ruleValue) => ruleValue !== ruleValueToCheck)
}
if (operator === "gt") {
return ruleValues.some((ruleValue) => ruleValue > ruleValueToCheck)
}
if (operator === "gte") {
return ruleValues.some((ruleValue) => ruleValue >= ruleValueToCheck)
}
if (operator === "lt") {
return ruleValues.some((ruleValue) => ruleValue < ruleValueToCheck)
}
if (operator === "lte") {
return ruleValues.some((ruleValue) => ruleValue <= ruleValueToCheck)
}
return false
})
}
+1 -1
View File
@@ -20,7 +20,7 @@
"author": "Medusa", "author": "Medusa",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@medusajs/types": "^1.11.8", "@medusajs/types": "^1.11.9",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"jest": "^29.6.3", "jest": "^29.6.3",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
+6
View File
@@ -27,6 +27,9 @@ export interface CreateCartDTO {
email?: string email?: string
currency_code: string currency_code: string
shipping_address_id?: string
billing_address_id?: string
shipping_address?: CreateAddressDTO | UpdateAddressDTO shipping_address?: CreateAddressDTO | UpdateAddressDTO
billing_address?: CreateAddressDTO | UpdateAddressDTO billing_address?: CreateAddressDTO | UpdateAddressDTO
@@ -42,6 +45,9 @@ export interface UpdateCartDTO {
email?: string email?: string
currency_code?: string currency_code?: string
shipping_address_id?: string
billing_address_id?: string
billing_address?: CreateAddressDTO | UpdateAddressDTO billing_address?: CreateAddressDTO | UpdateAddressDTO
shipping_address?: CreateAddressDTO | UpdateAddressDTO shipping_address?: CreateAddressDTO | UpdateAddressDTO
+35 -2
View File
@@ -1,8 +1,14 @@
import { AddressDTO } from "../address"
import { FindConfig } from "../common" import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { CartDTO, FilterableCartProps } from "./common" import { CartAddressDTO, CartDTO, FilterableAddressProps, FilterableCartProps } from "./common"
import { CreateCartDTO, UpdateCartDTO } from "./mutations" import {
CreateAddressDTO,
CreateCartDTO,
UpdateAddressDTO,
UpdateCartDTO,
} from "./mutations"
export interface ICartModuleService extends IModuleService { export interface ICartModuleService extends IModuleService {
retrieve( retrieve(
@@ -32,6 +38,33 @@ export interface ICartModuleService extends IModuleService {
delete(cartIds: string[], sharedContext?: Context): Promise<void> delete(cartIds: string[], sharedContext?: Context): Promise<void>
delete(cartId: string, sharedContext?: Context): Promise<void> delete(cartId: string, sharedContext?: Context): Promise<void>
listAddresses(
filters?: FilterableAddressProps,
config?: FindConfig<AddressDTO>,
sharedContext?: Context
): Promise<CartAddressDTO[]>
createAddresses(
data: CreateAddressDTO[],
sharedContext?: Context
): Promise<CartAddressDTO[]>
createAddresses(
data: CreateAddressDTO,
sharedContext?: Context
): Promise<CartAddressDTO>
updateAddresses(
data: UpdateAddressDTO[],
sharedContext?: Context
): Promise<CartAddressDTO[]>
updateAddresses(
data: UpdateAddressDTO,
sharedContext?: Context
): Promise<CartAddressDTO>
deleteAddresses(ids: string[], sharedContext?: Context): Promise<void>
deleteAddresses(ids: string, sharedContext?: Context): Promise<void>
// addLineItems(data: AddLineItemsDTO, sharedContext?: Context): Promise<CartDTO> // addLineItems(data: AddLineItemsDTO, sharedContext?: Context): Promise<CartDTO>
// addLineItems( // addLineItems(
// data: AddLineItemsDTO[], // data: AddLineItemsDTO[],
@@ -3,7 +3,10 @@ import { PromotionDTO } from "./promotion"
import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule" import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
export type ApplicationMethodTypeValues = "fixed" | "percentage" export type ApplicationMethodTypeValues = "fixed" | "percentage"
export type ApplicationMethodTargetTypeValues = "order" | "shipping" | "item" export type ApplicationMethodTargetTypeValues =
| "order"
| "shipping_methods"
| "items"
export type ApplicationMethodAllocationValues = "each" | "across" export type ApplicationMethodAllocationValues = "each" | "across"
export interface ApplicationMethodDTO { export interface ApplicationMethodDTO {
@@ -0,0 +1,16 @@
import { BaseFilterable } from "../../dal"
export type CampaignBudgetTypeValues = "spend" | "usage"
export interface CampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: string | null
used?: string
}
export interface FilterableCampaignBudgetProps
extends BaseFilterable<FilterableCampaignBudgetProps> {
id?: string[]
type?: string[]
}
@@ -0,0 +1,19 @@
import { BaseFilterable } from "../../dal"
import { CampaignBudgetDTO } from "./campaign-budget"
export interface CampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
budget?: CampaignBudgetDTO
}
export interface FilterableCampaignProps
extends BaseFilterable<FilterableCampaignProps> {
id?: string[]
campaign_identifier?: string[]
}
@@ -0,0 +1,55 @@
export type ComputeActions =
| AddItemAdjustmentAction
| RemoveItemAdjustmentAction
| AddShippingMethodAdjustment
| RemoveShippingMethodAdjustment
export interface AddItemAdjustmentAction {
action: "addItemAdjustment"
item_id: string
amount: number
code: string
description?: string
}
export interface RemoveItemAdjustmentAction {
action: "removeItemAdjustment"
adjustment_id: string
description?: string
}
export interface AddShippingMethodAdjustment {
action: "addShippingMethodAdjustment"
shipping_method_id: string
amount: number
code: string
description?: string
}
export interface RemoveShippingMethodAdjustment {
action: "removeShippingMethodAdjustment"
adjustment_id: string
}
export interface ComputeActionAdjustmentLine {
id: string
code: string
}
export interface ComputeActionItemLine {
id: string
quantity: number
unit_price: number
adjustments?: ComputeActionAdjustmentLine[]
}
export interface ComputeActionShippingLine {
id: string
unit_price: number
adjustments?: ComputeActionAdjustmentLine[]
}
export interface ComputeActionContext {
items?: ComputeActionItemLine[]
shipping_methods?: ComputeActionShippingLine[]
}
@@ -1,4 +1,7 @@
export * from "./application-method" export * from "./application-method"
export * from "./campaign"
export * from "./campaign-budget"
export * from "./compute-actions"
export * from "./promotion" export * from "./promotion"
export * from "./promotion-rule" export * from "./promotion-rule"
export * from "./promotion-rule-value" export * from "./promotion-rule-value"
@@ -3,10 +3,11 @@ import { PromotionRuleDTO } from "./promotion-rule"
export interface PromotionRuleValueDTO { export interface PromotionRuleValueDTO {
id: string id: string
value?: string
} }
export interface CreatePromotionRuleValueDTO { export interface CreatePromotionRuleValueDTO {
value: any value: string
promotion_rule: PromotionRuleDTO promotion_rule: PromotionRuleDTO
} }
@@ -1,4 +1,5 @@
import { BaseFilterable } from "../../dal" import { BaseFilterable } from "../../dal"
import { PromotionRuleValueDTO } from "./promotion-rule-value"
export type PromotionRuleOperatorValues = export type PromotionRuleOperatorValues =
| "gt" | "gt"
@@ -11,10 +12,14 @@ export type PromotionRuleOperatorValues =
export interface PromotionRuleDTO { export interface PromotionRuleDTO {
id: string id: string
description?: string | null
attribute?: string
operator?: PromotionRuleOperatorValues
values: PromotionRuleValueDTO[]
} }
export interface CreatePromotionRuleDTO { export interface CreatePromotionRuleDTO {
description?: string description?: string | null
attribute: string attribute: string
operator: PromotionRuleOperatorValues operator: PromotionRuleOperatorValues
values: string[] | string values: string[] | string
@@ -31,4 +36,5 @@ export interface RemovePromotionRuleDTO {
export interface FilterablePromotionRuleProps export interface FilterablePromotionRuleProps
extends BaseFilterable<FilterablePromotionRuleProps> { extends BaseFilterable<FilterablePromotionRuleProps> {
id?: string[] id?: string[]
code?: string[]
} }
@@ -4,7 +4,7 @@ import {
CreateApplicationMethodDTO, CreateApplicationMethodDTO,
UpdateApplicationMethodDTO, UpdateApplicationMethodDTO,
} from "./application-method" } from "./application-method"
import { CreatePromotionRuleDTO } from "./promotion-rule" import { CreatePromotionRuleDTO, PromotionRuleDTO } from "./promotion-rule"
export type PromotionType = "standard" | "buyget" export type PromotionType = "standard" | "buyget"
@@ -14,6 +14,7 @@ export interface PromotionDTO {
type?: PromotionType type?: PromotionType
is_automatic?: boolean is_automatic?: boolean
application_method?: ApplicationMethodDTO application_method?: ApplicationMethodDTO
rules?: PromotionRuleDTO[]
} }
export interface CreatePromotionDTO { export interface CreatePromotionDTO {
+1
View File
@@ -1,2 +1,3 @@
export * from "./common" export * from "./common"
export * from "./mutations"
export * from "./service" export * from "./service"
+35
View File
@@ -0,0 +1,35 @@
import { CampaignBudgetTypeValues } from "./common"
export interface CreateCampaignBudgetDTO {
type: CampaignBudgetTypeValues
limit: number | null
used?: number
}
export interface UpdateCampaignBudgetDTO {
id: string
type?: CampaignBudgetTypeValues
limit?: number | null
used?: number
}
export interface CreateCampaignDTO {
name: string
description?: string
currency?: string
campaign_identifier: string
starts_at: Date
ends_at: Date
budget?: CreateCampaignBudgetDTO
}
export interface UpdateCampaignDTO {
id: string
name?: string
description?: string
currency?: string
campaign_identifier?: string
starts_at?: Date
ends_at?: Date
budget?: Omit<UpdateCampaignBudgetDTO, "id">
}
+58
View File
@@ -2,25 +2,47 @@ import { FindConfig } from "../common"
import { IModuleService } from "../modules-sdk" import { IModuleService } from "../modules-sdk"
import { Context } from "../shared-context" import { Context } from "../shared-context"
import { import {
CampaignDTO,
ComputeActionContext,
ComputeActions,
CreatePromotionDTO, CreatePromotionDTO,
CreatePromotionRuleDTO, CreatePromotionRuleDTO,
FilterableCampaignProps,
FilterablePromotionProps, FilterablePromotionProps,
PromotionDTO, PromotionDTO,
RemovePromotionRuleDTO, RemovePromotionRuleDTO,
UpdatePromotionDTO, UpdatePromotionDTO,
} from "./common" } from "./common"
import { CreateCampaignDTO, UpdateCampaignDTO } from "./mutations"
export interface IPromotionModuleService extends IModuleService { export interface IPromotionModuleService extends IModuleService {
computeActions(
promotionCodesToApply: string[],
applicationContext: ComputeActionContext,
options?: Record<string, any>
): Promise<ComputeActions[]>
create( create(
data: CreatePromotionDTO[], data: CreatePromotionDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<PromotionDTO[]> ): Promise<PromotionDTO[]>
create(
data: CreatePromotionDTO,
sharedContext?: Context
): Promise<PromotionDTO>
update( update(
data: UpdatePromotionDTO[], data: UpdatePromotionDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<PromotionDTO[]> ): Promise<PromotionDTO[]>
update(
data: UpdatePromotionDTO,
sharedContext?: Context
): Promise<PromotionDTO>
list( list(
filters?: FilterablePromotionProps, filters?: FilterablePromotionProps,
config?: FindConfig<PromotionDTO>, config?: FindConfig<PromotionDTO>,
@@ -34,6 +56,7 @@ export interface IPromotionModuleService extends IModuleService {
): Promise<PromotionDTO> ): Promise<PromotionDTO>
delete(ids: string[], sharedContext?: Context): Promise<void> delete(ids: string[], sharedContext?: Context): Promise<void>
delete(ids: string, sharedContext?: Context): Promise<void>
addPromotionRules( addPromotionRules(
promotionId: string, promotionId: string,
@@ -58,4 +81,39 @@ export interface IPromotionModuleService extends IModuleService {
rulesData: RemovePromotionRuleDTO[], rulesData: RemovePromotionRuleDTO[],
sharedContext?: Context sharedContext?: Context
): Promise<PromotionDTO> ): Promise<PromotionDTO>
createCampaigns(
data: CreateCampaignDTO,
sharedContext?: Context
): Promise<CampaignDTO>
createCampaigns(
data: CreateCampaignDTO[],
sharedContext?: Context
): Promise<CampaignDTO[]>
updateCampaigns(
data: UpdateCampaignDTO[],
sharedContext?: Context
): Promise<CampaignDTO[]>
updateCampaigns(
data: UpdateCampaignDTO,
sharedContext?: Context
): Promise<CampaignDTO>
listCampaigns(
filters?: FilterableCampaignProps,
config?: FindConfig<CampaignDTO>,
sharedContext?: Context
): Promise<CampaignDTO[]>
retrieveCampaign(
id: string,
config?: FindConfig<CampaignDTO>,
sharedContext?: Context
): Promise<CampaignDTO>
deleteCampaigns(ids: string[], sharedContext?: Context): Promise<void>
deleteCampaigns(ids: string, sharedContext?: Context): Promise<void>
} }
@@ -0,0 +1,111 @@
import { pickValueFromObject } from "../pick-value-from-object"
describe("pickValueFromObject", function () {
it("should return true or false for different types of data", function () {
const expectations = [
{
input: {
1: "attribute.another_attribute",
2: {
attribute: {
another_attribute: "test",
},
},
},
output: "test",
},
{
input: {
1: "attribute.another_attribute.array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: "test 1",
},
{
array_attribute: "test 2",
},
],
},
},
},
output: ["test 1", "test 2"],
},
{
input: {
1: "attribute.another_attribute.array_attribute.deep_array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
array_attribute: [],
},
],
},
},
},
output: ["test 1", "test 2"],
},
{
input: {
1: "attribute.another_attribute.array_attribute",
2: {
attribute: {
another_attribute: [
{
array_attribute: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
array_attribute: [],
},
],
},
},
},
output: [
{
deep_array_attribute: "test 1",
},
{
deep_array_attribute: "test 2",
},
],
},
{
input: {
1: "attribute.missing_attribute",
2: {
attribute: {
another_attribute: "test",
},
},
},
output: undefined,
},
]
expectations.forEach((expectation) => {
expect(
pickValueFromObject(expectation.input["1"], expectation.input["2"])
).toEqual(expectation.output)
})
})
})
+1
View File
@@ -23,6 +23,7 @@ export * from "./medusa-container"
export * from "./object-from-string-path" export * from "./object-from-string-path"
export * from "./object-to-string-path" export * from "./object-to-string-path"
export * from "./optional-numeric-serializer" export * from "./optional-numeric-serializer"
export * from "./pick-value-from-object"
export * from "./promise-all" export * from "./promise-all"
export * from "./remote-query-object-from-string" export * from "./remote-query-object-from-string"
export * from "./remote-query-object-to-string" export * from "./remote-query-object-to-string"
@@ -0,0 +1,37 @@
import { isObject } from "./is-object"
export function pickValueFromObject(
path: string,
object: Record<any, any>
): any {
const segments = path.split(".")
let result: any = undefined
for (const segment of segments) {
const segmentsLeft = [...segments].splice(1, segments.length - 1)
const segmentOutput = object[segment]
if (segmentsLeft.length === 0) {
result = segmentOutput
break
}
if (isObject(segmentOutput)) {
result = pickValueFromObject(segmentsLeft.join("."), segmentOutput)
break
}
if (Array.isArray(segmentOutput)) {
result = segmentOutput
.map((segmentOutput_) =>
pickValueFromObject(segmentsLeft.join("."), segmentOutput_)
)
.flat()
break
}
result = segmentOutput
}
return result
}
@@ -7,7 +7,6 @@ import {
import { import {
EntityManager, EntityManager,
EntitySchema, EntitySchema,
FilterQuery,
LoadStrategy, LoadStrategy,
RequiredEntityData, RequiredEntityData,
} from "@mikro-orm/core" } from "@mikro-orm/core"
@@ -17,9 +16,9 @@ import {
EntityName, EntityName,
FilterQuery as MikroFilterQuery, FilterQuery as MikroFilterQuery,
} from "@mikro-orm/core/typings" } from "@mikro-orm/core/typings"
import { MedusaError, arrayDifference, isString } from "../../common" import { isString, MedusaError } from "../../common"
import { MedusaContext } from "../../decorators" import { MedusaContext } from "../../decorators"
import { InjectTransactionManager, buildQuery } from "../../modules-sdk" import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
import { import {
getSoftDeletedCascadedEntitiesIdsMappedBy, getSoftDeletedCascadedEntitiesIdsMappedBy,
transactionWrapper, transactionWrapper,
@@ -77,10 +76,11 @@ export class MikroOrmBase<T = any> {
export class MikroOrmBaseRepository< export class MikroOrmBaseRepository<
T extends object = object T extends object = object
> extends MikroOrmBase<T> { > extends MikroOrmBase<T> {
constructor() { constructor(...args: any[]) {
// @ts-ignore // @ts-ignore
super(...arguments) super(...arguments)
} }
create(data: unknown[], context?: Context): Promise<T[]> { create(data: unknown[], context?: Context): Promise<T[]> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -89,7 +89,7 @@ export class MikroOrmBaseRepository<
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
delete(ids: string[], context?: Context): Promise<void> { delete(ids: string[] | object[], context?: Context): Promise<void> {
throw new Error("Method not implemented.") throw new Error("Method not implemented.")
} }
@@ -227,11 +227,25 @@ export function mikroOrmBaseRepositoryFactory<
TDTos extends { [K in DtoBasedMutationMethods]?: any } = { TDTos extends { [K in DtoBasedMutationMethods]?: any } = {
[K in DtoBasedMutationMethods]?: any [K in DtoBasedMutationMethods]?: any
} }
>( >(entity: EntityClass<T> | EntitySchema<T>) {
entity: EntityClass<T> | EntitySchema<T> | string,
primaryKey: string = "id"
) {
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> { class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
// @ts-ignore
constructor(...args: any[]) {
// @ts-ignore
super(...arguments)
}
static buildUniqueCompositeKeyValue(keys: string[], data: object) {
return keys.map((k) => data[k]).join("_")
}
static retrievePrimaryKeys(entity: EntityClass<T> | EntitySchema<T>) {
return (
(entity as EntitySchema<T>).meta?.primaryKeys ??
(entity as EntityClass<T>).prototype.__meta.primaryKeys
)
}
async create(data: TDTos["create"][], context?: Context): Promise<T[]> { async create(data: TDTos["create"][], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
@@ -250,42 +264,71 @@ export function mikroOrmBaseRepositoryFactory<
async update(data: TDTos["update"][], context?: Context): Promise<T[]> { async update(data: TDTos["update"][], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
const primaryKeyValues: string[] = data.map((data_) => data_[primaryKey]) const primaryKeys =
const existingEntities = await this.find( MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
{
where: { let primaryKeysCriteria: { [key: string]: any }[] = []
[primaryKey]: { if (primaryKeys.length === 1) {
$in: primaryKeyValues, primaryKeysCriteria.push({
}, [primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]),
}, })
} as DAL.FindOptions<T>, } else {
context primaryKeysCriteria = data.map((d) => ({
$and: primaryKeys.map((key) => ({ [key]: d[key] })),
}))
}
const allEntities = await Promise.all(
primaryKeysCriteria.map(
async (criteria) =>
await this.find({ where: criteria } as DAL.FindOptions<T>, context)
)
) )
const missingEntities = arrayDifference( const existingEntities = allEntities.flat()
data.map((d) => d[primaryKey]),
existingEntities.map((d: any) => d[primaryKey]) const existingEntitiesMap = new Map<string, T>()
) existingEntities.forEach((entity) => {
if (entity) {
const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
entity
)
existingEntitiesMap.set(key, entity)
}
})
const missingEntities = data.filter((data_) => {
const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
data_
)
return !existingEntitiesMap.has(key)
})
if (missingEntities.length) { if (missingEntities.length) {
const entityName = (entity as EntityClass<T>).name ?? entity const entityName = (entity as EntityClass<T>).name ?? entity
const missingEntitiesKeys = data.map((data_) =>
primaryKeys.map((key) => data_[key]).join(", ")
)
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
`${entityName} with ${[primaryKey]} "${missingEntities.join( `${entityName} with ${primaryKeys.join(
", " ", "
)}" not found` )} "${missingEntitiesKeys.join(", ")}" not found`
) )
} }
const existingEntitiesMap = new Map(
existingEntities.map<[string, T]>((entity_: any) => [
entity_[primaryKey],
entity_,
])
)
const entities = data.map((data_) => { const entities = data.map((data_) => {
const existingEntity = existingEntitiesMap.get(data_[primaryKey])! const key =
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
primaryKeys,
data_
)
const existingEntity = existingEntitiesMap.get(key)!
return manager.assign(existingEntity, data_ as RequiredEntityData<T>) return manager.assign(existingEntity, data_ as RequiredEntityData<T>)
}) })
@@ -294,13 +337,41 @@ export function mikroOrmBaseRepositoryFactory<
return entities return entities
} }
async delete(primaryKeyValues: string[], context?: Context): Promise<void> { async delete(
primaryKeyValues: string[] | object[],
context?: Context
): Promise<void> {
const manager = this.getActiveManager<EntityManager>(context) const manager = this.getActiveManager<EntityManager>(context)
await manager.nativeDelete<T>( const primaryKeys =
entity as EntityName<T>, MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
{ [primaryKey]: { $in: primaryKeyValues } } as unknown as FilterQuery<T>
) let deletionCriteria
if (primaryKeys.length > 1) {
deletionCriteria = {
$or: primaryKeyValues.map((compositeKeyValue) => {
const keys = Object.keys(compositeKeyValue)
if (!primaryKeys.every((k) => keys.includes(k))) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Composite key must contain all primary key fields: ${primaryKeys.join(
", "
)}. Found: ${keys}`
)
}
const criteria: { [key: string]: any } = {}
for (const key of primaryKeys) {
criteria[key] = compositeKeyValue[key]
}
return criteria
}),
}
} else {
deletionCriteria = { [primaryKeys[0]]: { $in: primaryKeyValues } }
}
await manager.nativeDelete<T>(entity as EntityName<T>, deletionCriteria)
} }
async find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> { async find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> {
+7 -2
View File
@@ -10,8 +10,8 @@ export enum ApplicationMethodType {
export enum ApplicationMethodTargetType { export enum ApplicationMethodTargetType {
ORDER = "order", ORDER = "order",
SHIPPING = "shipping", SHIPPING_METHODS = "shipping_methods",
ITEM = "item", ITEMS = "items",
} }
export enum ApplicationMethodAllocation { export enum ApplicationMethodAllocation {
@@ -28,3 +28,8 @@ export enum PromotionRuleOperator {
NE = "ne", NE = "ne",
IN = "in", IN = "in",
} }
export enum CampaignBudgetType {
SPEND = "spend",
USAGE = "usage",
}
+1 -1
View File
@@ -1 +1 @@
{"id":"https://github.com/medusajs/medusa/releases/tag/v1.20.0","content":"v1.20.0 is out","isCloseable":true} {}
+1 -1
View File
@@ -1240,7 +1240,7 @@ Notice that as there are two types of flows, you'll be creating the components f
const api_key = useMemo(() => keys?.[0]?.id || "", [keys]) const api_key = useMemo(() => keys?.[0]?.id || "", [keys])
const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ? const backendUrl = process.env.MEDUSA_BACKEND_URL === "/" || process.env.MEDUSA_ADMIN_BACKEND_URL === "/" ?
location.origin : location.origin :
process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://location:9000" process.env.MEDUSA_BACKEND_URL || process.env.MEDUSA_ADMIN_BACKEND_URL || "http://localhost:9000"
useEffect(() => { useEffect(() => {
if (!isLoading && !keys?.length) { if (!isLoading && !keys?.length) {
@@ -196,7 +196,7 @@ class MyFulfillmentService extends AbstractFulfillmentService {
// ... // ...
async createFulfillment( async createFulfillment(
data: Record<string, unknown>, data: Record<string, unknown>,
items: LineItem, items: LineItem[],
order: Order, order: Order,
fulfillment: Fulfillment fulfillment: Fulfillment
) { ) {
@@ -194,7 +194,7 @@ class MyFulfillmentService extends AbstractFulfillmentService {
// ... // ...
async createFulfillment( async createFulfillment(
data: Record<string, unknown>, data: Record<string, unknown>,
items: LineItem, items: LineItem[],
order: Order, order: Order,
fulfillment: Fulfillment fulfillment: Fulfillment
) { ) {
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more