feat(medusa): Add AbstractFulfillmentService (#4922)
* feat(medusa): Add abstract fulfillment service * add data types * add loaders * Create nine-glasses-allow.md * address pr comments
This commit is contained in:
6
.changeset/nine-glasses-allow.md
Normal file
6
.changeset/nine-glasses-allow.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
"medusa-fulfillment-webshipper": minor
|
||||
---
|
||||
|
||||
[wip] feat(medusa): Add AbstractFulfillmentService
|
||||
@@ -1,8 +1,8 @@
|
||||
import { humanizeAmount } from "medusa-core-utils"
|
||||
import { FulfillmentService } from "medusa-interfaces"
|
||||
import Webshipper from "../utils/webshipper"
|
||||
import { AbstractFulfillmentService } from "@medusajs/medusa"
|
||||
|
||||
class WebshipperFulfillmentService extends FulfillmentService {
|
||||
class WebshipperFulfillmentService extends AbstractFulfillmentService {
|
||||
static identifier = "webshipper"
|
||||
|
||||
constructor(
|
||||
@@ -97,10 +97,6 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
// Calculate prices
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a return shipment in webshipper using the given method data, and
|
||||
* return lines.
|
||||
*/
|
||||
async createReturn(returnOrder) {
|
||||
let orderId
|
||||
if (returnOrder.order_id) {
|
||||
@@ -477,13 +473,11 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This plugin doesn't support shipment documents.
|
||||
*/
|
||||
async retrieveDocuments(fulfillmentData, documentType) {
|
||||
const labelRelation = fulfillmentData?.relationships?.labels
|
||||
const docRelation = fulfillmentData?.relationships?.documents
|
||||
switch (documentType) {
|
||||
case "label":
|
||||
const labelRelation = fulfillmentData?.relationships?.labels
|
||||
if (labelRelation) {
|
||||
const docs = await this.retrieveRelationship(labelRelation)
|
||||
.then(({ data }) => data)
|
||||
@@ -498,7 +492,6 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
return []
|
||||
|
||||
case "invoice":
|
||||
const docRelation = fulfillmentData?.relationships?.documents
|
||||
if (docRelation) {
|
||||
const docs = await this.retrieveRelationship(docRelation)
|
||||
.then(({ data }) => data)
|
||||
@@ -517,11 +510,6 @@ class WebshipperFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the documents associated with an order.
|
||||
* @return {Promise<Array<_>>} an array of document objects to store in the
|
||||
* database.
|
||||
*/
|
||||
async getFulfillmentDocuments(data) {
|
||||
const order = await this.client_.orders.retrieve(data.id)
|
||||
const docs = await this.retrieveRelationship(
|
||||
|
||||
169
packages/medusa/src/interfaces/fulfillment-service.ts
Normal file
169
packages/medusa/src/interfaces/fulfillment-service.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { MedusaContainer } from "@medusajs/types"
|
||||
import { Cart, Fulfillment, LineItem, Order } from "../models"
|
||||
import { CreateReturnType } from "../types/fulfillment-provider"
|
||||
|
||||
type FulfillmentProviderData = Record<string, unknown>
|
||||
type ShippingOptionData = Record<string, unknown>
|
||||
type ShippingMethodData = Record<string, unknown>
|
||||
|
||||
/**
|
||||
* Fulfillment Provider interface
|
||||
* Fullfillment provider plugin services should extend the AbstractFulfillmentService from this file
|
||||
*/
|
||||
export interface FulfillmentService {
|
||||
/**
|
||||
* Return a unique identifier to retrieve the fulfillment plugin provider
|
||||
*/
|
||||
getIdentifier(): string
|
||||
|
||||
/**
|
||||
* Called before a shipping option is created in Admin. The method should
|
||||
* return all of the options that the fulfillment provider can be used with,
|
||||
* and it is here the distinction between different shipping options are
|
||||
* enforced. For example, a fulfillment provider may offer Standard Shipping
|
||||
* and Express Shipping as fulfillment options, it is up to the store operator
|
||||
* to create shipping options in Medusa that are offered to the customer.
|
||||
*/
|
||||
getFulfillmentOptions(): Promise<any[]>
|
||||
|
||||
/**
|
||||
* Called before a shipping method is set on a cart to ensure that the data
|
||||
* sent with the shipping method is valid. The data object may contain extra
|
||||
* data about the shipment such as an id of a drop point. It is up to the
|
||||
* fulfillment provider to enforce that the correct data is being sent
|
||||
* through.
|
||||
* @return the data to populate `cart.shipping_methods.$.data` this
|
||||
* is usually important for future actions like generating shipping labels
|
||||
*/
|
||||
validateFulfillmentData(
|
||||
optionData: ShippingOptionData,
|
||||
data: FulfillmentProviderData,
|
||||
cart: Cart
|
||||
): Promise<Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* Called before a shipping option is created in Admin. Use this to ensure
|
||||
* that a fulfillment option does in fact exist.
|
||||
*/
|
||||
validateOption(data: ShippingOptionData): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Used to determine if a shipping option can have a calculated price
|
||||
*/
|
||||
canCalculate(data: ShippingOptionData): Promise<boolean>
|
||||
|
||||
/**
|
||||
* Used to calculate a price for a given shipping option.
|
||||
*/
|
||||
calculatePrice(
|
||||
optionData: ShippingOptionData,
|
||||
data: FulfillmentProviderData,
|
||||
cart: Cart
|
||||
): Promise<number>
|
||||
|
||||
/**
|
||||
* Create a fulfillment using data from shipping method, line items, and fulfillment. All from the order.
|
||||
* The returned value of this method will populate the `fulfillment.data` field.
|
||||
*/
|
||||
createFulfillment(
|
||||
data: ShippingMethodData,
|
||||
items: LineItem,
|
||||
order: Order,
|
||||
fulfillment: Fulfillment
|
||||
): Promise<FulfillmentProviderData>
|
||||
|
||||
/**
|
||||
* Cancel a fulfillment using data from the fulfillment
|
||||
*/
|
||||
cancelFulfillment(fulfillmentData: FulfillmentProviderData): Promise<any>
|
||||
|
||||
/**
|
||||
* Used to create a return order. Should return the data necessary for future
|
||||
* operations on the return; in particular the data may be used to receive
|
||||
* documents attached to the return.
|
||||
*/
|
||||
createReturn(returnOrder: CreateReturnType): Promise<Record<string, unknown>>
|
||||
|
||||
/**
|
||||
* Used to retrieve documents associated with a fulfillment.
|
||||
*/
|
||||
getFulfillmentDocuments(data: FulfillmentProviderData): Promise<any>
|
||||
|
||||
/**
|
||||
* Used to retrieve documents related to a return order.
|
||||
*/
|
||||
getReturnDocuments(data: Record<string, unknown>): Promise<any>
|
||||
|
||||
/**
|
||||
* Used to retrieve documents related to a shipment.
|
||||
*/
|
||||
getShipmentDocuments(data: Record<string, unknown>): Promise<any>
|
||||
|
||||
retrieveDocuments(
|
||||
fulfillmentData: FulfillmentProviderData,
|
||||
documentType: "invoice" | "label"
|
||||
): Promise<any>
|
||||
}
|
||||
|
||||
export abstract class AbstractFulfillmentService implements FulfillmentService {
|
||||
protected constructor(
|
||||
protected readonly container: MedusaContainer,
|
||||
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
) {}
|
||||
|
||||
public static identifier: string
|
||||
|
||||
public getIdentifier(): string {
|
||||
const ctr = this.constructor as typeof AbstractFulfillmentService
|
||||
|
||||
if (!ctr.identifier) {
|
||||
throw new Error(`Missing static property "identifier".`)
|
||||
}
|
||||
|
||||
return ctr.identifier
|
||||
}
|
||||
|
||||
abstract getFulfillmentOptions(): Promise<any[]>
|
||||
|
||||
abstract validateFulfillmentData(
|
||||
optionData: ShippingOptionData,
|
||||
data: FulfillmentProviderData,
|
||||
cart: Cart
|
||||
): Promise<Record<string, unknown>>
|
||||
|
||||
abstract validateOption(data: ShippingOptionData): Promise<boolean>
|
||||
|
||||
abstract canCalculate(data: ShippingOptionData): Promise<boolean>
|
||||
|
||||
abstract calculatePrice(
|
||||
optionData: ShippingOptionData,
|
||||
data: FulfillmentProviderData,
|
||||
cart: Cart
|
||||
): Promise<number>
|
||||
|
||||
abstract createFulfillment(
|
||||
data: ShippingMethodData,
|
||||
items: LineItem,
|
||||
order: Order,
|
||||
fulfillment: Fulfillment
|
||||
): Promise<FulfillmentProviderData>
|
||||
|
||||
abstract cancelFulfillment(fulfillment: FulfillmentProviderData): Promise<any>
|
||||
|
||||
abstract createReturn(
|
||||
returnOrder: CreateReturnType
|
||||
): Promise<Record<string, unknown>>
|
||||
|
||||
abstract getFulfillmentDocuments(data: FulfillmentProviderData): Promise<any>
|
||||
|
||||
abstract getReturnDocuments(data: Record<string, unknown>): Promise<any>
|
||||
|
||||
abstract getShipmentDocuments(data: Record<string, unknown>): Promise<any>
|
||||
|
||||
abstract retrieveDocuments(
|
||||
fulfillmentData: Record<string, unknown>,
|
||||
documentType: "invoice" | "label"
|
||||
): Promise<any>
|
||||
}
|
||||
|
||||
export default AbstractFulfillmentService
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./batch-job-strategy"
|
||||
export * from "./cart-completion-strategy"
|
||||
export * from "./file-service"
|
||||
export * from "./fulfillment-service"
|
||||
export * from "./models/base-entity"
|
||||
export * from "./models/soft-deletable-entity"
|
||||
export * from "./notification-service"
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { AwilixContainer } from "awilix"
|
||||
import {
|
||||
BaseFulfillmentService,
|
||||
BaseNotificationService,
|
||||
BasePaymentService,
|
||||
BaseFulfillmentService,
|
||||
BaseNotificationService,
|
||||
BasePaymentService,
|
||||
} from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
AbstractTaxService,
|
||||
AbstractFulfillmentService,
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
AbstractTaxService,
|
||||
} from "../interfaces"
|
||||
import { CountryRepository } from "../repositories/country"
|
||||
import { CurrencyRepository } from "../repositories/currency"
|
||||
import {
|
||||
FulfillmentProviderService,
|
||||
NotificationService,
|
||||
PaymentProviderService,
|
||||
SalesChannelService,
|
||||
ShippingProfileService,
|
||||
StoreService,
|
||||
TaxProviderService,
|
||||
FulfillmentProviderService,
|
||||
NotificationService,
|
||||
PaymentProviderService,
|
||||
SalesChannelService,
|
||||
ShippingProfileService,
|
||||
StoreService,
|
||||
TaxProviderService,
|
||||
} from "../services"
|
||||
import { Logger } from "../types/global"
|
||||
import { countries } from "../utils/countries"
|
||||
@@ -227,7 +228,7 @@ async function registerNotificationProvider({
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const notiProviders =
|
||||
silentResolution<typeof BaseNotificationService[]>(
|
||||
silentResolution<(typeof BaseNotificationService)[]>(
|
||||
container,
|
||||
"notificationProviders",
|
||||
logger
|
||||
@@ -252,11 +253,9 @@ async function registerFulfillmentProvider({
|
||||
logger: Logger
|
||||
}): Promise<void> {
|
||||
const fulfilProviders =
|
||||
silentResolution<typeof BaseFulfillmentService[]>(
|
||||
container,
|
||||
"fulfillmentProviders",
|
||||
logger
|
||||
) || []
|
||||
silentResolution<
|
||||
(typeof BaseFulfillmentService | AbstractFulfillmentService)[]
|
||||
>(container, "fulfillmentProviders", logger) || []
|
||||
const fulfilIds = fulfilProviders.map((p) => p.getIdentifier())
|
||||
|
||||
const fProviderService = container.resolve<FulfillmentProviderService>(
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { ClassConstructor, MedusaContainer } from "../../types/global"
|
||||
import { Lifetime, LifetimeType, aliasTo, asFunction } from "awilix"
|
||||
import { FulfillmentService } from "medusa-interfaces"
|
||||
import {
|
||||
AbstractFulfillmentService,
|
||||
AbstractPaymentProcessor,
|
||||
AbstractPaymentService,
|
||||
isPaymentProcessor,
|
||||
isPaymentService,
|
||||
} from "../../interfaces"
|
||||
import { aliasTo, asFunction, Lifetime, LifetimeType } from "awilix"
|
||||
import { ClassConstructor, MedusaContainer } from "../../types/global"
|
||||
|
||||
type Context = {
|
||||
container: MedusaContainer
|
||||
@@ -74,3 +76,66 @@ export function registerPaymentProcessorFromClass(
|
||||
aliasTo(registrationName),
|
||||
})
|
||||
}
|
||||
|
||||
export function registerAbstractFulfillmentServiceFromClass(
|
||||
klass: ClassConstructor<AbstractFulfillmentService> & {
|
||||
LIFE_TIME?: LifetimeType
|
||||
},
|
||||
context: Context
|
||||
): void {
|
||||
if (!(klass.prototype instanceof AbstractFulfillmentService)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { container, pluginDetails, registrationName } = context
|
||||
|
||||
container.registerAdd(
|
||||
"fulfillmentProviders",
|
||||
asFunction((cradle) => new klass(cradle, pluginDetails.options), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
})
|
||||
)
|
||||
|
||||
container.register({
|
||||
[registrationName]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginDetails.options),
|
||||
{
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
[`fp_${
|
||||
(klass as unknown as typeof AbstractFulfillmentService).identifier
|
||||
}`]: aliasTo(registrationName),
|
||||
})
|
||||
}
|
||||
|
||||
export function registerFulfillmentServiceFromClass(
|
||||
klass: ClassConstructor<typeof FulfillmentService> & {
|
||||
LIFE_TIME?: LifetimeType
|
||||
},
|
||||
context: Context
|
||||
): void {
|
||||
if (!(klass.prototype instanceof FulfillmentService)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { container, pluginDetails, registrationName } = context
|
||||
|
||||
container.registerAdd(
|
||||
"fulfillmentProviders",
|
||||
asFunction((cradle) => new klass(cradle, pluginDetails.options), {
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
})
|
||||
)
|
||||
|
||||
container.register({
|
||||
[registrationName]: asFunction(
|
||||
(cradle) => new klass(cradle, pluginDetails.options),
|
||||
{
|
||||
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
[`fp_${(klass as unknown as typeof FulfillmentService).identifier}`]:
|
||||
aliasTo(registrationName),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { SearchUtils, upperCaseFirst } from "@medusajs/utils"
|
||||
import { Lifetime, aliasTo, asFunction, asValue } from "awilix"
|
||||
import { FileService, OauthService } from "medusa-interfaces"
|
||||
import {
|
||||
AbstractTaxService,
|
||||
isBatchJobStrategy,
|
||||
@@ -13,34 +16,29 @@ import {
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
} from "../types/global"
|
||||
import {
|
||||
FileService,
|
||||
FulfillmentService,
|
||||
OauthService,
|
||||
} from "medusa-interfaces"
|
||||
import { Lifetime, aliasTo, asFunction, asValue } from "awilix"
|
||||
import { SearchUtils, upperCaseFirst } from "@medusajs/utils"
|
||||
import {
|
||||
formatRegistrationName,
|
||||
formatRegistrationNameWithoutNamespace,
|
||||
} from "../utils/format-registration-name"
|
||||
import {
|
||||
registerAbstractFulfillmentServiceFromClass,
|
||||
registerFulfillmentServiceFromClass,
|
||||
registerPaymentProcessorFromClass,
|
||||
registerPaymentServiceFromClass,
|
||||
} from "./helpers/plugins"
|
||||
|
||||
import { EntitySchema } from "typeorm"
|
||||
import { Express } from "express"
|
||||
import { MiddlewareService } from "../services"
|
||||
import fs from "fs"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
import glob from "glob"
|
||||
import _ from "lodash"
|
||||
import { createRequireFromPath } from "medusa-core-utils"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
import fs from "fs"
|
||||
import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
|
||||
import glob from "glob"
|
||||
import logger from "./logger"
|
||||
import path from "path"
|
||||
import { trackInstallation } from "medusa-telemetry"
|
||||
import path from "path"
|
||||
import { EntitySchema } from "typeorm"
|
||||
import { MiddlewareService } from "../services"
|
||||
import { getModelExtensionsMap } from "./helpers/get-model-extension-map"
|
||||
import logger from "./logger"
|
||||
|
||||
type Options = {
|
||||
rootDirectory: string
|
||||
@@ -396,6 +394,9 @@ export async function registerServices(
|
||||
registerPaymentServiceFromClass(loaded, context)
|
||||
registerPaymentProcessorFromClass(loaded, context)
|
||||
|
||||
registerFulfillmentServiceFromClass(loaded, context)
|
||||
registerAbstractFulfillmentServiceFromClass(loaded, context)
|
||||
|
||||
if (loaded.prototype instanceof OauthService) {
|
||||
const appDetails = loaded.getAppDetails(pluginDetails.options)
|
||||
|
||||
@@ -412,26 +413,6 @@ export async function registerServices(
|
||||
}
|
||||
),
|
||||
})
|
||||
} else if (loaded.prototype instanceof FulfillmentService) {
|
||||
// Register our payment providers to paymentProviders
|
||||
container.registerAdd(
|
||||
"fulfillmentProviders",
|
||||
asFunction((cradle) => new loaded(cradle, pluginDetails.options), {
|
||||
lifetime: loaded.LIFE_TIME || Lifetime.SINGLETON,
|
||||
})
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which fulfillment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
(cradle) => new loaded(cradle, pluginDetails.options),
|
||||
{
|
||||
lifetime: loaded.LIFE_TIME || Lifetime.SINGLETON,
|
||||
}
|
||||
),
|
||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (isNotificationService(loaded.prototype)) {
|
||||
container.registerAdd(
|
||||
"notificationProviders",
|
||||
|
||||
Reference in New Issue
Block a user