diff --git a/.changeset/swift-carpets-count.md b/.changeset/swift-carpets-count.md new file mode 100644 index 0000000000..5df38898bb --- /dev/null +++ b/.changeset/swift-carpets-count.md @@ -0,0 +1,8 @@ +--- +"@medusajs/link-modules": patch +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat: sales channel <> order link diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap index f4bc042560..30f77a12fb 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/__snapshots__/index.js.snap @@ -425,7 +425,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -753,7 +755,9 @@ Object { exports[`medusa-plugin-sendgrid order canceled data 1`] = ` Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": Any, @@ -982,7 +986,9 @@ Object { exports[`medusa-plugin-sendgrid order placed data 1`] = ` Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": null, @@ -1236,7 +1242,9 @@ Object { }, "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address": null, "billing_address_id": null, "canceled_at": null, @@ -1612,7 +1620,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -2078,7 +2088,9 @@ Object { ], "locale": null, "order": Object { + "afterLoad": [Function], "beforeInsert": [Function], + "beforeUpdate": [Function], "billing_address_id": null, "canceled_at": null, "cart_id": null, @@ -2533,4 +2545,4 @@ Object { "tracking_links": Array [], "tracking_number": "", } -`; \ No newline at end of file +`; diff --git a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js index 06f628ceb7..bc29f61351 100644 --- a/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js +++ b/integration-tests/plugins/__tests__/medusa-plugin-sendgrid/index.js @@ -4,10 +4,7 @@ const { startBootstrapApp, } = require("../../../environment-helpers/bootstrap-app") const { initDb, useDb } = require("../../../environment-helpers/use-db") -const { - useApi, - useExpressServer, -} = require("../../../environment-helpers/use-api") +const { useApi } = require("../../../environment-helpers/use-api") const adminSeeder = require("../../../helpers/admin-seeder") diff --git a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts index 72ecdcc1aa..31d029ab9e 100644 --- a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts +++ b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts @@ -38,14 +38,13 @@ export async function detachSalesChannelFromProducts({ if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { const remoteLink = container.resolve("remoteLink") - const promises: Promise[] = [] for (const [ salesChannelId, productIds, ] of salesChannelIdProductIdsMap.entries()) { - productIds.forEach((id) => - promises.push( + await promiseAll( + productIds.map((id) => remoteLink.dismiss({ [Modules.PRODUCT]: { product_id: id, @@ -57,8 +56,6 @@ export async function detachSalesChannelFromProducts({ ) ) } - - return } else { await promiseAll( Array.from(salesChannelIdProductIdsMap.entries()).map( diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 3d18bac1ba..14cf0ca3d5 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -4,3 +4,4 @@ export * from "./product-variant-price-set" export * from "./product-shipping-profile" export * from "./product-sales-channel" export * from "./cart-sales-channel" +export * from "./order-sales-channel" diff --git a/packages/link-modules/src/definitions/order-sales-channel.ts b/packages/link-modules/src/definitions/order-sales-channel.ts new file mode 100644 index 0000000000..5ddc4678b1 --- /dev/null +++ b/packages/link-modules/src/definitions/order-sales-channel.ts @@ -0,0 +1,66 @@ +import { ModuleJoinerConfig } from "@medusajs/types" + +import { LINKS } from "../links" + +export const OrderSalesChannel: ModuleJoinerConfig = { + serviceName: LINKS.OrderSalesChannel, + isLink: true, + databaseConfig: { + tableName: "order_sales_channel", + idPrefix: "ordersc", + }, + alias: [ + { + name: "order_sales_channel", + }, + { + name: "order_sales_channels", + }, + ], + primaryKeys: ["id", "order_id", "sales_channel_id"], + relationships: [ + { + serviceName: "orderService", + isInternalService: true, + primaryKey: "id", + foreignKey: "order_id", + alias: "order", + }, + { + serviceName: "salesChannelService", + isInternalService: true, + primaryKey: "id", + foreignKey: "sales_channel_id", + alias: "sales_channel", + }, + ], + extends: [ + { + serviceName: "orderService", + fieldAlias: { + sales_channel: "sales_channel_link.sales_channel", + }, + relationship: { + serviceName: LINKS.OrderSalesChannel, + isInternalService: true, + primaryKey: "order_id", + foreignKey: "id", + alias: "sales_channel_link", + }, + }, + { + serviceName: "salesChannelService", + fieldAlias: { + orders: "order_link.order", + }, + relationship: { + serviceName: LINKS.OrderSalesChannel, + isInternalService: true, + primaryKey: "sales_channel_id", + foreignKey: "id", + alias: "order_link", + isList: true, + }, + }, + ], +} diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index 117777eae4..c38b0ed715 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -34,4 +34,10 @@ export const LINKS = { "salesChannelService", "sales_channel_id" ), + OrderSalesChannel: composeLinkName( + "orderService", + "order_id", + "salesChannelService", + "sales_channel_id" + ), } diff --git a/packages/medusa/src/joiner-configs/order-service.ts b/packages/medusa/src/joiner-configs/order-service.ts new file mode 100644 index 0000000000..3a5e3dc5fe --- /dev/null +++ b/packages/medusa/src/joiner-configs/order-service.ts @@ -0,0 +1,15 @@ +import { ModuleJoinerConfig } from "@medusajs/types" + +export default { + serviceName: "orderService", + primaryKeys: ["id"], + linkableKeys: { order_id: "Order" }, + alias: [ + { + name: "order", + }, + { + name: "orders", + }, + ], +} as ModuleJoinerConfig diff --git a/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts b/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts new file mode 100644 index 0000000000..d9477d4094 --- /dev/null +++ b/packages/medusa/src/migrations/1701860329931-order-sales-channels-link.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { MedusaV2Flag } from "@medusajs/utils" + +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" + +export const featureFlag = [SalesChannelFeatureFlag.key, MedusaV2Flag.key] + +export class OrderSalesChannelLink1701860329931 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "order_sales_channel" + ( + "id" character varying NOT NULL, + "order_id" character varying NOT NULL, + "sales_channel_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, + CONSTRAINT "order_sales_channel_pk" PRIMARY KEY ("order_id", "sales_channel_id"), + CONSTRAINT "order_sales_channel_order_id_unique" UNIQUE ("order_id") + ); + CREATE INDEX IF NOT EXISTS "IDX_id_order_sales_channel" ON "order_sales_channel" ("id"); + + insert into "order_sales_channel" (id, order_id, sales_channel_id) + (select 'ordersc_' || substr(md5(random()::text), 0, 27), id, sales_channel_id from "order" WHERE sales_channel_id IS NOT NULL); + + ALTER TABLE "order" DROP CONSTRAINT IF EXISTS "FK_6ff7e874f01b478c115fdd462eb"; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "order" + SET "sales_channel_id" = "order_sales_channel"."sales_channel_id" + FROM "order_sales_channel" + WHERE "order"."id" = "order_sales_channel"."order_id"; + + DROP TABLE IF EXISTS "order_sales_channel"; + + ALTER TABLE "order" ADD CONSTRAINT "FK_6ff7e874f01b478c115fdd462eb" FOREIGN KEY ("sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/models/order-sales-channel.ts b/packages/medusa/src/models/order-sales-channel.ts new file mode 100644 index 0000000000..497f464169 --- /dev/null +++ b/packages/medusa/src/models/order-sales-channel.ts @@ -0,0 +1,29 @@ +import { BeforeInsert, Column, Index, PrimaryColumn } from "typeorm" +import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils" + +import { generateEntityId } from "../utils" +import { SoftDeletableEntity } from "../interfaces" +import { FeatureFlagEntity } from "../utils/feature-flag-decorators" + +@FeatureFlagEntity([MedusaV2Flag.key, SalesChannelFeatureFlag.key]) +export class OrderSalesChannel extends SoftDeletableEntity { + @Column() + id: string + + @Index("order_sales_channel_order_id_unique", { + unique: true, + }) + @PrimaryColumn() + order_id: string + + @PrimaryColumn() + sales_channel_id: string + + /** + * @apiIgnore + */ + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "ordersc") + } +} diff --git a/packages/medusa/src/models/order.ts b/packages/medusa/src/models/order.ts index 7891b53748..c5141e4cfb 100644 --- a/packages/medusa/src/models/order.ts +++ b/packages/medusa/src/models/order.ts @@ -1,5 +1,7 @@ import { + AfterLoad, BeforeInsert, + BeforeUpdate, Column, Entity, Generated, @@ -12,7 +14,10 @@ import { OneToOne, } from "typeorm" import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column" -import { FeatureFlagColumn, FeatureFlagDecorators, } from "../utils/feature-flag-decorators" +import { + FeatureFlagColumn, + FeatureFlagDecorators, +} from "../utils/feature-flag-decorators" import { BaseEntity } from "../interfaces/models/base-entity" import { generateEntityId } from "../utils/generate-entity-id" @@ -36,10 +41,11 @@ import { Return } from "./return" import { SalesChannel } from "./sales-channel" import { ShippingMethod } from "./shipping-method" import { Swap } from "./swap" +import { MedusaV2Flag } from "@medusajs/utils" /** * @enum - * + * * The order's status. */ export enum OrderStatus { @@ -48,7 +54,7 @@ export enum OrderStatus { */ PENDING = "pending", /** - * The order is completed, meaning that + * The order is completed, meaning that * the items have been fulfilled and the payment * has been captured. */ @@ -69,7 +75,7 @@ export enum OrderStatus { /** * @enum - * + * * The order's fulfillment status. */ export enum FulfillmentStatus { @@ -78,7 +84,7 @@ export enum FulfillmentStatus { */ NOT_FULFILLED = "not_fulfilled", /** - * Some of the order's items, but not all, are fulfilled. + * Some of the order's items, but not all, are fulfilled. */ PARTIALLY_FULFILLED = "partially_fulfilled", /** @@ -113,7 +119,7 @@ export enum FulfillmentStatus { /** * @enum - * + * * The order's payment status. */ export enum PaymentStatus { @@ -321,6 +327,25 @@ export class Order extends BaseEntity { ]) sales_channel: SalesChannel + @FeatureFlagDecorators( + [MedusaV2Flag.key, "sales_channels"], + [ + ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }), + JoinTable({ + name: "order_sales_channel", + joinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + }), + ] + ) + sales_channels?: SalesChannel[] + // Total fields shipping_total: number shipping_tax_total: number | null @@ -345,6 +370,12 @@ export class Order extends BaseEntity { private async beforeInsert(): Promise { this.id = generateEntityId(this.id, "order") + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] + } + if (process.env.NODE_ENV === "development" && !this.display_id) { const disId = await manualAutoIncrement("order") @@ -353,6 +384,30 @@ export class Order extends BaseEntity { } } } + + /** + * @apiIgnore + */ + @BeforeUpdate() + private beforeUpdate(): void { + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] + } + } + + /** + * @apiIgnore + */ + @AfterLoad() + private afterLoad(): void { + if (this.sales_channels) { + this.sales_channel = this.sales_channels?.[0] + this.sales_channel_id = this.sales_channel?.id + delete this.sales_channels + } + } } /** diff --git a/packages/medusa/src/models/sales-channel.ts b/packages/medusa/src/models/sales-channel.ts index b7fcea59b7..6a71b6ad26 100644 --- a/packages/medusa/src/models/sales-channel.ts +++ b/packages/medusa/src/models/sales-channel.ts @@ -1,15 +1,16 @@ import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm" -import { MedusaV2Flag } from "@medusajs/utils" import { FeatureFlagDecorators, FeatureFlagEntity, } from "../utils/feature-flag-decorators" +import { MedusaV2Flag } from "@medusajs/utils" import { SoftDeletableEntity } from "../interfaces" import { DbAwareColumn, generateEntityId } from "../utils" import { SalesChannelLocation } from "./sales-channel-location" import { Product } from "./product" import { Cart } from "./cart" +import { Order } from "./order" @FeatureFlagEntity("sales_channels") export class SalesChannel extends SoftDeletableEntity { @@ -55,6 +56,24 @@ export class SalesChannel extends SoftDeletableEntity { ]) carts: Cart[] + @FeatureFlagDecorators(MedusaV2Flag.key, + [ + ManyToMany(() => Order), + JoinTable({ + name: "order_sales_channel", + joinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "order_id", + referencedColumnName: "id", + }, + }), + ] + ) + orders: Order[] + @OneToMany( () => SalesChannelLocation, (scLocation) => scLocation.sales_channel, diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index d51d3f2fda..3e28b46b24 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1,4 +1,5 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { FlagRouter } from "@medusajs/utils" import { LineItemServiceMock } from "../__mocks__/line-item" import { newTotalsServiceMock } from "../__mocks__/new-totals" import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory" @@ -151,6 +152,7 @@ describe("OrderService", () => { eventBusService, cartService, productVariantInventoryService, + featureFlagRouter: new FlagRouter({}), }) beforeEach(async () => { diff --git a/packages/medusa/src/services/order.ts b/packages/medusa/src/services/order.ts index 278ceaccaa..c72337c8d8 100644 --- a/packages/medusa/src/services/order.ts +++ b/packages/medusa/src/services/order.ts @@ -5,6 +5,7 @@ import { FlagRouter, isDefined, MedusaError, + MedusaV2Flag, promiseAll, selectorConstraintsToString, } from "@medusajs/utils" import { @@ -64,6 +65,7 @@ import { TotalsContext, UpdateOrderInput } from "../types/orders" import { CreateShippingMethodDto } from "../types/shipping-options" import { buildQuery, isString, setMetadata } from "../utils" import EventBusService from "./event-bus" +import { RemoteLink } from "@medusajs/modules-sdk" export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists" @@ -90,6 +92,7 @@ type InjectedDependencies = { eventBusService: EventBusService featureFlagRouter: FlagRouter productVariantInventoryService: ProductVariantInventoryService + remoteLink: RemoteLink } class OrderService extends TransactionBaseService { @@ -132,6 +135,7 @@ class OrderService extends TransactionBaseService { protected readonly inventoryService_: IInventoryService protected readonly eventBus_: EventBusService protected readonly featureFlagRouter_: FlagRouter + protected remoteLink_: RemoteLink // eslint-disable-next-line max-len protected readonly productVariantInventoryService_: ProductVariantInventoryService @@ -150,6 +154,7 @@ class OrderService extends TransactionBaseService { taxProviderService, regionService, cartService, + remoteLink, addressRepository, giftCardService, draftOrderService, @@ -180,6 +185,7 @@ class OrderService extends TransactionBaseService { this.draftOrderService_ = draftOrderService this.featureFlagRouter_ = featureFlagRouter this.productVariantInventoryService_ = productVariantInventoryService + this.remoteLink_ = remoteLink } /** @@ -693,7 +699,8 @@ class OrderService extends TransactionBaseService { if ( cart.sales_channel_id && - this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) + this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) && + !this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key) ) { toCreate.sales_channel_id = cart.sales_channel_id } @@ -710,6 +717,22 @@ class OrderService extends TransactionBaseService { const rawOrder = orderRepo.create(toCreate) const order = await orderRepo.save(rawOrder) + if ( + this.featureFlagRouter_.isFeatureEnabled([ + SalesChannelFeatureFlag.key, + MedusaV2Flag.key, + ]) + ) { + await this.remoteLink_.create({ + orderService: { + order_id: order.id, + }, + salesChannelService: { + sales_channel_id: cart.sales_channel_id as string, + }, + }) + } + if (total !== 0 && payment) { await this.paymentProviderService_ .withTransaction(manager) @@ -2082,7 +2105,7 @@ class OrderService extends TransactionBaseService { relationSet.add("shipping_methods.tax_lines") relationSet.add("region") relationSet.add("payments") - + return Array.from(relationSet.values()) } } diff --git a/packages/medusa/src/utils/feature-flag-decorators.ts b/packages/medusa/src/utils/feature-flag-decorators.ts index 5bf28caaa4..472d14a2d1 100644 --- a/packages/medusa/src/utils/feature-flag-decorators.ts +++ b/packages/medusa/src/utils/feature-flag-decorators.ts @@ -51,7 +51,7 @@ export function FeatureFlagDecorators( } export function FeatureFlagClassDecorators( - featureFlag: string, + featureFlag: string | string[], decorators: ClassDecorator[] ): ClassDecorator { return function (target) {