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:
Oli Juhl
2023-09-03 20:05:36 +02:00
committed by GitHub
parent c348263fdb
commit 17d91c276a
7 changed files with 281 additions and 72 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/medusa": patch
"medusa-fulfillment-webshipper": minor
---
[wip] feat(medusa): Add AbstractFulfillmentService

View File

@@ -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(

View 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

View File

@@ -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"

View File

@@ -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>(

View File

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

View File

@@ -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",