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:
Adrien de Peretti
2022-07-11 23:05:28 +02:00
committed by GitHub
parent 19f35ba6aa
commit b402b9f159
10 changed files with 194 additions and 7 deletions

View File

@@ -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)
})
})
})

View File

@@ -9,11 +9,15 @@ import {
FulfillmentProviderService,
NotificationService,
PaymentProviderService,
SalesChannelService,
ShippingProfileService,
StoreService, TaxProviderService,
StoreService,
TaxProviderService,
} from "../services"
import { CurrencyRepository } from "../repositories/currency"
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 => {
try {
@@ -49,7 +53,9 @@ export default async ({ container }: { container: AwilixContainer }): Promise<vo
const currencyRepository = container.resolve<typeof CurrencyRepository>("currencyRepository")
const countryRepository = container.resolve<typeof CountryRepository>("countryRepository")
const profileService = container.resolve<ShippingProfileService>("shippingProfileService")
const salesChannelService = container.resolve<SalesChannelService>("salesChannelService")
const logger = container.resolve<Logger>("logger")
const featureFlagRouter = container.resolve<FlagRouter>("featureFlagRouter")
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 storeService.withTransaction(manager).create()
const payProviders =
silentResolution<typeof BasePaymentService[]>(container, "paymentProviders", logger) || []
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).createGiftCardDefault()
const isSalesChannelEnabled = featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)
if (isSalesChannelEnabled) {
await salesChannelService.withTransaction(manager).createDefault()
}
})
}

View File

@@ -2,7 +2,6 @@ import { BeforeInsert, Column } from "typeorm"
import { SoftDeletableEntity } from "../interfaces"
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
import { resolveDbType } from "../utils/db-aware-column"
import { generateEntityId } from "../utils"
@FeatureFlagEntity("sales_channels")

View File

@@ -31,6 +31,9 @@ export const PaymentProviderServiceMock = {
list: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
registerInstalledProviders: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
createSession: jest.fn().mockImplementation((providerId, cart) => {
return Promise.resolve({
id: `${providerId}_session`,

View File

@@ -27,6 +27,14 @@ export const SalesChannelServiceMock = {
delete: jest.fn().mockImplementation((id, config) => {
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(() => {

View File

@@ -16,12 +16,21 @@ export const profiles = {
}
export const ShippingProfileServiceMock = {
withTransaction: function () {
return this
},
update: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),
create: jest.fn().mockImplementation(data => {
return Promise.resolve(data)
}),
createDefault: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
createGiftCardDefault: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
retrieve: jest.fn().mockImplementation(data => {
if (data === IdMap.getId("default")) {
return Promise.resolve(profiles.default)

View File

@@ -7,6 +7,12 @@ export const store = {
}
export const StoreServiceMock = {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation(data => {
return Promise.resolve(data)
}),
addCurrency: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),

View File

@@ -1,9 +1,10 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import SalesChannelService from "../sales-channel"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import { EventBusService } from "../index"
import { EventBusService, StoreService } from "../index"
import { FindConditions, FindOneOptions } from "typeorm"
import { SalesChannel } from "../../models"
import { store, StoreServiceMock } from "../__mocks__/store";
describe("SalesChannelService", () => {
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 => {
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", () => {
const salesChannelService = new SalesChannelService({
manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock,
storeService: StoreServiceMock as unknown as StoreService
})
beforeEach(() => {
@@ -67,6 +127,7 @@ describe("SalesChannelService", () => {
manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock,
storeService: StoreServiceMock as unknown as StoreService
})
const update = {
@@ -100,7 +161,8 @@ describe("SalesChannelService", () => {
const salesChannelService = new SalesChannelService({
manager: MockManager,
eventBusService: EventBusServiceMock as unknown as EventBusService,
salesChannelRepository: salesChannelRepositoryMock
salesChannelRepository: salesChannelRepositoryMock,
storeService: StoreServiceMock as unknown as StoreService
})
beforeEach(() => {

View File

@@ -11,12 +11,13 @@ import {
} from "../types/sales-channels"
import EventBusService from "./event-bus"
import { buildQuery } from "../utils"
import { PostgresError } from "../utils/exception-formatter"
import StoreService from "./store"
type InjectedDependencies = {
salesChannelRepository: typeof SalesChannelRepository
eventBusService: EventBusService
manager: EntityManager
storeService: StoreService
}
class SalesChannelService extends TransactionBaseService<SalesChannelService> {
@@ -31,11 +32,13 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
protected readonly salesChannelRepository_: typeof SalesChannelRepository
protected readonly eventBusService_: EventBusService
protected readonly storeService_: StoreService
constructor({
salesChannelRepository,
eventBusService,
manager,
storeService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
@@ -43,6 +46,7 @@ class SalesChannelService extends TransactionBaseService<SalesChannelService> {
this.manager_ = manager
this.salesChannelRepository_ = salesChannelRepository
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

View File

@@ -6,4 +6,5 @@ export type UpdateStoreInput = {
default_currency_code?: string
currencies?: string[]
metadata?: Record<string, unknown>
default_sales_channel_id?: string
}