Feat(medusa): Convert fulfillment service to typescript (#1659)

**What**
- convert fulfillment service to typescript


I have removed the `transform` parameter from the getFulfillmentItems_ function since it was not being used with different methods, only `validateFulfillmentLineItem_`. Instead I have just reference the validateFulfillmentLineItem_ function directly. 

We have the same pattern across some different methods, is there a specific reason or just for future proofing?
This commit is contained in:
Philip Korsholm
2022-07-02 09:28:38 +02:00
committed by GitHub
parent fc1cbe72c7
commit 198681f7d8
3 changed files with 189 additions and 110 deletions

View File

@@ -6,13 +6,13 @@ describe("FulfillmentService", () => {
const fulfillmentRepository = MockRepository({})
const fulfillmentProviderService = {
createFulfillment: jest.fn().mockImplementation(data => {
createFulfillment: jest.fn().mockImplementation((data) => {
return Promise.resolve(data)
}),
}
const shippingProfileService = {
retrieve: jest.fn().mockImplementation(data => {
retrieve: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: IdMap.getId("default"),
name: "default_profile",
@@ -22,11 +22,18 @@ describe("FulfillmentService", () => {
}),
}
const lineItemRepository = {
create: jest.fn().mockImplementation((data) => {
return data
}),
}
const fulfillmentService = new FulfillmentService({
manager: MockManager,
fulfillmentProviderService,
fulfillmentRepository,
shippingProfileService,
lineItemRepository,
})
beforeEach(async () => {
@@ -101,7 +108,7 @@ describe("FulfillmentService", () => {
canceled_at: new Date(),
items: [{ item_id: 1, quantity: 2 }],
}),
save: f => f,
save: (f) => f,
})
const lineItemService = {
@@ -111,13 +118,13 @@ describe("FulfillmentService", () => {
Promise.resolve({ id: 1, fulfilled_quantity: 2 })
),
update: jest.fn(),
withTransaction: function() {
withTransaction: function () {
return this
},
}
const fulfillmentProviderService = {
cancelFulfillment: f => f,
cancelFulfillment: (f) => f,
}
const fulfillmentService = new FulfillmentService({
@@ -150,9 +157,9 @@ describe("FulfillmentService", () => {
})
describe("createShipment", () => {
const trackingLinkRepository = MockRepository({ create: c => c })
const trackingLinkRepository = MockRepository({ create: (c) => c })
const fulfillmentRepository = MockRepository({
findOne: q => {
findOne: (q) => {
switch (q.where.id) {
case IdMap.getId("canceled"):
return Promise.resolve({ canceled_at: new Date() })

View File

@@ -1,11 +1,49 @@
import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { ShippingProfileService } from "."
import { TransactionBaseService } from "../interfaces"
import { Fulfillment, LineItem, ShippingMethod } from "../models"
import { FulfillmentRepository } from "../repositories/fulfillment"
import { LineItemRepository } from "../repositories/line-item"
import { TrackingLinkRepository } from "../repositories/tracking-link"
import { FindConfig } from "../types/common"
import {
CreateFulfillmentOrder,
CreateShipmentConfig,
FulfillmentItemPartition,
FulFillmentItemType,
} from "../types/fulfillment"
import { buildQuery } from "../utils"
import FulfillmentProviderService from "./fulfillment-provider"
import LineItemService from "./line-item"
import TotalsService from "./totals"
type InjectedDependencies = {
manager: EntityManager
totalsService: TotalsService
shippingProfileService: ShippingProfileService
lineItemService: LineItemService
fulfillmentProviderService: FulfillmentProviderService
fulfillmentRepository: typeof FulfillmentRepository
trackingLinkRepository: typeof TrackingLinkRepository
lineItemRepository: typeof LineItemRepository
}
/**
* Handles Fulfillments
* @extends BaseService
*/
class FulfillmentService extends BaseService {
class FulfillmentService extends TransactionBaseService<FulfillmentService> {
protected manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly totalsService_: TotalsService
protected readonly lineItemService_: LineItemService
protected readonly shippingProfileService_: ShippingProfileService
protected readonly fulfillmentProviderService_: FulfillmentProviderService
protected readonly fulfillmentRepository_: typeof FulfillmentRepository
protected readonly trackingLinkRepository_: typeof TrackingLinkRepository
protected readonly lineItemRepository_: typeof LineItemRepository
constructor({
manager,
totalsService,
@@ -14,56 +52,33 @@ class FulfillmentService extends BaseService {
shippingProfileService,
lineItemService,
fulfillmentProviderService,
}) {
super()
lineItemRepository,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
/** @private @const {EntityManager} */
this.manager_ = manager
/** @private @const {TotalsService} */
this.lineItemRepository_ = lineItemRepository
this.totalsService_ = totalsService
/** @private @const {FulfillmentRepository} */
this.fulfillmentRepository_ = fulfillmentRepository
/** @private @const {TrackingLinkRepository} */
this.trackingLinkRepository_ = trackingLinkRepository
/** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService
/** @private @const {LineItemService} */
this.lineItemService_ = lineItemService
/** @private @const {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService
}
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new FulfillmentService({
manager: transactionManager,
totalsService: this.totalsService_,
trackingLinkRepository: this.trackingLinkRepository_,
fulfillmentRepository: this.fulfillmentRepository_,
shippingProfileService: this.shippingProfileService_,
lineItemService: this.lineItemService_,
fulfillmentProviderService: this.fulfillmentProviderService_,
})
cloned.transactionManager_ = transactionManager
return cloned
}
partitionItems_(shippingMethods, items) {
const partitioned = []
partitionItems_(
shippingMethods: ShippingMethod[],
items: LineItem[]
): FulfillmentItemPartition[] {
const partitioned: FulfillmentItemPartition[] = []
// partition order items to their dedicated shipping method
for (const method of shippingMethods) {
const temp = { shipping_method: method }
const temp: FulfillmentItemPartition = {
shipping_method: method,
items: [],
}
// for each method find the items in the order, that are associated
// with the profile on the current shipping method
@@ -83,19 +98,22 @@ class FulfillmentService extends BaseService {
/**
* Retrieves the order line items, given an array of items.
* @param {Order} order - the order to get line items from
* @param {{ item_id: string, quantity: number }} items - the items to get
* @param {function} transformer - a function to apply to each of the items
* @param order - the order to get line items from
* @param items - the items to get
* @param transformer - a function to apply to each of the items
* retrieved from the order, should return a line item. If the transformer
* returns an undefined value the line item will be filtered from the
* returned array.
* @return {Promise<Array<LineItem>>} the line items generated by the transformer.
* @return the line items generated by the transformer.
*/
async getFulfillmentItems_(order, items, transformer) {
async getFulfillmentItems_(
order: CreateFulfillmentOrder,
items: FulFillmentItemType[]
): Promise<(LineItem | null)[]> {
const toReturn = await Promise.all(
items.map(async ({ item_id, quantity }) => {
const item = order.items.find((i) => i.id === item_id)
return transformer(item, quantity)
return this.validateFulfillmentLineItem_(item, quantity)
})
)
@@ -107,13 +125,19 @@ class FulfillmentService extends BaseService {
* fulfillable quantity is lower than the requested fulfillment quantity.
* Fulfillable quantity is calculated by subtracting the already fulfilled
* quantity from the quantity that was originally purchased.
* @param {LineItem} item - the line item to check has sufficient fulfillable
* @param item - the line item to check has sufficient fulfillable
* quantity.
* @param {number} quantity - the quantity that is requested to be fulfilled.
* @return {LineItem} a line item that has the requested fulfillment quantity
* @param quantity - the quantity that is requested to be fulfilled.
* @return a line item that has the requested fulfillment quantity
* set.
*/
validateFulfillmentLineItem_(item, quantity) {
validateFulfillmentLineItem_(
item: LineItem | undefined,
quantity: number
): LineItem | null {
const manager = this.transactionManager_ ?? this.manager_
const lineItemRepo = manager.getCustomRepository(this.lineItemRepository_)
if (!item) {
// This will in most cases be called by a webhook so to ensure that
// things go through smoothly in instances where extra items outside
@@ -127,35 +151,39 @@ class FulfillmentService extends BaseService {
"Cannot fulfill more items than have been purchased"
)
}
return {
return lineItemRepo.create({
...item,
quantity,
}
})
}
/**
* Retrieves a fulfillment by its id.
* @param {string} id - the id of the fulfillment to retrieve
* @param {object} config - optional values to include with fulfillmentRepository query
* @return {Fulfillment} the fulfillment
* @param id - the id of the fulfillment to retrieve
* @param config - optional values to include with fulfillmentRepository query
* @return the fulfillment
*/
async retrieve(id, config = {}) {
const fulfillmentRepository = this.manager_.getCustomRepository(
this.fulfillmentRepository_
)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const fulfillment = await fulfillmentRepository.findOne(query)
if (!fulfillment) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Fulfillment with id: ${id} was not found`
async retrieve(
id: string,
config: FindConfig<Fulfillment> = {}
): Promise<Fulfillment> {
return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_
)
}
return fulfillment
const query = buildQuery({ id }, config)
const fulfillment = await fulfillmentRepository.findOne(query)
if (!fulfillment) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Fulfillment with id: ${id} was not found`
)
}
return fulfillment
})
}
/**
@@ -163,27 +191,30 @@ class FulfillmentService extends BaseService {
* If items needs to be fulfilled by different provider, we make
* sure to partition those items, and create fulfillment for
* those partitions.
* @param {Order} order - order to create fulfillment for
* @param {{ item_id: string, quantity: number}[]} itemsToFulfill - the items in the order to fulfill
* @param {object} custom - potential custom values to add
* @return {Fulfillment[]} the created fulfillments
* @param order - order to create fulfillment for
* @param itemsToFulfill - the items in the order to fulfill
* @param custom - potential custom values to add
* @return the created fulfillments
*/
async createFulfillment(order, itemsToFulfill, custom = {}) {
return this.atomicPhase_(async (manager) => {
async createFulfillment(
order: CreateFulfillmentOrder,
itemsToFulfill: FulFillmentItemType[],
custom: Partial<Fulfillment> = {}
): Promise<Fulfillment[]> {
return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_
)
const lineItems = await this.getFulfillmentItems_(
order,
itemsToFulfill,
this.validateFulfillmentLineItem_
)
const lineItems = await this.getFulfillmentItems_(order, itemsToFulfill)
const { shipping_methods } = order
// partition order items to their dedicated shipping method
const fulfillments = this.partitionItems_(shipping_methods, lineItems)
const fulfillments = this.partitionItems_(
shipping_methods,
lineItems as LineItem[]
)
const created = await Promise.all(
fulfillments.map(async ({ shipping_method, items }) => {
@@ -216,16 +247,19 @@ class FulfillmentService extends BaseService {
* Cancels a fulfillment with the fulfillment provider. Will decrement the
* fulfillment_quantity on the line items associated with the fulfillment.
* Throws if the fulfillment has already been shipped.
* @param {Fulfillment|string} fulfillmentOrId - the fulfillment object or id.
* @return {Promise} the result of the save operation
* @param fulfillmentOrId - the fulfillment object or id.
* @return the result of the save operation
*
*/
cancelFulfillment(fulfillmentOrId) {
return this.atomicPhase_(async (manager) => {
let id = fulfillmentOrId
if (typeof fulfillmentOrId === "object") {
id = fulfillmentOrId.id
}
async cancelFulfillment(
fulfillmentOrId: Fulfillment | string
): Promise<Fulfillment> {
return await this.atomicPhase_(async (manager) => {
const id =
typeof fulfillmentOrId === "string"
? fulfillmentOrId
: fulfillmentOrId.id
const fulfillment = await this.retrieve(id, {
relations: ["items", "claim_order", "swap"],
})
@@ -262,22 +296,22 @@ class FulfillmentService extends BaseService {
/**
* Creates a shipment by marking a fulfillment as shipped. Adds
* tracking links and potentially more metadata.
* @param {Order} fulfillmentId - the fulfillment to ship
* @param {TrackingLink[]} trackingLinks - tracking links for the shipment
* @param {object} config - potential configuration settings, such as no_notification and metadata
* @return {Fulfillment} the shipped fulfillment
* @param fulfillmentId - the fulfillment to ship
* @param trackingLinks - tracking links for the shipment
* @param config - potential configuration settings, such as no_notification and metadata
* @return the shipped fulfillment
*/
async createShipment(
fulfillmentId,
trackingLinks,
config = {
fulfillmentId: string,
trackingLinks: { tracking_number: string }[],
config: CreateShipmentConfig = {
metadata: {},
no_notification: undefined,
}
) {
): Promise<Fulfillment> {
const { metadata, no_notification } = config
return this.atomicPhase_(async (manager) => {
return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_
)
@@ -303,7 +337,7 @@ class FulfillmentService extends BaseService {
trackingLinkRepo.create(tl)
)
if (no_notification) {
if (typeof no_notification !== "undefined") {
fulfillment.no_notification = no_notification
}
@@ -312,8 +346,7 @@ class FulfillmentService extends BaseService {
...metadata,
}
const updated = fulfillmentRepository.save(fulfillment)
return updated
return await fulfillmentRepository.save(fulfillment)
})
}
}

View File

@@ -0,0 +1,39 @@
import {
Address,
ClaimOrder,
Discount,
LineItem,
Order,
Payment,
ShippingMethod,
} from "../models"
export type FulFillmentItemType = {
item_id: string
quantity: number
}
export type FulfillmentItemPartition = {
shipping_method: ShippingMethod
items: LineItem[]
}
export type CreateShipmentConfig = {
metadata: Record<string, unknown>
no_notification?: boolean
}
export type CreateFulfillmentOrder = Omit<ClaimOrder, "beforeInsert"> & {
is_claim?: boolean
email?: string
payments: Payment[]
discounts: Discount[]
currency_code: string
tax_rate: number | null
region_id: string
display_id: number
billing_address: Address
items: LineItem[]
shipping_methods: ShippingMethod[]
no_notification: boolean
}