From 99ad43bf47c3922f391d433448b1c4affd88f457 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 18 Feb 2021 16:59:19 +0100 Subject: [PATCH] feat(medusa): tracking links (#177) * fix: creates tracking links on fulfillments * fix: typo --- .../api/src/services/test-not.js | 19 ++ .../__tests__/webshipper-fulfillment.js | 211 ++++++++++++++++++ .../src/services/webshipper-fulfillment.js | 17 +- .../admin/orders/__tests__/get-order.js | 2 + .../admin/orders/create-claim-shipment.js | 2 +- .../routes/admin/orders/create-shipment.js | 2 +- .../admin/orders/create-swap-shipment.js | 2 +- .../src/api/routes/admin/orders/index.js | 3 + .../1613656135167-tracking_links.ts | 16 ++ packages/medusa/src/models/fulfillment.ts | 8 + packages/medusa/src/models/tracking-link.ts | 63 ++++++ .../medusa/src/repositories/tracking-link.ts | 5 + .../src/services/__tests__/fulfillment.js | 9 +- .../medusa/src/services/__tests__/order.js | 4 +- packages/medusa/src/services/claim.js | 4 +- packages/medusa/src/services/fulfillment.js | 18 +- packages/medusa/src/services/order.js | 6 +- packages/medusa/src/services/swap.js | 6 +- 18 files changed, 371 insertions(+), 26 deletions(-) create mode 100644 integration-tests/api/src/services/test-not.js create mode 100644 packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js create mode 100644 packages/medusa/src/migrations/1613656135167-tracking_links.ts create mode 100644 packages/medusa/src/models/tracking-link.ts create mode 100644 packages/medusa/src/repositories/tracking-link.ts diff --git a/integration-tests/api/src/services/test-not.js b/integration-tests/api/src/services/test-not.js new file mode 100644 index 0000000000..77f3d9340e --- /dev/null +++ b/integration-tests/api/src/services/test-not.js @@ -0,0 +1,19 @@ +import { NotificationService } from "medusa-interfaces"; + +class TestNotiService extends NotificationService { + static identifier = "test-not"; + + constructor() { + super(); + } + + async sendNotification() { + return Promise.resolve(); + } + + async resendNotification() { + return Promise.resolve(); + } +} + +export default TestNotiService; diff --git a/packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js b/packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js new file mode 100644 index 0000000000..71cbe37e95 --- /dev/null +++ b/packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js @@ -0,0 +1,211 @@ +import WebshipperFulfillmentService from "../webshipper-fulfillment" + +describe("WebshipperFulfillmentService", () => { + const orderService = { + createShipment: jest.fn(), + } + const swapService = { + createShipment: jest.fn(), + } + const claimService = { + createShipment: jest.fn(), + } + + describe("handleWebhook", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("creates an order shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "order_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(claimService.createShipment).toHaveBeenCalledTimes(0) + expect(swapService.createShipment).toHaveBeenCalledTimes(0) + + expect(orderService.createShipment).toHaveBeenCalledTimes(1) + expect(orderService.createShipment).toHaveBeenCalledWith( + "order_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + + it("creates a claim shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "claim_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(orderService.createShipment).toHaveBeenCalledTimes(0) + expect(swapService.createShipment).toHaveBeenCalledTimes(0) + + expect(claimService.createShipment).toHaveBeenCalledTimes(1) + expect(claimService.createShipment).toHaveBeenCalledWith( + "claim_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + + it("creates a swap shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "swap_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(orderService.createShipment).toHaveBeenCalledTimes(0) + expect(claimService.createShipment).toHaveBeenCalledTimes(0) + + expect(swapService.createShipment).toHaveBeenCalledTimes(1) + expect(swapService.createShipment).toHaveBeenCalledWith( + "swap_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + }) +}) diff --git a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js index 43d96fd573..3504b906bc 100644 --- a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js +++ b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js @@ -363,9 +363,10 @@ class WebshipperFulfillmentService extends FulfillmentService { body.data.relationships.order ) if (wsOrder.data && wsOrder.data.attributes.ext_ref) { - const trackingNumbers = body.data.attributes.tracking_links.map( - (l) => l.number - ) + const trackingLinks = body.data.attributes.tracking_links.map((l) => ({ + url: l.url, + tracking_number: l.number, + })) const [orderId, fulfillmentIndex] = wsOrder.data.attributes.ext_ref.split( "." ) @@ -375,7 +376,7 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.swapService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { const swap = await this.swapService_.retrieve(orderId.substring(1), { @@ -385,21 +386,21 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.swapService_.createShipment( swap.id, fulfillment.id, - trackingNumbers + trackingLinks ) } } else if (orderId.charAt(0).toLowerCase() === "c") { return this.claimService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { if (fulfillmentIndex.startsWith("ful")) { return this.orderService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { const order = await this.orderService_.retrieve(orderId, { @@ -411,7 +412,7 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.orderService_.createShipment( order.id, fulfillment.id, - trackingNumbers + trackingLinks ) } } diff --git a/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js index 743e5c4f40..eaa0fc0136 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js @@ -10,6 +10,8 @@ const defaultRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", + "fulfillments.items", "returns", "gift_cards", "gift_card_transactions", diff --git a/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js index 840270e91a..d4c6c36a0c 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js @@ -23,7 +23,7 @@ export default async (req, res) => { await claimService.createShipment( claim_id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/create-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-shipment.js index 24e0ebe199..2a5a097641 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-shipment.js @@ -22,7 +22,7 @@ export default async (req, res) => { await orderService.createShipment( id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js index 700e1599e0..a407570f81 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js @@ -23,7 +23,7 @@ export default async (req, res) => { await swapService.createShipment( swap_id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/index.js b/packages/medusa/src/api/routes/admin/orders/index.js index ffbc5023d9..89278a310a 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.js +++ b/packages/medusa/src/api/routes/admin/orders/index.js @@ -188,6 +188,8 @@ export const defaultRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", + "fulfillments.items", "returns", "gift_cards", "gift_card_transactions", @@ -271,6 +273,7 @@ export const allowedRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", "returns", "claims", "swaps", diff --git a/packages/medusa/src/migrations/1613656135167-tracking_links.ts b/packages/medusa/src/migrations/1613656135167-tracking_links.ts new file mode 100644 index 0000000000..4755a24c04 --- /dev/null +++ b/packages/medusa/src/migrations/1613656135167-tracking_links.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class trackingLinks1613656135167 implements MigrationInterface { + name = 'trackingLinks1613656135167' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tracking_link" ("id" character varying NOT NULL, "url" character varying, "tracking_number" character varying NOT NULL, "fulfillment_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "PK_fcfd77feb9012ec2126d7c0bfb6" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "tracking_link" ADD CONSTRAINT "FK_471e9e4c96e02ba209a307db32b" FOREIGN KEY ("fulfillment_id") REFERENCES "fulfillment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tracking_link" DROP CONSTRAINT "FK_471e9e4c96e02ba209a307db32b"`); + await queryRunner.query(`DROP TABLE "tracking_link"`); + } + +} diff --git a/packages/medusa/src/models/fulfillment.ts b/packages/medusa/src/models/fulfillment.ts index e071feef3b..be6f4bb8f3 100644 --- a/packages/medusa/src/models/fulfillment.ts +++ b/packages/medusa/src/models/fulfillment.ts @@ -22,6 +22,7 @@ import { FulfillmentProvider } from "./fulfillment-provider" import { FulfillmentItem } from "./fulfillment-item" import { Swap } from "./swap" import { ClaimOrder } from "./claim-order" +import { TrackingLink } from "./tracking-link" @Entity() export class Fulfillment { @@ -76,6 +77,13 @@ export class Fulfillment { ) items: FulfillmentItem[] + @OneToMany( + () => TrackingLink, + tl => tl.fulfillment, + { cascade: ["insert"] } + ) + tracking_links: TrackingLink[] + @Column({ type: "jsonb", default: [] }) tracking_numbers: string[] diff --git a/packages/medusa/src/models/tracking-link.ts b/packages/medusa/src/models/tracking-link.ts new file mode 100644 index 0000000000..ded8e9bc1e --- /dev/null +++ b/packages/medusa/src/models/tracking-link.ts @@ -0,0 +1,63 @@ +import { + Entity, + Index, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Fulfillment } from "./fulfillment" + +@Entity() +export class TrackingLink { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + url: string + + @Column() + tracking_number: string + + @Column() + fulfillment_id: string + + @ManyToOne( + () => Fulfillment, + ful => ful.tracking_links + ) + @JoinColumn({ name: "fulfillment_id" }) + fulfillment: Fulfillment + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `tlink_${id}` + } +} diff --git a/packages/medusa/src/repositories/tracking-link.ts b/packages/medusa/src/repositories/tracking-link.ts new file mode 100644 index 0000000000..785321a045 --- /dev/null +++ b/packages/medusa/src/repositories/tracking-link.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { TrackingLink } from "../models/tracking-link" + +@EntityRepository(TrackingLink) +export class TrackingLinkRepository extends Repository {} diff --git a/packages/medusa/src/services/__tests__/fulfillment.js b/packages/medusa/src/services/__tests__/fulfillment.js index 2d20deadf0..2c178ad705 100644 --- a/packages/medusa/src/services/__tests__/fulfillment.js +++ b/packages/medusa/src/services/__tests__/fulfillment.js @@ -95,6 +95,7 @@ describe("FulfillmentService", () => { }) describe("createShipment", () => { + const trackingLinkRepository = MockRepository({ create: c => c }) const fulfillmentRepository = MockRepository({ findOne: () => Promise.resolve({ id: IdMap.getId("fulfillment") }), }) @@ -102,6 +103,7 @@ describe("FulfillmentService", () => { const fulfillmentService = new FulfillmentService({ manager: MockManager, fulfillmentRepository, + trackingLinkRepository, }) const now = new Date() @@ -113,14 +115,17 @@ describe("FulfillmentService", () => { it("calls order model functions", async () => { await fulfillmentService.createShipment( IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) expect(fulfillmentRepository.save).toHaveBeenCalledTimes(1) expect(fulfillmentRepository.save).toHaveBeenCalledWith({ id: IdMap.getId("fulfillment"), - tracking_numbers: ["1234", "2345"], + tracking_links: [ + { tracking_number: "1234" }, + { tracking_number: "2345" }, + ], metadata: {}, shipped_at: now, }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index d2c270fdb0..262504ea33 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1182,14 +1182,14 @@ describe("OrderService", () => { await orderService.createShipment( IdMap.getId("test"), IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1) expect(fulfillmentService.createShipment).toHaveBeenCalledWith( IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) diff --git a/packages/medusa/src/services/claim.js b/packages/medusa/src/services/claim.js index f8aa1db3ae..ccc2b663e5 100644 --- a/packages/medusa/src/services/claim.js +++ b/packages/medusa/src/services/claim.js @@ -418,7 +418,7 @@ class ClaimService extends BaseService { }) } - async createShipment(id, fulfillmentId, trackingNumbers, metadata = []) { + async createShipment(id, fulfillmentId, trackingLinks, metadata = []) { return this.atomicPhase_(async manager => { const claim = await this.retrieve(id, { relations: ["additional_items"], @@ -426,7 +426,7 @@ class ClaimService extends BaseService { const shipment = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) claim.fulfillment_status = "shipped" diff --git a/packages/medusa/src/services/fulfillment.js b/packages/medusa/src/services/fulfillment.js index 0548ff2510..b05363978d 100644 --- a/packages/medusa/src/services/fulfillment.js +++ b/packages/medusa/src/services/fulfillment.js @@ -11,6 +11,7 @@ class FulfillmentService extends BaseService { manager, totalsService, fulfillmentRepository, + trackingLinkRepository, shippingProfileService, lineItemService, fulfillmentProviderService, @@ -26,6 +27,9 @@ class FulfillmentService extends BaseService { /** @private @const {FulfillmentRepository} */ this.fulfillmentRepository_ = fulfillmentRepository + /** @private @const {TrackingLinkRepository} */ + this.trackingLinkRepository_ = trackingLinkRepository + /** @private @const {ShippingProfileService} */ this.shippingProfileService_ = shippingProfileService @@ -44,6 +48,7 @@ class FulfillmentService extends BaseService { const cloned = new FulfillmentService({ manager: transactionManager, totalsService: this.totalsService_, + trackingLinkRepository: this.trackingLinkRepository_, fulfillmentRepository: this.fulfillmentRepository_, shippingProfileService: this.shippingProfileService_, lineItemService: this.lineItemService_, @@ -235,15 +240,18 @@ class FulfillmentService extends BaseService { * Creates a shipment by marking a fulfillment as shipped. Adds * tracking numbers and potentially more metadata. * @param {Order} fulfillmentId - the fulfillment to ship - * @param {string[]} trackingNumbers - tracking numbers for the shipment + * @param {TrackingLink[]} trackingNumbers - tracking numbers for the shipment * @param {object} metadata - potential metadata to add * @return {Fulfillment} the shipped fulfillment */ - async createShipment(fulfillmentId, trackingNumbers, metadata) { + async createShipment(fulfillmentId, trackingLinks, metadata) { return this.atomicPhase_(async manager => { const fulfillmentRepository = manager.getCustomRepository( this.fulfillmentRepository_ ) + const trackingLinkRepo = manager.getCustomRepository( + this.trackingLinkRepository_ + ) const fulfillment = await this.retrieve(fulfillmentId, { relations: ["items"], @@ -251,7 +259,11 @@ class FulfillmentService extends BaseService { const now = new Date() fulfillment.shipped_at = now - fulfillment.tracking_numbers = trackingNumbers + + fulfillment.tracking_links = trackingLinks.map(tl => + trackingLinkRepo.create(tl) + ) + fulfillment.metadata = { ...fulfillment.metadata, ...metadata, diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index 4f37c5a030..aee1438081 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -553,13 +553,13 @@ class OrderService extends BaseService { * have been created in regards to the shipment. * @param {string} orderId - the id of the order that has been shipped * @param {string} fulfillmentId - the fulfillment that has now been shipped - * @param {Array} trackingNumbers - array of tracking numebers + * @param {TrackingLink[]} trackingLinks - array of tracking numebers * associated with the shipment * @param {Dictionary} metadata - optional metadata to add to * the fulfillment * @return {order} the resulting order following the update. */ - async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) { + async createShipment(orderId, fulfillmentId, trackingLinks, metadata = {}) { return this.atomicPhase_(async manager => { const order = await this.retrieve(orderId, { relations: ["items"] }) const shipment = await this.fulfillmentService_.retrieve(fulfillmentId) @@ -573,7 +573,7 @@ class OrderService extends BaseService { const shipmentRes = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) order.fulfillment_status = "shipped" for (const item of order.items) { diff --git a/packages/medusa/src/services/swap.js b/packages/medusa/src/services/swap.js index bc97b3aff4..5d06abfd1e 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -671,12 +671,12 @@ class SwapService extends BaseService { * @param {string} swapId - the id of the swap that has been shipped. * @param {string} fulfillmentId - the id of the specific fulfillment that * has been shipped - * @param {Array} trackingNumbers - the tracking numbers associated + * @param {TrackingLink[]} trackingLinks - the tracking numbers associated * with the shipment * @param {object} metadata - optional metadata to attach to the shipment. * @returns {Promise} the updated swap with new fulfillments and status. */ - async createShipment(swapId, fulfillmentId, trackingNumbers, metadata = {}) { + async createShipment(swapId, fulfillmentId, trackingLinks, metadata = {}) { return this.atomicPhase_(async manager => { const swap = await this.retrieve(swapId, { relations: ["additional_items"], @@ -685,7 +685,7 @@ class SwapService extends BaseService { // Update the fulfillment to register const shipment = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) swap.fulfillment_status = "shipped"