feat(medusa): Create default sales channel associated to the store (#1830)
**What** Add support for default sales channel **How** - Implement a new method in the salesChannelService `createDefault` - call the new method above in the default loader **Test** - Unit tests of the sales channel service method createDefaulta - Init default loader unit tests Fixes CORE-316
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
import { asValue, createContainer } from "awilix";
|
||||||
|
import { MockRepository, MockManager } from "medusa-test-utils"
|
||||||
|
import { StoreServiceMock } from "../../services/__mocks__/store";
|
||||||
|
import { ShippingProfileServiceMock } from "../../services/__mocks__/shipping-profile";
|
||||||
|
import Logger from "../logger";
|
||||||
|
import featureFlagsLoader from "../feature-flags";
|
||||||
|
import { default as defaultLoader } from "../defaults"
|
||||||
|
import { SalesChannelServiceMock } from "../../services/__mocks__/sales-channel";
|
||||||
|
import { PaymentProviderServiceMock } from "../../services/__mocks__/payment-provider";
|
||||||
|
|
||||||
|
describe('default', () => {
|
||||||
|
describe('sales channel default', () => {
|
||||||
|
let featureFlagRouter
|
||||||
|
const container = createContainer()
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
featureFlagRouter = await featureFlagsLoader({
|
||||||
|
featureFlags: {
|
||||||
|
sales_channels: true,
|
||||||
|
},
|
||||||
|
}, Logger)
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
storeService: asValue(StoreServiceMock),
|
||||||
|
currencyRepository: asValue(MockRepository()),
|
||||||
|
countryRepository: asValue(MockRepository()),
|
||||||
|
shippingProfileService: asValue(ShippingProfileServiceMock),
|
||||||
|
salesChannelService: asValue(SalesChannelServiceMock),
|
||||||
|
logger: asValue(Logger),
|
||||||
|
featureFlagRouter: asValue(featureFlagRouter),
|
||||||
|
manager: asValue(MockManager),
|
||||||
|
paymentProviders: asValue([]),
|
||||||
|
paymentProviderService: asValue(PaymentProviderServiceMock),
|
||||||
|
notificationProviders: asValue([]),
|
||||||
|
notificationService: asValue({
|
||||||
|
registerInstalledProviders: jest.fn(),
|
||||||
|
}),
|
||||||
|
fulfillmentProviders: asValue([]),
|
||||||
|
fulfillmentProviderService: asValue({
|
||||||
|
registerInstalledProviders: jest.fn(),
|
||||||
|
}),
|
||||||
|
taxProviders: asValue([]),
|
||||||
|
taxProviderService: asValue({
|
||||||
|
registerInstalledProviders: jest.fn(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should create a new default sales channel attach to the store", async () => {
|
||||||
|
await defaultLoader({ container })
|
||||||
|
expect(SalesChannelServiceMock.createDefault).toHaveBeenCalledTimes(1)
|
||||||
|
expect(SalesChannelServiceMock.createDefault).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -9,11 +9,15 @@ import {
|
|||||||
FulfillmentProviderService,
|
FulfillmentProviderService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
PaymentProviderService,
|
PaymentProviderService,
|
||||||
|
SalesChannelService,
|
||||||
ShippingProfileService,
|
ShippingProfileService,
|
||||||
StoreService, TaxProviderService,
|
StoreService,
|
||||||
|
TaxProviderService,
|
||||||
} from "../services"
|
} from "../services"
|
||||||
import { CurrencyRepository } from "../repositories/currency"
|
import { CurrencyRepository } from "../repositories/currency"
|
||||||
import { AbstractTaxService } from "../interfaces"
|
import { AbstractTaxService } from "../interfaces"
|
||||||
|
import { FlagRouter } from "../utils/flag-router";
|
||||||
|
import SalesChannelFeatureFlag from "./feature-flags/sales-channels";
|
||||||
|
|
||||||
const silentResolution = <T>(container: AwilixContainer, name: string, logger: Logger): T | never | undefined => {
|
const silentResolution = <T>(container: AwilixContainer, name: string, logger: Logger): T | never | undefined => {
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +53,9 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
|
|||||||
const currencyRepository = container.resolve<typeof CurrencyRepository>("currencyRepository")
|
const currencyRepository = container.resolve<typeof CurrencyRepository>("currencyRepository")
|
||||||
const countryRepository = container.resolve<typeof CountryRepository>("countryRepository")
|
const countryRepository = container.resolve<typeof CountryRepository>("countryRepository")
|
||||||
const profileService = container.resolve<ShippingProfileService>("shippingProfileService")
|
const profileService = container.resolve<ShippingProfileService>("shippingProfileService")
|
||||||
|
const salesChannelService = container.resolve<SalesChannelService>("salesChannelService")
|
||||||
const logger = container.resolve<Logger>("logger")
|
const logger = container.resolve<Logger>("logger")
|
||||||
|
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
|
||||||
|
|
||||||
const entityManager = container.resolve<EntityManager>("manager")
|
const entityManager = container.resolve<EntityManager>("manager")
|
||||||
|
|
||||||
@@ -97,7 +103,6 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
|
|||||||
await entityManager.transaction(async (manager: EntityManager) => {
|
await entityManager.transaction(async (manager: EntityManager) => {
|
||||||
await storeService.withTransaction(manager).create()
|
await storeService.withTransaction(manager).create()
|
||||||
|
|
||||||
|
|
||||||
const payProviders =
|
const payProviders =
|
||||||
silentResolution<typeof BasePaymentService[]>(container, "paymentProviders", logger) || []
|
silentResolution<typeof BasePaymentService[]>(container, "paymentProviders", logger) || []
|
||||||
const payIds = payProviders.map((p) => p.getIdentifier())
|
const payIds = payProviders.map((p) => p.getIdentifier())
|
||||||
@@ -129,5 +134,10 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
|
|||||||
|
|
||||||
await profileService.withTransaction(manager).createDefault()
|
await profileService.withTransaction(manager).createDefault()
|
||||||
await profileService.withTransaction(manager).createGiftCardDefault()
|
await profileService.withTransaction(manager).createGiftCardDefault()
|
||||||
|
|
||||||
|
const isSalesChannelEnabled = featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||||
|
if (isSalesChannelEnabled) {
|
||||||
|
await salesChannelService.withTransaction(manager).createDefault()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { BeforeInsert, Column } from "typeorm"
|
|||||||
|
|
||||||
import { SoftDeletableEntity } from "../interfaces"
|
import { SoftDeletableEntity } from "../interfaces"
|
||||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||||
import { resolveDbType } from "../utils/db-aware-column"
|
|
||||||
import { generateEntityId } from "../utils"
|
import { generateEntityId } from "../utils"
|
||||||
|
|
||||||
@FeatureFlagEntity("sales_channels")
|
@FeatureFlagEntity("sales_channels")
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const PaymentProviderServiceMock = {
|
|||||||
list: jest.fn().mockImplementation(() => {
|
list: jest.fn().mockImplementation(() => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
|
registerInstalledProviders: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
createSession: jest.fn().mockImplementation((providerId, cart) => {
|
createSession: jest.fn().mockImplementation((providerId, cart) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: `${providerId}_session`,
|
id: `${providerId}_session`,
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ export const SalesChannelServiceMock = {
|
|||||||
delete: jest.fn().mockImplementation((id, config) => {
|
delete: jest.fn().mockImplementation((id, config) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
createDefault: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
name: "sales channel 1 name",
|
||||||
|
description: "sales channel 1 description",
|
||||||
|
is_disabled: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const mock = jest.fn().mockImplementation(() => {
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
|||||||
@@ -16,12 +16,21 @@ export const profiles = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShippingProfileServiceMock = {
|
export const ShippingProfileServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
update: jest.fn().mockImplementation(data => {
|
update: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
create: jest.fn().mockImplementation(data => {
|
create: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
}),
|
}),
|
||||||
|
createDefault: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
createGiftCardDefault: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
retrieve: jest.fn().mockImplementation(data => {
|
retrieve: jest.fn().mockImplementation(data => {
|
||||||
if (data === IdMap.getId("default")) {
|
if (data === IdMap.getId("default")) {
|
||||||
return Promise.resolve(profiles.default)
|
return Promise.resolve(profiles.default)
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export const store = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const StoreServiceMock = {
|
export const StoreServiceMock = {
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
create: jest.fn().mockImplementation(data => {
|
||||||
|
return Promise.resolve(data)
|
||||||
|
}),
|
||||||
addCurrency: jest.fn().mockImplementation(data => {
|
addCurrency: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||||
import SalesChannelService from "../sales-channel"
|
import SalesChannelService from "../sales-channel"
|
||||||
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
import { EventBusServiceMock } from "../__mocks__/event-bus"
|
||||||
import { EventBusService } from "../index"
|
import { EventBusService, StoreService } from "../index"
|
||||||
import { FindConditions, FindOneOptions } from "typeorm"
|
import { FindConditions, FindOneOptions } from "typeorm"
|
||||||
import { SalesChannel } from "../../models"
|
import { SalesChannel } from "../../models"
|
||||||
|
import { store, StoreServiceMock } from "../__mocks__/store";
|
||||||
|
|
||||||
describe("SalesChannelService", () => {
|
describe("SalesChannelService", () => {
|
||||||
const salesChannelData = {
|
const salesChannelData = {
|
||||||
@@ -27,17 +28,76 @@ describe("SalesChannelService", () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
save: (salesChannel) => Promise.resolve(salesChannel),
|
create: jest.fn().mockImplementation((data) => data),
|
||||||
|
save: (salesChannel) => Promise.resolve({
|
||||||
|
id: IdMap.getId("sales_channel_1"),
|
||||||
|
...salesChannel
|
||||||
|
}),
|
||||||
softRemove: jest.fn().mockImplementation((id: string): any => {
|
softRemove: jest.fn().mockImplementation((id: string): any => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("create default", async () => {
|
||||||
|
const salesChannelService = new SalesChannelService({
|
||||||
|
manager: MockManager,
|
||||||
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
|
salesChannelRepository: salesChannelRepositoryMock,
|
||||||
|
storeService: StoreServiceMock as unknown as StoreService
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should call the save method if the store does not have a default sales channel", async () => {
|
||||||
|
await salesChannelService.createDefault()
|
||||||
|
|
||||||
|
expect(salesChannelRepositoryMock.save).toHaveBeenCalledTimes(1)
|
||||||
|
expect(salesChannelRepositoryMock.save).toHaveBeenCalledWith({
|
||||||
|
description: "Created by Medusa",
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
is_disabled: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return the default sales channel if it already exists", async () => {
|
||||||
|
const localSalesChannelService = new SalesChannelService({
|
||||||
|
manager: MockManager,
|
||||||
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
|
salesChannelRepository: salesChannelRepositoryMock,
|
||||||
|
storeService: {
|
||||||
|
...StoreServiceMock,
|
||||||
|
retrieve: jest.fn().mockImplementation(() => {
|
||||||
|
return Promise.resolve({
|
||||||
|
...store,
|
||||||
|
default_sales_channel_id: IdMap.getId("sales_channel_1"),
|
||||||
|
default_sales_channel: {
|
||||||
|
id: IdMap.getId("sales_channel_1"),
|
||||||
|
...salesChannelData,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} as any
|
||||||
|
})
|
||||||
|
|
||||||
|
const salesChannel = await localSalesChannelService.createDefault()
|
||||||
|
|
||||||
|
expect(salesChannelRepositoryMock.save).toHaveBeenCalledTimes(0)
|
||||||
|
expect(salesChannelRepositoryMock.save).not.toHaveBeenCalledTimes(1)
|
||||||
|
expect(salesChannel).toEqual({
|
||||||
|
id: IdMap.getId("sales_channel_1"),
|
||||||
|
...salesChannelData,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("retrieve", () => {
|
describe("retrieve", () => {
|
||||||
const salesChannelService = new SalesChannelService({
|
const salesChannelService = new SalesChannelService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
salesChannelRepository: salesChannelRepositoryMock,
|
salesChannelRepository: salesChannelRepositoryMock,
|
||||||
|
storeService: StoreServiceMock as unknown as StoreService
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -67,6 +127,7 @@ describe("SalesChannelService", () => {
|
|||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
salesChannelRepository: salesChannelRepositoryMock,
|
salesChannelRepository: salesChannelRepositoryMock,
|
||||||
|
storeService: StoreServiceMock as unknown as StoreService
|
||||||
})
|
})
|
||||||
|
|
||||||
const update = {
|
const update = {
|
||||||
@@ -100,7 +161,8 @@ describe("SalesChannelService", () => {
|
|||||||
const salesChannelService = new SalesChannelService({
|
const salesChannelService = new SalesChannelService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
eventBusService: EventBusServiceMock as unknown as EventBusService,
|
||||||
salesChannelRepository: salesChannelRepositoryMock
|
salesChannelRepository: salesChannelRepositoryMock,
|
||||||
|
storeService: StoreServiceMock as unknown as StoreService
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import {
|
|||||||
} from "../types/sales-channels"
|
} from "../types/sales-channels"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
import { buildQuery } from "../utils"
|
import { buildQuery } from "../utils"
|
||||||
import { PostgresError } from "../utils/exception-formatter"
|
import StoreService from "./store"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
salesChannelRepository: typeof SalesChannelRepository
|
salesChannelRepository: typeof SalesChannelRepository
|
||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
|
storeService: StoreService
|
||||||
}
|
}
|
||||||
|
|
||||||
class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
||||||
@@ -31,11 +32,13 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
|||||||
|
|
||||||
protected readonly salesChannelRepository_: typeof SalesChannelRepository
|
protected readonly salesChannelRepository_: typeof SalesChannelRepository
|
||||||
protected readonly eventBusService_: EventBusService
|
protected readonly eventBusService_: EventBusService
|
||||||
|
protected readonly storeService_: StoreService
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
salesChannelRepository,
|
salesChannelRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
manager,
|
manager,
|
||||||
|
storeService,
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
super(arguments[0])
|
super(arguments[0])
|
||||||
@@ -43,6 +46,7 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
|||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
this.salesChannelRepository_ = salesChannelRepository
|
this.salesChannelRepository_ = salesChannelRepository
|
||||||
this.eventBusService_ = eventBusService
|
this.eventBusService_ = eventBusService
|
||||||
|
this.storeService_ = storeService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,6 +174,36 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a default sales channel, if this does not already exist.
|
||||||
|
* @return the sales channel
|
||||||
|
*/
|
||||||
|
async createDefault(): Promise<SalesChannel> {
|
||||||
|
return this.atomicPhase_(async (transactionManager) => {
|
||||||
|
const store = await this.storeService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.retrieve({
|
||||||
|
relations: ["default_sales_channel"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (store.default_sales_channel_id) {
|
||||||
|
return store.default_sales_channel
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSalesChannel = await this.create({
|
||||||
|
description: "Created by Medusa",
|
||||||
|
name: "Default Sales Channel",
|
||||||
|
is_disabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.storeService_.withTransaction(transactionManager).update({
|
||||||
|
default_sales_channel_id: defaultSalesChannel.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
return defaultSalesChannel
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SalesChannelService
|
export default SalesChannelService
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export type UpdateStoreInput = {
|
|||||||
default_currency_code?: string
|
default_currency_code?: string
|
||||||
currencies?: string[]
|
currencies?: string[]
|
||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
|
default_sales_channel_id?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user