From 5cfb662ec05fe53166cb6150d7eb92217999d447 Mon Sep 17 00:00:00 2001 From: Sebastian Rindom Date: Thu, 18 Jan 2024 10:38:33 +0100 Subject: [PATCH 1/2] fix: migrate segment subscribers + typescript support (#5904) Fixes #6068. **What** - Uses new Subscriber API. - Adds support for typescript files in Segment plugin. - Adds a peerDependency on @medusajs/medusa for the version that introduced new Subscriber API. --- packages/medusa-plugin-segment/.babelrc | 13 - packages/medusa-plugin-segment/package.json | 23 +- .../src/services/segment.js | 6 +- .../src/subscribers/claim-created.ts | 46 +++ .../src/subscribers/order-canceled.ts | 21 ++ .../src/subscribers/order-items-returned.ts | 82 +++++ .../src/subscribers/order-placed.ts | 87 +++++ .../src/subscribers/order-shipment-created.ts | 60 ++++ .../src/subscribers/order.js | 338 ------------------ .../src/subscribers/swap-created.ts | 24 ++ .../src/subscribers/swap-payment-completed.ts | 36 ++ .../src/subscribers/swap-shipment-created.ts | 41 +++ .../src/subscribers/swap.js | 149 -------- .../src/utils/gather-swap-report.ts | 71 ++++ .../medusa-plugin-segment/tsconfig.admin.json | 8 + packages/medusa-plugin-segment/tsconfig.json | 35 ++ .../tsconfig.server.json | 7 + .../medusa-plugin-segment/tsconfig.spec.json | 5 + yarn.lock | 15 +- 19 files changed, 536 insertions(+), 531 deletions(-) delete mode 100644 packages/medusa-plugin-segment/.babelrc create mode 100644 packages/medusa-plugin-segment/src/subscribers/claim-created.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/order-canceled.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/order-items-returned.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/order-placed.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/order-shipment-created.ts delete mode 100644 packages/medusa-plugin-segment/src/subscribers/order.js create mode 100644 packages/medusa-plugin-segment/src/subscribers/swap-created.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/swap-payment-completed.ts create mode 100644 packages/medusa-plugin-segment/src/subscribers/swap-shipment-created.ts delete mode 100644 packages/medusa-plugin-segment/src/subscribers/swap.js create mode 100644 packages/medusa-plugin-segment/src/utils/gather-swap-report.ts create mode 100644 packages/medusa-plugin-segment/tsconfig.admin.json create mode 100644 packages/medusa-plugin-segment/tsconfig.json create mode 100644 packages/medusa-plugin-segment/tsconfig.server.json create mode 100644 packages/medusa-plugin-segment/tsconfig.spec.json diff --git a/packages/medusa-plugin-segment/.babelrc b/packages/medusa-plugin-segment/.babelrc deleted file mode 100644 index 4d2dfe8f09274..0000000000000 --- a/packages/medusa-plugin-segment/.babelrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "plugins": [ - "@babel/plugin-proposal-class-properties", - "@babel/plugin-transform-instanceof", - "@babel/plugin-transform-classes" - ], - "presets": ["@babel/preset-env"], - "env": { - "test": { - "plugins": ["@babel/plugin-transform-runtime"] - } - } -} diff --git a/packages/medusa-plugin-segment/package.json b/packages/medusa-plugin-segment/package.json index 4598e08d101d0..9b1d5b63dc591 100644 --- a/packages/medusa-plugin-segment/package.json +++ b/packages/medusa-plugin-segment/package.json @@ -14,36 +14,27 @@ "author": "Sebastian Rindom", "license": "MIT", "devDependencies": { - "@babel/cli": "^7.7.5", - "@babel/core": "^7.7.5", - "@babel/node": "^7.7.4", - "@babel/plugin-proposal-class-properties": "^7.7.4", - "@babel/plugin-transform-classes": "^7.9.5", - "@babel/plugin-transform-instanceof": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.7.6", - "@babel/preset-env": "^7.7.5", - "@babel/register": "^7.7.4", - "@babel/runtime": "^7.9.6", "cross-env": "^5.2.1", "eslint": "^6.8.0", "jest": "^25.5.4", "medusa-interfaces": "^1.3.7", - "medusa-test-utils": "^1.1.40" + "medusa-test-utils": "^1.1.40", + "rimraf": "^5.0.1", + "typescript": "^4.9.5" }, "scripts": { - "prepare": "cross-env NODE_ENV=production yarn run build", + "prepublishOnly": "cross-env NODE_ENV=production tsc --build", "test": "jest --passWithNoTests src", - "build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'", - "watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'" + "build": "rimraf dist && tsc -p ./tsconfig.server.json", + "watch": "tsc --watch" }, "peerDependencies": { + "@medusajs/medusa": ">= 1.18.0 < 2", "medusa-interfaces": "^1.3.7" }, "dependencies": { "analytics-node": "^3.4.0-beta.1", "axios": "^0.19.2", - "body-parser": "^1.19.0", - "express": "^4.17.1", "medusa-core-utils": "^1.2.0" }, "gitHead": "cd1f5afa5aa8c0b15ea957008ee19f1d695cbd2e", diff --git a/packages/medusa-plugin-segment/src/services/segment.js b/packages/medusa-plugin-segment/src/services/segment.js index caaae460e43da..9fa275de4fb7c 100644 --- a/packages/medusa-plugin-segment/src/services/segment.js +++ b/packages/medusa-plugin-segment/src/services/segment.js @@ -78,10 +78,10 @@ class SegmentService extends BaseService { order_id: order.id, email: order.email, region_id: order.region_id, - payment_provider: order.payments.map((p) => p.provider_id).join(","), + payment_provider: order.payments?.map((p) => p.provider_id).join(","), shipping_methods: order.shipping_methods, - shipping_country: order.shipping_address.country_code, - shipping_city: order.shipping_address.city, + shipping_country: order.shipping_address?.country_code, + shipping_city: order.shipping_address?.city, reporting_total: await this.getReportingValue(order.currency_code, total), reporting_subtotal: await this.getReportingValue( order.currency_code, diff --git a/packages/medusa-plugin-segment/src/subscribers/claim-created.ts b/packages/medusa-plugin-segment/src/subscribers/claim-created.ts new file mode 100644 index 0000000000000..01101ffc06619 --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/claim-created.ts @@ -0,0 +1,46 @@ +export default async function handler({ data: { id }, container }) { + const segmentService = container.resolve("segmentService") + const claimService = container.resolve("claimService") + + const claim = await claimService.retrieve(id, { + relations: [ + "order", + "claim_items", + "claim_items.item", + "claim_items.tags", + "claim_items.variant", + ], + }) + + for (const ci of claim.claim_items) { + const price = ci.item.unit_price / 100 + const reporting_price = await segmentService.getReportingValue( + claim.order.currency_code, + price + ) + const event = { + event: "Item Claimed", + userId: claim.order.customer_id, + timestamp: claim.created_at, + properties: { + price, + reporting_price, + order_id: claim.order_id, + claim_id: claim.id, + claim_item_id: ci.id, + type: claim.type, + quantity: ci.quantity, + variant: ci.variant.sku, + product_id: ci.variant.product_id, + reason: ci.reason, + note: ci.note, + tags: ci.tags.map((t) => ({ id: t.id, value: t.value })), + }, + } + await segmentService.track(event) + } +} + +export const config = { + event: "claim.created", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/order-canceled.ts b/packages/medusa-plugin-segment/src/subscribers/order-canceled.ts new file mode 100644 index 0000000000000..daaa23335141d --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/order-canceled.ts @@ -0,0 +1,21 @@ +export default async function handler({ data: { id }, container }) { + const orderService = container.resolve("orderService") + const segmentService = container.resolve("segmentService") + + const order = await orderService.retrieveWithTotals(id) + + const date = new Date() + const orderData = await segmentService.buildOrder(order) + const orderEvent = { + event: "Order Cancelled", + userId: order.customer_id, + properties: orderData, + timestamp: date, + } + + segmentService.track(orderEvent) +} + +export const config = { + event: "order.canceled", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/order-items-returned.ts b/packages/medusa-plugin-segment/src/subscribers/order-items-returned.ts new file mode 100644 index 0000000000000..07012730ea596 --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/order-items-returned.ts @@ -0,0 +1,82 @@ +export default async function handler({ data: { id, return_id }, container }) { + const orderService = container.resolve("orderService") + const returnService = container.resolve("returnService") + const segmentService = container.resolve("segmentService") + + const order = await orderService.retrieveWithTotals(id, { + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "discounts.rule", + "shipping_methods", + "shipping_methods.shipping_option", + "payments", + "fulfillments", + "returns", + "items", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + + const ret = await returnService.retrieve(return_id, { + relations: ["items", "items.reason"], + }) + + const shipping: object[] = [] + if (ret.shipping_method && ret.shipping_method.price) { + shipping.push({ + ...ret.shipping_method, + price: -1 * (ret.shipping_method.price / 100), + }) + } + + let merged = [...order.items] + + // merge items from order with items from order swaps + if (order.swaps && order.swaps.length) { + for (const s of order.swaps) { + merged = [...merged, ...s.additional_items] + } + } + + const toBuildFrom = { + ...order, + shipping_methods: shipping, + items: ret.items.map((i) => { + const li = merged.find((l) => l.id === i.item_id) + if (i.reason) { + li.reason = i.reason + } + + if (i.note) { + li.note = i.note + } + return li + }), + } + + const orderData = await segmentService.buildOrder(toBuildFrom) + const orderEvent = { + event: "Order Refunded", + userId: order.customer_id, + properties: orderData, + timestamp: new Date(), + } + + segmentService.track(orderEvent) +} + +export const config = { + event: "order.items-returned", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/order-placed.ts b/packages/medusa-plugin-segment/src/subscribers/order-placed.ts new file mode 100644 index 0000000000000..044d4637ed6fd --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/order-placed.ts @@ -0,0 +1,87 @@ +export default async function handler({ data: { id }, container }) { + const orderService = container.resolve("orderService") + const cartService = container.resolve("cartService") + const segmentService = container.resolve("segmentService") + const order = await orderService.retrieveWithTotals(id, { + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "discounts.rule", + "shipping_methods", + "shipping_methods.shipping_option", + "payments", + "fulfillments", + "items", + "returns", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + + const eventContext: Record = {} + const integrations: Record = {} + + if (order.cart_id) { + try { + const cart = await cartService.retrieve(order.cart_id, { + select: ["context"], + }) + + if (cart.context) { + if (cart.context.ip) { + eventContext.ip = cart.context.ip + } + + if (cart.context.user_agent) { + eventContext.user_agent = cart.context.user_agent + } + + if (segmentService.options_ && segmentService.options_.use_ga_id) { + if (cart.context.ga_id) { + integrations["Google Analytics"] = { + clientId: cart.context.ga_id, + } + } + } + } + } catch (err) { + console.log(err) + console.warn("Failed to gather context for order") + } + } + + const orderData = await segmentService.buildOrder(order) + const orderEvent = { + event: "Order Completed", + userId: order.customer_id, + properties: orderData, + timestamp: order.created_at, + context: eventContext, + integrations, + } + + segmentService.identify({ + userId: order.customer_id, + traits: { + email: order.email, + firstName: order.shipping_address.first_name, + lastName: order.shipping_address.last_name, + }, + }) + + segmentService.track(orderEvent) +} + +export const config = { + event: "order.placed", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/order-shipment-created.ts b/packages/medusa-plugin-segment/src/subscribers/order-shipment-created.ts new file mode 100644 index 0000000000000..1428acdff4cdb --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/order-shipment-created.ts @@ -0,0 +1,60 @@ +export default async function handler({ + data: { id, fulfillment_id }, + container, +}) { + const orderService = container.resolve("orderService") + const fulfillmentService = container.resolve("fulfillmentService") + const segmentService = container.resolve("segmentService") + + const order = await orderService.retrieveWithTotals(id, { + relations: [ + "customer", + "billing_address", + "shipping_address", + "discounts", + "discounts.rule", + "shipping_methods", + "shipping_methods.shipping_option", + "payments", + "fulfillments", + "returns", + "items", + "gift_cards", + "gift_card_transactions", + "swaps", + "swaps.return_order", + "swaps.payment", + "swaps.shipping_methods", + "swaps.shipping_methods.shipping_option", + "swaps.shipping_address", + "swaps.additional_items", + "swaps.fulfillments", + ], + }) + + const fulfillment = await fulfillmentService.retrieve(fulfillment_id, { + relations: ["items"], + }) + + const toBuildFrom = { + ...order, + provider_id: fulfillment.provider, + items: fulfillment.items.map((i) => + order.items.find((l) => l.id === i.item_id) + ), + } + + const orderData = await segmentService.buildOrder(toBuildFrom) + const orderEvent = { + event: "Order Shipped", + userId: order.customer_id, + properties: orderData, + timestamp: fulfillment.shipped_at, + } + + segmentService.track(orderEvent) +} + +export const config = { + event: "order.shipment_created", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/order.js b/packages/medusa-plugin-segment/src/subscribers/order.js deleted file mode 100644 index 3c13b334a5ce7..0000000000000 --- a/packages/medusa-plugin-segment/src/subscribers/order.js +++ /dev/null @@ -1,338 +0,0 @@ -class OrderSubscriber { - constructor({ - segmentService, - eventBusService, - orderService, - cartService, - claimService, - returnService, - fulfillmentService, - }) { - this.orderService_ = orderService - - this.cartService_ = cartService - - this.returnService_ = returnService - - this.claimService_ = claimService - - this.fulfillmentService_ = fulfillmentService - - // Swaps - // swap.created - // swap.received - // swap.shipment_created - // swap.payment_completed - // swap.payment_captured - // swap.refund_processed - - eventBusService.subscribe( - "order.shipment_created", - async ({ id, fulfillment_id }) => { - const order = await this.orderService_.retrieve(id, { - select: [ - "shipping_total", - "discount_total", - "tax_total", - "refunded_total", - "gift_card_total", - "subtotal", - "total", - ], - relations: [ - "customer", - "billing_address", - "shipping_address", - "discounts", - "discounts.rule", - "shipping_methods", - "shipping_methods.shipping_option", - "payments", - "fulfillments", - "returns", - "items", - "gift_cards", - "gift_card_transactions", - "swaps", - "swaps.return_order", - "swaps.payment", - "swaps.shipping_methods", - "swaps.shipping_methods.shipping_option", - "swaps.shipping_address", - "swaps.additional_items", - "swaps.fulfillments", - ], - }) - - const fulfillment = await this.fulfillmentService_.retrieve( - fulfillment_id, - { - relations: ["items"], - } - ) - - const toBuildFrom = { - ...order, - provider_id: fulfillment.provider, - items: fulfillment.items.map((i) => - order.items.find((l) => l.id === i.item_id) - ), - } - - const orderData = await segmentService.buildOrder(toBuildFrom) - const orderEvent = { - event: "Order Shipped", - userId: order.customer_id, - properties: orderData, - timestamp: fulfillment.shipped_at, - } - - segmentService.track(orderEvent) - } - ) - - eventBusService.subscribe("claim.created", async ({ id }) => { - const claim = await this.claimService_.retrieve(id, { - relations: [ - "order", - "claim_items", - "claim_items.item", - "claim_items.tags", - "claim_items.variant", - ], - }) - - for (const ci of claim.claim_items) { - const price = ci.item.unit_price / 100 - const reporting_price = await segmentService.getReportingValue( - claim.order.currency_code, - price - ) - const event = { - event: "Item Claimed", - userId: claim.order.customer_id, - timestamp: claim.created_at, - properties: { - price, - reporting_price, - order_id: claim.order_id, - claim_id: claim.id, - claim_item_id: ci.id, - type: claim.type, - quantity: ci.quantity, - variant: ci.variant.sku, - product_id: ci.variant.product_id, - reason: ci.reason, - note: ci.note, - tags: ci.tags.map((t) => ({ id: t.id, value: t.value })), - }, - } - await segmentService.track(event) - } - }) - - eventBusService.subscribe( - "order.items_returned", - async ({ id, return_id }) => { - const order = await this.orderService_.retrieve(id, { - select: [ - "shipping_total", - "discount_total", - "tax_total", - "refunded_total", - "gift_card_total", - "subtotal", - "total", - ], - relations: [ - "customer", - "billing_address", - "shipping_address", - "discounts", - "discounts.rule", - "shipping_methods", - "shipping_methods.shipping_option", - "payments", - "fulfillments", - "returns", - "items", - "gift_cards", - "gift_card_transactions", - "swaps", - "swaps.return_order", - "swaps.payment", - "swaps.shipping_methods", - "swaps.shipping_methods.shipping_option", - "swaps.shipping_address", - "swaps.additional_items", - "swaps.fulfillments", - ], - }) - - const ret = await this.returnService_.retrieve(return_id, { - relations: ["items", "items.reason"], - }) - - const shipping = [] - if (ret.shipping_method && ret.shipping_method.price) { - shipping.push({ - ...ret.shipping_method, - price: -1 * (ret.shipping_method.price / 100), - }) - } - - let merged = [...order.items] - - // merge items from order with items from order swaps - if (order.swaps && order.swaps.length) { - for (const s of order.swaps) { - merged = [...merged, ...s.additional_items] - } - } - - const toBuildFrom = { - ...order, - shipping_methods: shipping, - items: ret.items.map((i) => { - const li = merged.find((l) => l.id === i.item_id) - if (i.reason) { - li.reason = i.reason - } - - if (i.note) { - li.note = i.note - } - return li - }), - } - - const orderData = await segmentService.buildOrder(toBuildFrom) - const orderEvent = { - event: "Order Refunded", - userId: order.customer_id, - properties: orderData, - timestamp: new Date(), - } - - segmentService.track(orderEvent) - } - ) - - eventBusService.subscribe("order.canceled", async ({ id }) => { - const order = await this.orderService_.retrieve(id, { - select: [ - "shipping_total", - "discount_total", - "tax_total", - "refunded_total", - "gift_card_total", - "subtotal", - "total", - ], - }) - - const date = new Date() - const orderData = await segmentService.buildOrder(order) - const orderEvent = { - event: "Order Cancelled", - userId: order.customer_id, - properties: orderData, - timestamp: date, - } - - segmentService.track(orderEvent) - }) - - eventBusService.subscribe("order.placed", async ({ id }) => { - const order = await this.orderService_.retrieve(id, { - select: [ - "shipping_total", - "discount_total", - "tax_total", - "refunded_total", - "gift_card_total", - "subtotal", - "total", - ], - relations: [ - "customer", - "billing_address", - "shipping_address", - "discounts", - "discounts.rule", - "shipping_methods", - "shipping_methods.shipping_option", - "payments", - "fulfillments", - "items", - "returns", - "gift_cards", - "gift_card_transactions", - "swaps", - "swaps.return_order", - "swaps.payment", - "swaps.shipping_methods", - "swaps.shipping_methods.shipping_option", - "swaps.shipping_address", - "swaps.additional_items", - "swaps.fulfillments", - ], - }) - - const eventContext = {} - const integrations = {} - - if (order.cart_id) { - try { - const cart = await this.cartService_.retrieve(order.cart_id, { - select: ["context"], - }) - - if (cart.context) { - if (cart.context.ip) { - eventContext.ip = cart.context.ip - } - - if (cart.context.user_agent) { - eventContext.user_agent = cart.context.user_agent - } - - if (segmentService.options_ && segmentService.options_.use_ga_id) { - if (cart.context.ga_id) { - integrations["Google Analytics"] = { - clientId: cart.context.ga_id, - } - } - } - } - } catch (err) { - console.log(err) - console.warn("Failed to gather context for order") - } - } - - const orderData = await segmentService.buildOrder(order) - const orderEvent = { - event: "Order Completed", - userId: order.customer_id, - properties: orderData, - timestamp: order.created_at, - context: eventContext, - integrations, - } - - segmentService.identify({ - userId: order.customer_id, - traits: { - email: order.email, - firstName: order.shipping_address.first_name, - lastName: order.shipping_address.last_name, - }, - }) - - segmentService.track(orderEvent) - }) - } -} - -export default OrderSubscriber diff --git a/packages/medusa-plugin-segment/src/subscribers/swap-created.ts b/packages/medusa-plugin-segment/src/subscribers/swap-created.ts new file mode 100644 index 0000000000000..ca2be09343686 --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/swap-created.ts @@ -0,0 +1,24 @@ +import { gatherSwapReport } from "../utils/gather-swap-report" + +export default async function handler({ data: { id }, container }) { + const swapService = container.resolve("swapService") + const segmentService = container.resolve("segmentService") + const lineItemService = container.resolve("lineItemService") + + const [swap, swapReport] = await gatherSwapReport(id, { + swapService, + segmentService, + lineItemService, + }) + + return await segmentService.track({ + event: "Swap Created", + userId: swap.order.customer_id, + timestamp: swap.created_at, + properties: swapReport, + }) +} + +export const config = { + event: "swap.created", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/swap-payment-completed.ts b/packages/medusa-plugin-segment/src/subscribers/swap-payment-completed.ts new file mode 100644 index 0000000000000..c06ddecfa654e --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/swap-payment-completed.ts @@ -0,0 +1,36 @@ +import { humanizeAmount } from "medusa-core-utils" +import { gatherSwapReport } from "../utils/gather-swap-report" + +export default async function handler({ data: { id }, container }) { + const swapService = container.resolve("swapService") + const segmentService = container.resolve("segmentService") + const lineItemService = container.resolve("lineItemService") + + const [swap, swapReport] = await gatherSwapReport(id, { + swapService, + segmentService, + lineItemService, + }) + + const currency = swapReport.currency + const total = humanizeAmount(swap.difference_due, currency) + const reporting_total = await segmentService.getReportingValue( + currency, + total + ) + + return await segmentService.track({ + event: "Swap Confirmed", + userId: swap.order.customer_id, + timestamp: swap.confirmed_at, + properties: { + reporting_total, + total, + ...swapReport, + }, + }) +} + +export const config = { + event: "swap.payment_completed", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/swap-shipment-created.ts b/packages/medusa-plugin-segment/src/subscribers/swap-shipment-created.ts new file mode 100644 index 0000000000000..06b2a6957a8ed --- /dev/null +++ b/packages/medusa-plugin-segment/src/subscribers/swap-shipment-created.ts @@ -0,0 +1,41 @@ +import { humanizeAmount } from "medusa-core-utils" +import { gatherSwapReport } from "../utils/gather-swap-report" + +export default async function handler({ + data: { id, fulfillment_id }, + container, +}) { + const segmentService = container.resolve("segmentService") + const fulfillmentService = container.resolve("fulfillmentService") + const swapService = container.resolve("swapService") + const lineItemService = container.resolve("lineItemService") + + const [swap, swapReport] = await gatherSwapReport(id, { + swapService, + segmentService, + lineItemService, + }) + const fulfillment = await fulfillmentService.retrieve(fulfillment_id) + + const currency = swapReport.currency + const total = humanizeAmount(swap.difference_due, currency) + const reporting_total = await segmentService.getReportingValue( + currency, + total + ) + + return await segmentService.track({ + event: "Swap Shipped", + userId: swap.order.customer_id, + timestamp: fulfillment.shipped_at, + properties: { + reporting_total, + total, + ...swapReport, + }, + }) +} + +export const config = { + event: "swap.shipment_created", +} diff --git a/packages/medusa-plugin-segment/src/subscribers/swap.js b/packages/medusa-plugin-segment/src/subscribers/swap.js deleted file mode 100644 index 30c83d0b91afd..0000000000000 --- a/packages/medusa-plugin-segment/src/subscribers/swap.js +++ /dev/null @@ -1,149 +0,0 @@ -import { humanizeAmount } from "medusa-core-utils" - -class OrderSubscriber { - constructor({ - segmentService, - eventBusService, - swapService, - lineItemService, - fulfillmentService, - }) { - this.fulfillmentService_ = fulfillmentService - - this.lineItemService_ = lineItemService - - this.swapService_ = swapService - - this.segmentService_ = segmentService - - eventBusService.subscribe( - "swap.shipment_created", - async ({ id, fulfillment_id }) => { - const [swap, swapReport] = await this.gatherSwapReport(id) - const fulfillment = await this.fulfillmentService_.retrieve( - fulfillment_id - ) - - const currency = swapReport.currency - const total = humanizeAmount(swap.difference_due, currency) - const reporting_total = await this.segmentService_.getReportingValue( - currency, - total - ) - - return await segmentService.track({ - event: "Swap Shipped", - userId: swap.order.customer_id, - timestamp: fulfillment.shipped_at, - properties: { - reporting_total, - total, - ...swapReport, - }, - }) - } - ) - - eventBusService.subscribe("swap.payment_completed", async ({ id }) => { - const [swap, swapReport] = await this.gatherSwapReport(id) - - const currency = swapReport.currency - const total = humanizeAmount(swap.difference_due, currency) - const reporting_total = await this.segmentService_.getReportingValue( - currency, - total - ) - - return await segmentService.track({ - event: "Swap Confirmed", - userId: swap.order.customer_id, - timestamp: swap.confirmed_at, - properties: { - reporting_total, - total, - ...swapReport, - }, - }) - }) - - eventBusService.subscribe("swap.created", async ({ id }) => { - const [swap, swapReport] = await this.gatherSwapReport(id) - - return await segmentService.track({ - event: "Swap Created", - userId: swap.order.customer_id, - timestamp: swap.created_at, - properties: swapReport, - }) - }) - } - - async gatherSwapReport(id) { - const swap = await this.swapService_.retrieve(id, { - relations: [ - "order", - "additional_items", - "additional_items.variant", - "return_order", - "return_order.items", - "return_order.shipping_method", - ], - }) - - const currency = swap.order.currency_code - - const newItems = await Promise.all( - swap.additional_items.map(async (i) => { - const price = humanizeAmount(i.unit_price, currency) - const reporting_price = await this.segmentService_.getReportingValue( - currency, - price - ) - - return { - name: i.title, - product_id: i.variant.product_id, - variant: i.variant.sku, - quantity: i.quantity, - price, - reporting_price, - } - }) - ) - - const returnItems = await Promise.all( - swap.return_order.items.map(async (ri) => { - const i = await this.lineItemService_.retrieve(ri.item_id, { - relations: ["variant"], - }) - const price = humanizeAmount(i.unit_price, currency) - const reporting_price = await this.segmentService_.getReportingValue( - currency, - price - ) - - return { - name: i.title, - product_id: i.variant.product_id, - variant: i.variant.sku, - quantity: ri.quantity, - price, - reporting_price, - } - }) - ) - - return [ - swap, - { - swap_id: swap.id, - order_id: swap.order_id, - new_items: newItems, - return_items: returnItems, - currency, - }, - ] - } -} - -export default OrderSubscriber diff --git a/packages/medusa-plugin-segment/src/utils/gather-swap-report.ts b/packages/medusa-plugin-segment/src/utils/gather-swap-report.ts new file mode 100644 index 0000000000000..3055b8e33aedc --- /dev/null +++ b/packages/medusa-plugin-segment/src/utils/gather-swap-report.ts @@ -0,0 +1,71 @@ +import { humanizeAmount } from "medusa-core-utils" + +export async function gatherSwapReport( + id, + { swapService, segmentService, lineItemService } +) { + const swap = await swapService.retrieve(id, { + relations: [ + "order", + "additional_items", + "additional_items.variant", + "return_order", + "return_order.items", + "return_order.shipping_method", + ], + }) + + const currency = swap.order.currency_code + + const newItems = await Promise.all( + swap.additional_items.map(async (i) => { + const price = humanizeAmount(i.unit_price, currency) + const reporting_price = await segmentService.getReportingValue( + currency, + price + ) + + return { + name: i.title, + product_id: i.variant.product_id, + variant: i.variant.sku, + quantity: i.quantity, + price, + reporting_price, + } + }) + ) + + const returnItems = await Promise.all( + swap.return_order.items.map(async (ri) => { + const i = await lineItemService.retrieve(ri.item_id, { + relations: ["variant"], + }) + const price = humanizeAmount(i.unit_price, currency) + const reporting_price = await segmentService.getReportingValue( + currency, + price + ) + + return { + name: i.title, + product_id: i.variant.product_id, + variant: i.variant.sku, + quantity: ri.quantity, + price, + reporting_price, + } + }) + ) + + return [ + swap, + { + swap_id: swap.id, + order_id: swap.order_id, + new_items: newItems, + return_items: returnItems, + currency, + }, + ] +} diff --git a/packages/medusa-plugin-segment/tsconfig.admin.json b/packages/medusa-plugin-segment/tsconfig.admin.json new file mode 100644 index 0000000000000..b109ee6f17f3b --- /dev/null +++ b/packages/medusa-plugin-segment/tsconfig.admin.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext" + }, + "include": ["src/admin"], + "exclude": ["**/*.spec.js"] +} diff --git a/packages/medusa-plugin-segment/tsconfig.json b/packages/medusa-plugin-segment/tsconfig.json new file mode 100644 index 0000000000000..869a718e13d56 --- /dev/null +++ b/packages/medusa-plugin-segment/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "lib": [ + "es5", + "es6", + "es2019" + ], + "target": "es5", + "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling + }, + "include": ["src"], + "exclude": [ + "dist", + "build", + "src/**/__tests__", + "src/**/__mocks__", + "src/**/__fixtures__", + "node_modules", + ".eslintrc.js" + ] +} diff --git a/packages/medusa-plugin-segment/tsconfig.server.json b/packages/medusa-plugin-segment/tsconfig.server.json new file mode 100644 index 0000000000000..0a84ca8c0c8d9 --- /dev/null +++ b/packages/medusa-plugin-segment/tsconfig.server.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */ + }, + "exclude": ["src/admin", "**/*.spec.js"] +} diff --git a/packages/medusa-plugin-segment/tsconfig.spec.json b/packages/medusa-plugin-segment/tsconfig.spec.json new file mode 100644 index 0000000000000..9b6240919113c --- /dev/null +++ b/packages/medusa-plugin-segment/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index e94514da4ab30..a4ac711383bc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37380,27 +37380,18 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-plugin-segment@workspace:packages/medusa-plugin-segment" dependencies: - "@babel/cli": ^7.7.5 - "@babel/core": ^7.7.5 - "@babel/node": ^7.7.4 - "@babel/plugin-proposal-class-properties": ^7.7.4 - "@babel/plugin-transform-classes": ^7.9.5 - "@babel/plugin-transform-instanceof": ^7.8.3 - "@babel/plugin-transform-runtime": ^7.7.6 - "@babel/preset-env": ^7.7.5 - "@babel/register": ^7.7.4 - "@babel/runtime": ^7.9.6 analytics-node: ^3.4.0-beta.1 axios: ^0.19.2 - body-parser: ^1.19.0 cross-env: ^5.2.1 eslint: ^6.8.0 - express: ^4.17.1 jest: ^25.5.4 medusa-core-utils: ^1.2.0 medusa-interfaces: ^1.3.7 medusa-test-utils: ^1.1.40 + rimraf: ^5.0.1 + typescript: ^4.9.5 peerDependencies: + "@medusajs/medusa": ">= 1.18.0 < 2" medusa-interfaces: ^1.3.7 languageName: unknown linkType: soft From 7f7cb2a263c26baf540b05a40ab3732ffeb0c73c Mon Sep 17 00:00:00 2001 From: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:16:15 +0100 Subject: [PATCH 2/2] feat(cart): Shipping methods (#6101) --- .changeset/olive-ads-brake.md | 5 + .../services/cart-module/index.spec.ts | 127 +++++++++++++++-- packages/cart/src/loaders/container.ts | 14 +- packages/cart/src/models/line-item.ts | 6 +- packages/cart/src/models/shipping-method.ts | 9 +- packages/cart/src/repositories/address.ts | 7 +- packages/cart/src/repositories/index.ts | 1 + packages/cart/src/repositories/line-item.ts | 7 +- .../cart/src/repositories/shipping-method.ts | 16 +++ packages/cart/src/services/cart-module.ts | 133 +++++++++++++++++- packages/cart/src/services/cart.ts | 2 +- packages/cart/src/services/index.ts | 1 + packages/cart/src/services/line-item.ts | 9 +- packages/cart/src/services/shipping-method.ts | 23 +++ packages/cart/src/types/index.ts | 1 + packages/cart/src/types/shipping-method.ts | 13 ++ packages/types/src/cart/common.ts | 15 +- packages/types/src/cart/mutations.ts | 39 +++-- packages/types/src/cart/service.ts | 34 +++++ 19 files changed, 431 insertions(+), 31 deletions(-) create mode 100644 .changeset/olive-ads-brake.md create mode 100644 packages/cart/src/repositories/shipping-method.ts create mode 100644 packages/cart/src/services/shipping-method.ts create mode 100644 packages/cart/src/types/shipping-method.ts diff --git a/.changeset/olive-ads-brake.md b/.changeset/olive-ads-brake.md new file mode 100644 index 0000000000000..a5fc56c03b432 --- /dev/null +++ b/.changeset/olive-ads-brake.md @@ -0,0 +1,5 @@ +--- +"@medusajs/types": patch +--- + +feat(cart): Shipping methods diff --git a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts index acbdd99435945..980076263c808 100644 --- a/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts +++ b/packages/cart/integration-tests/__tests__/services/cart-module/index.spec.ts @@ -1,4 +1,5 @@ import { ICartModuleService } from "@medusajs/types" +import { CheckConstraintViolationException } from "@mikro-orm/core" import { initialize } from "../../../../src/initialize" import { DB_URL, MikroOrmWrapper } from "../../../utils" @@ -466,12 +467,9 @@ describe("Cart Module Service", () => { expect(item.title).toBe("test") - const updatedItem = await service.updateLineItems( - item.id, - { - title: "test2", - } - ) + const updatedItem = await service.updateLineItems(item.id, { + title: "test2", + }) expect(updatedItem.title).toBe("test2") }) @@ -519,13 +517,13 @@ describe("Cart Module Service", () => { selector: { cart_id: createdCart.id }, data: { title: "changed-test", - } + }, }, { selector: { id: itemTwo!.id }, data: { title: "changed-other-test", - } + }, }, ]) @@ -603,4 +601,117 @@ describe("Cart Module Service", () => { expect(cart.items?.length).toBe(0) }) }) + + describe("addShippingMethods", () => { + it("should add a shipping method to cart succesfully", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [method] = await service.addShippingMethods(createdCart.id, [ + { + amount: 100, + name: "Test", + }, + ]) + + const cart = await service.retrieve(createdCart.id, { + relations: ["shipping_methods"], + }) + + expect(method.id).toBe(cart.shipping_methods![0].id) + }) + + it("should throw when amount is negative", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const error = await service + .addShippingMethods(createdCart.id, [ + { + amount: -100, + name: "Test", + }, + ]) + .catch((e) => e) + + expect(error.name).toBe(CheckConstraintViolationException.name) + }) + + it("should add multiple shipping methods to multiple carts succesfully", async () => { + let [eurCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + let [usdCart] = await service.create([ + { + currency_code: "usd", + }, + ]) + + const methods = await service.addShippingMethods([ + { + cart_id: eurCart.id, + amount: 100, + name: "Test One", + }, + { + cart_id: usdCart.id, + amount: 100, + name: "Test One", + }, + ]) + + const carts = await service.list( + { id: [eurCart.id, usdCart.id] }, + { relations: ["shipping_methods"] } + ) + + eurCart = carts.find((c) => c.currency_code === "eur")! + usdCart = carts.find((c) => c.currency_code === "usd")! + + const eurMethods = methods.filter((m) => m.cart_id === eurCart.id) + const usdMethods = methods.filter((m) => m.cart_id === usdCart.id) + + expect(eurCart.shipping_methods![0].id).toBe(eurMethods[0].id) + expect(usdCart.shipping_methods![0].id).toBe(usdMethods[0].id) + + expect(eurCart.shipping_methods?.length).toBe(1) + expect(usdCart.shipping_methods?.length).toBe(1) + }) + }) + + describe("removeShippingMethods", () => { + it("should remove a line item succesfully", async () => { + const [createdCart] = await service.create([ + { + currency_code: "eur", + }, + ]) + + const [method] = await service.addShippingMethods(createdCart.id, [ + { + amount: 100, + name: "test", + }, + ]) + + expect(method.id).not.toBe(null) + + await service.removeShippingMethods(method.id) + + const cart = await service.retrieve(createdCart.id, { + relations: ["shipping_methods"], + }) + + expect(cart.shipping_methods?.length).toBe(0) + }) + }) }) diff --git a/packages/cart/src/loaders/container.ts b/packages/cart/src/loaders/container.ts index ae8a1be50edc8..1affad85579f8 100644 --- a/packages/cart/src/loaders/container.ts +++ b/packages/cart/src/loaders/container.ts @@ -20,6 +20,9 @@ export default async ({ container.register({ cartService: asClass(defaultServices.CartService).singleton(), addressService: asClass(defaultServices.AddressService).singleton(), + shippingMethodService: asClass( + defaultServices.ShippingMethodService + ).singleton(), lineItemService: asClass(defaultServices.LineItemService).singleton(), }) @@ -38,7 +41,14 @@ function loadDefaultRepositories({ container }) { container.register({ baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), cartRepository: asClass(defaultRepositories.CartRepository).singleton(), - addressRepository: asClass(defaultRepositories.AddressRepository).singleton(), - lineItemRepository: asClass(defaultRepositories.LineItemRepository).singleton(), + addressRepository: asClass( + defaultRepositories.AddressRepository + ).singleton(), + lineItemRepository: asClass( + defaultRepositories.LineItemRepository + ).singleton(), + shippingMethodRepository: asClass( + defaultRepositories.ShippingMethodRepository + ).singleton(), }) } diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index da54c7046db2f..885de86489e4c 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -94,13 +94,13 @@ export default class LineItem { @Property({ columnType: "jsonb", nullable: true }) variant_option_values?: Record | null - @Property({ columnType: "boolean", default: true }) + @Property({ columnType: "boolean" }) requires_shipping = true - @Property({ columnType: "boolean", default: true }) + @Property({ columnType: "boolean" }) is_discountable = true - @Property({ columnType: "boolean", default: false }) + @Property({ columnType: "boolean" }) is_tax_inclusive = false @Property({ columnType: "numeric", nullable: true }) diff --git a/packages/cart/src/models/shipping-method.ts b/packages/cart/src/models/shipping-method.ts index c51478eefb3e1..9d8eab459c31e 100644 --- a/packages/cart/src/models/shipping-method.ts +++ b/packages/cart/src/models/shipping-method.ts @@ -16,16 +16,20 @@ import ShippingMethodAdjustmentLine from "./shipping-method-adjustment-line" import ShippingMethodTaxLine from "./shipping-method-tax-line" @Entity({ tableName: "cart_shipping_method" }) +@Check({ expression: (columns) => `${columns.amount} >= 0` }) export default class ShippingMethod { @PrimaryKey({ columnType: "text" }) id: string + @Property({ columnType: "text" }) + cart_id: string + @ManyToOne(() => Cart, { onDelete: "cascade", index: "IDX_shipping_method_cart_id", - fieldName: "cart_id", + nullable: true, }) - cart: Cart + cart?: Cart | null @Property({ columnType: "text" }) name: string @@ -34,7 +38,6 @@ export default class ShippingMethod { description?: string | null @Property({ columnType: "numeric", serializer: Number }) - @Check({ expression: "amount >= 0" }) // TODO: Validate that numeric types work with the expression amount: number @Property({ columnType: "boolean" }) diff --git a/packages/cart/src/repositories/address.ts b/packages/cart/src/repositories/address.ts index a480877948237..2fb052cf7fde7 100644 --- a/packages/cart/src/repositories/address.ts +++ b/packages/cart/src/repositories/address.ts @@ -8,4 +8,9 @@ export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory< create: CreateAddressDTO update: UpdateAddressDTO } ->(Address) {} +>(Address) { + constructor(...args: any[]) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/repositories/index.ts b/packages/cart/src/repositories/index.ts index fabd202b2db03..d1bf0c1118c4f 100644 --- a/packages/cart/src/repositories/index.ts +++ b/packages/cart/src/repositories/index.ts @@ -2,4 +2,5 @@ export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" export * from "./address" export * from "./cart" export * from "./line-item" +export * from "./shipping-method" diff --git a/packages/cart/src/repositories/line-item.ts b/packages/cart/src/repositories/line-item.ts index 6741e7ee89927..8ac5e3c8636d9 100644 --- a/packages/cart/src/repositories/line-item.ts +++ b/packages/cart/src/repositories/line-item.ts @@ -8,4 +8,9 @@ export class LineItemRepository extends DALUtils.mikroOrmBaseRepositoryFactory< create: CreateLineItemDTO update: UpdateLineItemDTO } ->(LineItem) {} +>(LineItem) { + constructor(...args: any[]) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/repositories/shipping-method.ts b/packages/cart/src/repositories/shipping-method.ts new file mode 100644 index 0000000000000..2b228ba5a4697 --- /dev/null +++ b/packages/cart/src/repositories/shipping-method.ts @@ -0,0 +1,16 @@ +import { DALUtils } from "@medusajs/utils" +import { ShippingMethod } from "@models" +import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "@types" + +export class ShippingMethodRepository extends DALUtils.mikroOrmBaseRepositoryFactory< + ShippingMethod, + { + create: CreateShippingMethodDTO + update: UpdateShippingMethodDTO + } +>(ShippingMethod) { + constructor(...args: any[]) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index 5a20a5e9eaafa..b60686ddd6a29 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -17,7 +17,7 @@ import { isObject, isString, } from "@medusajs/utils" -import { LineItem } from "@models" +import { LineItem, ShippingMethod } from "@models" import { UpdateLineItemDTO } from "@types" import { joinerConfig } from "../joiner-config" import * as services from "../services" @@ -27,6 +27,7 @@ type InjectedDependencies = { cartService: services.CartService addressService: services.AddressService lineItemService: services.LineItemService + shippingMethodService: services.ShippingMethodService } export default class CartModuleService implements ICartModuleService { @@ -34,6 +35,7 @@ export default class CartModuleService implements ICartModuleService { protected cartService_: services.CartService protected addressService_: services.AddressService protected lineItemService_: services.LineItemService + protected shippingMethodService_: services.ShippingMethodService constructor( { @@ -41,6 +43,7 @@ export default class CartModuleService implements ICartModuleService { cartService, addressService, lineItemService, + shippingMethodService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { @@ -48,6 +51,7 @@ export default class CartModuleService implements ICartModuleService { this.cartService_ = cartService this.addressService_ = addressService this.lineItemService_ = lineItemService + this.shippingMethodService_ = shippingMethodService } __joinerConfig(): ModuleJoinerConfig { @@ -252,6 +256,25 @@ export default class CartModuleService implements ICartModuleService { ) } + @InjectManager("baseRepository_") + async listShippingMethods( + filters: CartTypes.FilterableShippingMethodProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const methods = await this.shippingMethodService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize< + CartTypes.CartShippingMethodDTO[] + >(methods, { + populate: true, + }) + } + addLineItems( data: CartTypes.CreateLineItemForCartDTO ): Promise @@ -527,4 +550,112 @@ export default class CartModuleService implements ICartModuleService { const addressIds = Array.isArray(ids) ? ids : [ids] await this.addressService_.delete(addressIds, sharedContext) } + + async addShippingMethods( + data: CartTypes.CreateShippingMethodDTO + ): Promise + async addShippingMethods( + data: CartTypes.CreateShippingMethodDTO[] + ): Promise + async addShippingMethods( + cartId: string, + methods: CartTypes.CreateShippingMethodDTO[], + sharedContext?: Context + ): Promise + + @InjectManager("baseRepository_") + async addShippingMethods( + cartIdOrData: + | string + | CartTypes.CreateShippingMethodDTO[] + | CartTypes.CreateShippingMethodDTO, + data?: CartTypes.CreateShippingMethodDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise< + CartTypes.CartShippingMethodDTO[] | CartTypes.CartShippingMethodDTO + > { + let methods: ShippingMethod[] = [] + if (isString(cartIdOrData)) { + methods = await this.addShippingMethods_( + cartIdOrData, + data as CartTypes.CreateShippingMethodDTO[], + sharedContext + ) + } else { + const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData] + methods = await this.addShippingMethodsBulk_(data, sharedContext) + } + + return await this.baseRepository_.serialize< + CartTypes.CartShippingMethodDTO[] + >(methods, { + populate: true, + }) + } + + @InjectTransactionManager("baseRepository_") + protected async addShippingMethods_( + cartId: string, + data: CartTypes.CreateShippingMethodDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const cart = await this.retrieve(cartId, { select: ["id"] }, sharedContext) + + const methods = data.map((method) => { + return { + ...method, + cart_id: cart.id, + } + }) + + return await this.addShippingMethodsBulk_(methods, sharedContext) + } + + @InjectTransactionManager("baseRepository_") + protected async addShippingMethodsBulk_( + data: CartTypes.CreateShippingMethodDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return await this.shippingMethodService_.create(data, sharedContext) + } + + async removeShippingMethods( + methodIds: string[], + sharedContext?: Context + ): Promise + async removeShippingMethods( + methodIds: string, + sharedContext?: Context + ): Promise + async removeShippingMethods( + selector: Partial, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async removeShippingMethods( + methodIdsOrSelector: + | string + | string[] + | Partial, + @MedusaContext() sharedContext: Context = {} + ): Promise { + let toDelete: string[] = [] + if (isObject(methodIdsOrSelector)) { + const methods = await this.listShippingMethods( + { + ...(methodIdsOrSelector as Partial), + }, + {}, + sharedContext + ) + + toDelete = methods.map((m) => m.id) + } else { + toDelete = Array.isArray(methodIdsOrSelector) + ? methodIdsOrSelector + : [methodIdsOrSelector] + } + await this.shippingMethodService_.delete(toDelete, sharedContext) + } } diff --git a/packages/cart/src/services/cart.ts b/packages/cart/src/services/cart.ts index e206f557c3637..a9c594555b1ca 100644 --- a/packages/cart/src/services/cart.ts +++ b/packages/cart/src/services/cart.ts @@ -16,7 +16,7 @@ export default class CartService< update: UpdateCartDTO } >(Cart) { - constructor({ cartRepository }: InjectedDependencies) { + constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index a72d00a3361ea..b3c18ffa65e7d 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -2,4 +2,5 @@ export { default as AddressService } from "./address" export { default as CartService } from "./cart" export { default as CartModuleService } from "./cart-module" export { default as LineItemService } from "./line-item" +export { default as ShippingMethodService } from "./shipping-method" diff --git a/packages/cart/src/services/line-item.ts b/packages/cart/src/services/line-item.ts index 0794665e737ed..ec736f4010249 100644 --- a/packages/cart/src/services/line-item.ts +++ b/packages/cart/src/services/line-item.ts @@ -1,7 +1,7 @@ import { DAL } from "@medusajs/types" import { ModulesSdkUtils } from "@medusajs/utils" import { LineItem } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "../types" +import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" type InjectedDependencies = { lineItemRepository: DAL.RepositoryService @@ -15,4 +15,9 @@ export default class LineItemService< create: CreateLineItemDTO update: UpdateLineItemDTO } ->(LineItem) {} +>(LineItem) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/services/shipping-method.ts b/packages/cart/src/services/shipping-method.ts new file mode 100644 index 0000000000000..f3cf6671442ee --- /dev/null +++ b/packages/cart/src/services/shipping-method.ts @@ -0,0 +1,23 @@ +import { DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { ShippingMethod } from "@models" +import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "../types" + +type InjectedDependencies = { + shippingMethodRepository: DAL.RepositoryService +} + +export default class ShippingMethodService< + TEntity extends ShippingMethod = ShippingMethod +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreateShippingMethodDTO + update: UpdateShippingMethodDTO + } +>(ShippingMethod) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/cart/src/types/index.ts b/packages/cart/src/types/index.ts index e9e853846c725..243d7d3c8d49f 100644 --- a/packages/cart/src/types/index.ts +++ b/packages/cart/src/types/index.ts @@ -2,6 +2,7 @@ import { Logger } from "@medusajs/types" export * from "./address" export * from "./cart" export * from "./line-item" +export * from "./shipping-method" export type InitializeModuleInjectableDependencies = { logger?: Logger diff --git a/packages/cart/src/types/shipping-method.ts b/packages/cart/src/types/shipping-method.ts new file mode 100644 index 0000000000000..6e70a4056bc0c --- /dev/null +++ b/packages/cart/src/types/shipping-method.ts @@ -0,0 +1,13 @@ +export interface CreateShippingMethodDTO { + name: string + cart_id: string + amount: number + data?: Record +} + +export interface UpdateShippingMethodDTO { + id: string + name?: string + amount?: number + data?: Record +} diff --git a/packages/types/src/cart/common.ts b/packages/types/src/cart/common.ts index 6c345d59454f3..508bfc8dc7733 100644 --- a/packages/types/src/cart/common.ts +++ b/packages/types/src/cart/common.ts @@ -168,6 +168,11 @@ export interface CartShippingMethodDTO { */ id: string + /** + * The ID of the associated cart + */ + cart_id: string + /** * The name of the shipping method */ @@ -489,8 +494,16 @@ export interface FilterableLineItemProps product_id?: string | string[] } +export interface FilterableShippingMethodProps + extends BaseFilterable { + id?: string | string[] + cart_id?: string | string[] + name?: string + shipping_option_id?: string | string[] +} + /** - * TODO: Remove this in favor of CartDTO, when module is released + * TODO: Remove this in favor of CartDTO, when module is released * @deprecated Use CartDTO instead */ export type legacy_CartDTO = { diff --git a/packages/types/src/cart/mutations.ts b/packages/types/src/cart/mutations.ts index f128d819c911c..294180cd89c52 100644 --- a/packages/types/src/cart/mutations.ts +++ b/packages/types/src/cart/mutations.ts @@ -56,7 +56,7 @@ export interface UpdateCartDTO { metadata?: Record } -export interface CreateLineItemTaxLineDTO { +export interface CreateTaxLineDTO { description?: string tax_rate_id?: string code: string @@ -64,7 +64,7 @@ export interface CreateLineItemTaxLineDTO { provider_id?: string } -export interface CreateLineItemAdjustmentDTO { +export interface CreateAdjustmentDTO { code: string amount: number description?: string @@ -72,7 +72,7 @@ export interface CreateLineItemAdjustmentDTO { provider_id?: string } -export interface UpdateLineItemTaxLineDTO { +export interface UpdateTaxLineDTO { id: string description?: string tax_rate_id?: string @@ -81,7 +81,7 @@ export interface UpdateLineItemTaxLineDTO { provider_id?: string } -export interface UpdateLineItemAdjustmentDTO { +export interface UpdateAdjustmentDTO { id: string code?: string amount?: number @@ -120,8 +120,8 @@ export interface CreateLineItemDTO { compare_at_unit_price?: number unit_price: number - tax_lines?: CreateLineItemTaxLineDTO[] - adjustments?: CreateLineItemAdjustmentDTO[] + tax_lines?: CreateTaxLineDTO[] + adjustments?: CreateAdjustmentDTO[] } export interface CreateLineItemForCartDTO extends CreateLineItemDTO { @@ -144,6 +144,29 @@ export interface UpdateLineItemDTO quantity?: number unit_price?: number - tax_lines?: UpdateLineItemTaxLineDTO[] | CreateLineItemTaxLineDTO[] - adjustments?: UpdateLineItemAdjustmentDTO[] | CreateLineItemAdjustmentDTO[] + tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[] + adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] +} + +export interface CreateShippingMethodDTO { + name: string + + cart_id: string + + amount: number + data?: Record + + tax_lines?: CreateTaxLineDTO[] + adjustments?: CreateAdjustmentDTO[] +} + +export interface UpdateShippingMethodDTO { + id: string + name?: string + + amount?: number + data?: Record + + tax_lines?: UpdateTaxLineDTO[] | CreateTaxLineDTO[] + adjustments?: UpdateAdjustmentDTO[] | CreateAdjustmentDTO[] } diff --git a/packages/types/src/cart/service.ts b/packages/types/src/cart/service.ts index 9f7f413acc3ff..14e6222998d17 100644 --- a/packages/types/src/cart/service.ts +++ b/packages/types/src/cart/service.ts @@ -5,14 +5,17 @@ import { CartAddressDTO, CartDTO, CartLineItemDTO, + CartShippingMethodDTO, FilterableAddressProps, FilterableCartProps, + FilterableShippingMethodProps, } from "./common" import { CreateAddressDTO, CreateCartDTO, CreateLineItemDTO, CreateLineItemForCartDTO, + CreateShippingMethodDTO, UpdateAddressDTO, UpdateCartDTO, UpdateLineItemDTO, @@ -102,4 +105,35 @@ export interface ICartModuleService extends IModuleService { selector: Partial, sharedContext?: Context ): Promise + + listShippingMethods( + filters: FilterableShippingMethodProps, + config: FindConfig, + sharedContext: Context + ): Promise + + addShippingMethods( + data: CreateShippingMethodDTO + ): Promise + addShippingMethods( + data: CreateShippingMethodDTO[] + ): Promise + addShippingMethods( + cartId: string, + methods: CreateShippingMethodDTO[], + sharedContext?: Context + ): Promise + + removeShippingMethods( + methodIds: string[], + sharedContext?: Context + ): Promise + removeShippingMethods( + methodIds: string, + sharedContext?: Context + ): Promise + removeShippingMethods( + selector: Partial, + sharedContext?: Context + ): Promise }