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 authored and GitHub committed 2022-07-02 07:28:38 +00:00
1 parent fc1cbe72c7
commit 198681f7d8
3 files changed
+177 -98

No files matched your search

@@ -6,13 +6,13 @@ describe("FulfillmentService", () => {
const fulfillmentRepository = MockRepository({}) const fulfillmentRepository = MockRepository({})
const fulfillmentProviderService = { const fulfillmentProviderService = {
createFulfillment: jest.fn().mockImplementation(data => { createFulfillment: jest.fn().mockImplementation((data) => {
return Promise.resolve(data) return Promise.resolve(data)
}), }),
} }
const shippingProfileService = { const shippingProfileService = {
retrieve: jest.fn().mockImplementation(data => { retrieve: jest.fn().mockImplementation((data) => {
return Promise.resolve({ return Promise.resolve({
id: IdMap.getId("default"), id: IdMap.getId("default"),
name: "default_profile", name: "default_profile",
@@ -22,11 +22,18 @@ describe("FulfillmentService", () => {
}), }),
} }
const lineItemRepository = {
create: jest.fn().mockImplementation((data) => {
return data
}),
}
const fulfillmentService = new FulfillmentService({ const fulfillmentService = new FulfillmentService({
manager: MockManager, manager: MockManager,
fulfillmentProviderService, fulfillmentProviderService,
fulfillmentRepository, fulfillmentRepository,
shippingProfileService, shippingProfileService,
lineItemRepository,
}) })
beforeEach(async () => { beforeEach(async () => {
@@ -101,7 +108,7 @@ describe("FulfillmentService", () => {
canceled_at: new Date(), canceled_at: new Date(),
items: [{ item_id: 1, quantity: 2 }], items: [{ item_id: 1, quantity: 2 }],
}), }),
save: f => f, save: (f) => f,
}) })
const lineItemService = { const lineItemService = {
@@ -111,13 +118,13 @@ describe("FulfillmentService", () => {
Promise.resolve({ id: 1, fulfilled_quantity: 2 }) Promise.resolve({ id: 1, fulfilled_quantity: 2 })
), ),
update: jest.fn(), update: jest.fn(),
withTransaction: function() { withTransaction: function () {
return this return this
}, },
} }
const fulfillmentProviderService = { const fulfillmentProviderService = {
cancelFulfillment: f => f, cancelFulfillment: (f) => f,
} }
const fulfillmentService = new FulfillmentService({ const fulfillmentService = new FulfillmentService({
@@ -150,9 +157,9 @@ describe("FulfillmentService", () => {
}) })
describe("createShipment", () => { describe("createShipment", () => {
const trackingLinkRepository = MockRepository({ create: c => c }) const trackingLinkRepository = MockRepository({ create: (c) => c })
const fulfillmentRepository = MockRepository({ const fulfillmentRepository = MockRepository({
findOne: q => { findOne: (q) => {
switch (q.where.id) { switch (q.where.id) {
case IdMap.getId("canceled"): case IdMap.getId("canceled"):
return Promise.resolve({ canceled_at: new Date() }) return Promise.resolve({ canceled_at: new Date() })
@@ -1,11 +1,49 @@
import { BaseService } from "medusa-interfaces"
import { MedusaError } from "medusa-core-utils" 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 * 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({ constructor({
manager, manager,
totalsService, totalsService,
@@ -14,56 +52,33 @@ class FulfillmentService extends BaseService {
shippingProfileService, shippingProfileService,
lineItemService, lineItemService,
fulfillmentProviderService, fulfillmentProviderService,
}) { lineItemRepository,
super() }: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
/** @private @const {EntityManager} */
this.manager_ = manager this.manager_ = manager
/** @private @const {TotalsService} */ this.lineItemRepository_ = lineItemRepository
this.totalsService_ = totalsService this.totalsService_ = totalsService
/** @private @const {FulfillmentRepository} */
this.fulfillmentRepository_ = fulfillmentRepository this.fulfillmentRepository_ = fulfillmentRepository
/** @private @const {TrackingLinkRepository} */
this.trackingLinkRepository_ = trackingLinkRepository this.trackingLinkRepository_ = trackingLinkRepository
/** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService this.shippingProfileService_ = shippingProfileService
/** @private @const {LineItemService} */
this.lineItemService_ = lineItemService this.lineItemService_ = lineItemService
/** @private @const {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService this.fulfillmentProviderService_ = fulfillmentProviderService
} }
withTransaction(transactionManager) { partitionItems_(
if (!transactionManager) { shippingMethods: ShippingMethod[],
return this items: LineItem[]
} ): FulfillmentItemPartition[] {
const partitioned: FulfillmentItemPartition[] = []
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 = []
// partition order items to their dedicated shipping method // partition order items to their dedicated shipping method
for (const method of shippingMethods) { 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 // for each method find the items in the order, that are associated
// with the profile on the current shipping method // 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. * Retrieves the order line items, given an array of items.
* @param {Order} order - the order to get line items from * @param order - the order to get line items from
* @param {{ item_id: string, quantity: number }} items - the items to get * @param items - the items to get
* @param {function} transformer - a function to apply to each of the items * @param transformer - a function to apply to each of the items
* retrieved from the order, should return a line item. If the transformer * retrieved from the order, should return a line item. If the transformer
* returns an undefined value the line item will be filtered from the * returns an undefined value the line item will be filtered from the
* returned array. * 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( const toReturn = await Promise.all(
items.map(async ({ item_id, quantity }) => { items.map(async ({ item_id, quantity }) => {
const item = order.items.find((i) => i.id === item_id) 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 lower than the requested fulfillment quantity.
* Fulfillable quantity is calculated by subtracting the already fulfilled * Fulfillable quantity is calculated by subtracting the already fulfilled
* quantity from the quantity that was originally purchased. * 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. * quantity.
* @param {number} quantity - the quantity that is requested to be fulfilled. * @param quantity - the quantity that is requested to be fulfilled.
* @return {LineItem} a line item that has the requested fulfillment quantity * @return a line item that has the requested fulfillment quantity
* set. * 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) { if (!item) {
// This will in most cases be called by a webhook so to ensure that // This will in most cases be called by a webhook so to ensure that
// things go through smoothly in instances where extra items outside // things go through smoothly in instances where extra items outside
@@ -127,25 +151,28 @@ class FulfillmentService extends BaseService {
"Cannot fulfill more items than have been purchased" "Cannot fulfill more items than have been purchased"
) )
} }
return { return lineItemRepo.create({
...item, ...item,
quantity, quantity,
} })
} }
/** /**
* Retrieves a fulfillment by its id. * Retrieves a fulfillment by its id.
* @param {string} id - the id of the fulfillment to retrieve * @param id - the id of the fulfillment to retrieve
* @param {object} config - optional values to include with fulfillmentRepository query * @param config - optional values to include with fulfillmentRepository query
* @return {Fulfillment} the fulfillment * @return the fulfillment
*/ */
async retrieve(id, config = {}) { async retrieve(
const fulfillmentRepository = this.manager_.getCustomRepository( id: string,
config: FindConfig<Fulfillment> = {}
): Promise<Fulfillment> {
return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_ this.fulfillmentRepository_
) )
const validatedId = this.validateId_(id) const query = buildQuery({ id }, config)
const query = this.buildQuery_({ id: validatedId }, config)
const fulfillment = await fulfillmentRepository.findOne(query) const fulfillment = await fulfillmentRepository.findOne(query)
@@ -156,6 +183,7 @@ class FulfillmentService extends BaseService {
) )
} }
return fulfillment return fulfillment
})
} }
/** /**
@@ -163,27 +191,30 @@ class FulfillmentService extends BaseService {
* If items needs to be fulfilled by different provider, we make * If items needs to be fulfilled by different provider, we make
* sure to partition those items, and create fulfillment for * sure to partition those items, and create fulfillment for
* those partitions. * those partitions.
* @param {Order} order - order to create fulfillment for * @param order - order to create fulfillment for
* @param {{ item_id: string, quantity: number}[]} itemsToFulfill - the items in the order to fulfill * @param itemsToFulfill - the items in the order to fulfill
* @param {object} custom - potential custom values to add * @param custom - potential custom values to add
* @return {Fulfillment[]} the created fulfillments * @return the created fulfillments
*/ */
async createFulfillment(order, itemsToFulfill, custom = {}) { async createFulfillment(
return this.atomicPhase_(async (manager) => { order: CreateFulfillmentOrder,
itemsToFulfill: FulFillmentItemType[],
custom: Partial<Fulfillment> = {}
): Promise<Fulfillment[]> {
return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository( const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_ this.fulfillmentRepository_
) )
const lineItems = await this.getFulfillmentItems_( const lineItems = await this.getFulfillmentItems_(order, itemsToFulfill)
order,
itemsToFulfill,
this.validateFulfillmentLineItem_
)
const { shipping_methods } = order const { shipping_methods } = order
// partition order items to their dedicated shipping method // 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( const created = await Promise.all(
fulfillments.map(async ({ shipping_method, items }) => { fulfillments.map(async ({ shipping_method, items }) => {
@@ -216,16 +247,19 @@ class FulfillmentService extends BaseService {
* Cancels a fulfillment with the fulfillment provider. Will decrement the * Cancels a fulfillment with the fulfillment provider. Will decrement the
* fulfillment_quantity on the line items associated with the fulfillment. * fulfillment_quantity on the line items associated with the fulfillment.
* Throws if the fulfillment has already been shipped. * Throws if the fulfillment has already been shipped.
* @param {Fulfillment|string} fulfillmentOrId - the fulfillment object or id. * @param fulfillmentOrId - the fulfillment object or id.
* @return {Promise} the result of the save operation * @return the result of the save operation
* *
*/ */
cancelFulfillment(fulfillmentOrId) { async cancelFulfillment(
return this.atomicPhase_(async (manager) => { fulfillmentOrId: Fulfillment | string
let id = fulfillmentOrId ): Promise<Fulfillment> {
if (typeof fulfillmentOrId === "object") { return await this.atomicPhase_(async (manager) => {
id = fulfillmentOrId.id const id =
} typeof fulfillmentOrId === "string"
? fulfillmentOrId
: fulfillmentOrId.id
const fulfillment = await this.retrieve(id, { const fulfillment = await this.retrieve(id, {
relations: ["items", "claim_order", "swap"], relations: ["items", "claim_order", "swap"],
}) })
@@ -262,22 +296,22 @@ class FulfillmentService extends BaseService {
/** /**
* Creates a shipment by marking a fulfillment as shipped. Adds * Creates a shipment by marking a fulfillment as shipped. Adds
* tracking links and potentially more metadata. * tracking links and potentially more metadata.
* @param {Order} fulfillmentId - the fulfillment to ship * @param fulfillmentId - the fulfillment to ship
* @param {TrackingLink[]} trackingLinks - tracking links for the shipment * @param trackingLinks - tracking links for the shipment
* @param {object} config - potential configuration settings, such as no_notification and metadata * @param config - potential configuration settings, such as no_notification and metadata
* @return {Fulfillment} the shipped fulfillment * @return the shipped fulfillment
*/ */
async createShipment( async createShipment(
fulfillmentId, fulfillmentId: string,
trackingLinks, trackingLinks: { tracking_number: string }[],
config = { config: CreateShipmentConfig = {
metadata: {}, metadata: {},
no_notification: undefined, no_notification: undefined,
} }
) { ): Promise<Fulfillment> {
const { metadata, no_notification } = config const { metadata, no_notification } = config
return this.atomicPhase_(async (manager) => { return await this.atomicPhase_(async (manager) => {
const fulfillmentRepository = manager.getCustomRepository( const fulfillmentRepository = manager.getCustomRepository(
this.fulfillmentRepository_ this.fulfillmentRepository_
) )
@@ -303,7 +337,7 @@ class FulfillmentService extends BaseService {
trackingLinkRepo.create(tl) trackingLinkRepo.create(tl)
) )
if (no_notification) { if (typeof no_notification !== "undefined") {
fulfillment.no_notification = no_notification fulfillment.no_notification = no_notification
} }
@@ -312,8 +346,7 @@ class FulfillmentService extends BaseService {
...metadata, ...metadata,
} }
const updated = fulfillmentRepository.save(fulfillment) return await fulfillmentRepository.save(fulfillment)
return updated
}) })
} }
} }
+39
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
}