diff --git a/integration-tests/plugins/__tests__/cart/store/get-cart.ts b/integration-tests/plugins/__tests__/cart/store/get-cart.ts new file mode 100644 index 0000000000000..0ce5e7870e579 --- /dev/null +++ b/integration-tests/plugins/__tests__/cart/store/get-cart.ts @@ -0,0 +1,71 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +const env = { MEDUSA_FF_MEDUSA_V2: true } + +describe("GET /store/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let cartModuleService: ICartModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + cartModuleService = appContainer.resolve(ModuleRegistrationName.CART) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get cart", async () => { + const cart = await cartModuleService.create({ + currency_code: "usd", + items: [ + { + unit_price: 1000, + quantity: 1, + title: "Test item", + }, + ], + }) + + const api = useApi() as any + const response = await api.get(`/store/carts/${cart.id}`) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + currency_code: "usd", + items: expect.arrayContaining([ + expect.objectContaining({ + unit_price: 1000, + quantity: 1, + title: "Test item", + }), + ]), + }) + ) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index 2088ea4544e5c..41dffcbef92be 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -81,5 +81,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/sales-channel", }, + [Modules.CART]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/cart", + }, }, } diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 2add4cf3248d3..5d6be8fbcb718 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -1,3 +1,5 @@ +import jwt from "jsonwebtoken" + import { AuthenticationInput, AuthenticationResponse, @@ -8,6 +10,7 @@ import { InternalModuleDeclaration, MedusaContainer, ModuleJoinerConfig, + JWTGenerationOptions, } from "@medusajs/types" import { AuthProvider, AuthUser } from "@models" @@ -33,6 +36,15 @@ import { } from "@medusajs/types" import { ServiceTypes } from "@types" +type AuthModuleOptions = { + jwt_secret: string +} + +type AuthJWTPayload = { + id: string + scope: string +} + type InjectedDependencies = { baseRepository: DAL.RepositoryService authUserService: AuthUserService @@ -57,6 +69,7 @@ export default class AuthModuleService< protected authUserService_: AuthUserService protected authProviderService_: AuthProviderService + protected options_: AuthModuleOptions constructor( { @@ -64,12 +77,14 @@ export default class AuthModuleService< authProviderService, baseRepository, }: InjectedDependencies, + options: AuthModuleOptions, protected readonly moduleDeclaration: InternalModuleDeclaration ) { this.__container__ = arguments[0] this.baseRepository_ = baseRepository this.authUserService_ = authUserService this.authProviderService_ = authProviderService + this.options_ = options } async retrieveAuthProvider( @@ -100,9 +115,10 @@ export default class AuthModuleService< sharedContext ) - return await this.baseRepository_.serialize< - AuthTypes.AuthProviderDTO[] - >(authProviders, { populate: true }) + return await this.baseRepository_.serialize( + authProviders, + { populate: true } + ) } @InjectManager("baseRepository_") @@ -118,13 +134,54 @@ export default class AuthModuleService< ) return [ - await this.baseRepository_.serialize< - AuthTypes.AuthProviderDTO[] - >(authProviders, { populate: true }), + await this.baseRepository_.serialize( + authProviders, + { populate: true } + ), count, ] } + async generateJwtToken( + authUserId: string, + scope: string, + options: JWTGenerationOptions = {} + ): Promise { + const authUser = await this.authUserService_.retrieve(authUserId) + return jwt.sign({ id: authUser.id, scope }, this.options_.jwt_secret, { + expiresIn: options.expiresIn || "1d", + }) + } + + async retrieveAuthUserFromJwtToken( + token: string, + scope: string + ): Promise { + let decoded: AuthJWTPayload + try { + const verifiedToken = jwt.verify(token, this.options_.jwt_secret) + decoded = verifiedToken as AuthJWTPayload + } catch (err) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "The provided JWT token is invalid" + ) + } + + if (decoded.scope !== scope) { + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + "The provided JWT token is invalid" + ) + } + + const authUser = await this.authUserService_.retrieve(decoded.id) + return await this.baseRepository_.serialize( + authUser, + { populate: true } + ) + } + async createAuthProvider( data: CreateAuthProviderDTO[], sharedContext?: Context @@ -139,9 +196,7 @@ export default class AuthModuleService< async createAuthProvider( data: CreateAuthProviderDTO | CreateAuthProviderDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const providers = await this.createAuthProviders_(input, sharedContext) @@ -174,13 +229,9 @@ export default class AuthModuleService< @InjectManager("baseRepository_") async updateAuthProvider( - data: - | AuthTypes.UpdateAuthProviderDTO[] - | AuthTypes.UpdateAuthProviderDTO, + data: AuthTypes.UpdateAuthProviderDTO[] | AuthTypes.UpdateAuthProviderDTO, @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthProviderDTO | AuthTypes.AuthProviderDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const providers = await this.updateAuthProvider_(input, sharedContext) @@ -241,11 +292,12 @@ export default class AuthModuleService< sharedContext ) - return await this.baseRepository_.serialize< - AuthTypes.AuthUserDTO[] - >(authUsers, { - populate: true, - }) + return await this.baseRepository_.serialize( + authUsers, + { + populate: true, + } + ) } @InjectManager("baseRepository_") @@ -261,12 +313,9 @@ export default class AuthModuleService< ) return [ - await this.baseRepository_.serialize( - authUsers, - { - populate: true, - } - ), + await this.baseRepository_.serialize(authUsers, { + populate: true, + }), count, ] } @@ -284,9 +333,7 @@ export default class AuthModuleService< async createAuthUser( data: CreateAuthUserDTO[] | CreateAuthUserDTO, @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const authUsers = await this.createAuthUsers_(input, sharedContext) @@ -321,9 +368,7 @@ export default class AuthModuleService< async updateAuthUser( data: UpdateAuthUserDTO | UpdateAuthUserDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise< - AuthTypes.AuthUserDTO | AuthTypes.AuthUserDTO[] - > { + ): Promise { const input = Array.isArray(data) ? data : [data] const updatedUsers = await this.updateAuthUsers_(input, sharedContext) diff --git a/packages/cart/src/migrations/CartModuleSetup20240122122952.ts b/packages/cart/src/migrations/CartModuleSetup20240122122952.ts index a81894789b3c6..cea797ef6881d 100644 --- a/packages/cart/src/migrations/CartModuleSetup20240122122952.ts +++ b/packages/cart/src/migrations/CartModuleSetup20240122122952.ts @@ -21,6 +21,8 @@ export class CartModuleSetup20240122122952 extends Migration { ); ALTER TABLE "cart" ADD COLUMN IF NOT EXISTS "currency_code" TEXT NOT NULL; + ALTER TABLE "cart" ALTER COLUMN "region_id" DROP NOT NULL; + ALTER TABLE "cart" ALTER COLUMN "email" DROP NOT NULL; ALTER TABLE "cart" DROP CONSTRAINT IF EXISTS "FK_242205c81c1152fab1b6e848470"; ALTER TABLE "cart" DROP CONSTRAINT IF EXISTS "FK_484c329f4783be4e18e5e2ff090"; diff --git a/packages/cart/src/models/address.ts b/packages/cart/src/models/address.ts index a603018efad1e..c26baa66b3ed3 100644 --- a/packages/cart/src/models/address.ts +++ b/packages/cart/src/models/address.ts @@ -6,10 +6,9 @@ import { OnInit, OptionalProps, PrimaryKey, - Property + Property, } from "@mikro-orm/core" - type OptionalAddressProps = DAL.EntityDateColumns // TODO: To be revisited when more clear @Entity({ tableName: "cart_address" }) @@ -20,40 +19,40 @@ export default class Address { id!: string @Property({ columnType: "text", nullable: true }) - customer_id?: string | null + customer_id: string | null = null @Property({ columnType: "text", nullable: true }) - company?: string | null + company: string | null = null @Property({ columnType: "text", nullable: true }) - first_name?: string | null + first_name: string | null = null @Property({ columnType: "text", nullable: true }) - last_name?: string | null + last_name: string | null = null @Property({ columnType: "text", nullable: true }) - address_1?: string | null + address_1: string | null = null @Property({ columnType: "text", nullable: true }) - address_2?: string | null + address_2: string | null = null @Property({ columnType: "text", nullable: true }) - city?: string | null + city: string | null = null @Property({ columnType: "text", nullable: true }) - country_code?: string | null + country_code: string | null = null @Property({ columnType: "text", nullable: true }) - province?: string | null + province: string | null = null @Property({ columnType: "text", nullable: true }) - postal_code?: string | null + postal_code: string | null = null @Property({ columnType: "text", nullable: true }) - phone?: string | null + phone: string | null = null @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | null + metadata: Record | null = null @Property({ onCreate: () => new Date(), diff --git a/packages/cart/src/models/cart.ts b/packages/cart/src/models/cart.ts index b83d1b534e33a..42b5db19fc617 100644 --- a/packages/cart/src/models/cart.ts +++ b/packages/cart/src/models/cart.ts @@ -5,7 +5,6 @@ import { Cascade, Collection, Entity, - Index, ManyToOne, OnInit, OneToMany, @@ -34,50 +33,54 @@ export default class Cart { nullable: true, index: "IDX_cart_region_id", }) - region_id?: string | null + region_id: string | null = null @Property({ columnType: "text", nullable: true, index: "IDX_cart_customer_id", }) - customer_id?: string | null + customer_id: string | null = null @Property({ columnType: "text", nullable: true, index: "IDX_cart_sales_channel_id", }) - sales_channel_id?: string | null + sales_channel_id: string | null = null @Property({ columnType: "text", nullable: true }) - email?: string | null + email: string | null = null @Property({ columnType: "text", index: "IDX_cart_curency_code" }) currency_code: string - @Index({ name: "IDX_cart_shipping_address_id" }) @Property({ columnType: "text", nullable: true }) shipping_address_id?: string | null - @ManyToOne(() => Address, { + @ManyToOne({ + entity: () => Address, fieldName: "shipping_address_id", nullable: true, + index: "IDX_cart_shipping_address_id", + cascade: [Cascade.PERSIST], }) shipping_address?: Address | null - @Index({ name: "IDX_cart_billing_address_id" }) @Property({ columnType: "text", nullable: true }) billing_address_id?: string | null - @ManyToOne(() => Address, { + @ManyToOne({ + entity: () => Address, fieldName: "billing_address_id", nullable: true, + index: "IDX_cart_billing_address_id", + cascade: [Cascade.PERSIST], }) billing_address?: Address | null @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | null + metadata: Record | null = null @OneToMany(() => LineItem, (lineItem) => lineItem.cart, { cascade: [Cascade.REMOVE], @@ -89,46 +92,12 @@ export default class Cart { }) shipping_methods = new Collection(this) - /** COMPUTED PROPERTIES - START */ - - // compare_at_item_total?: number - // compare_at_item_subtotal?: number - // compare_at_item_tax_total?: number - - // original_item_total: number - // original_item_subtotal: number - // original_item_tax_total: number - - // item_total: number - // item_subtotal: number - // item_tax_total: number - - // original_total: number - // original_subtotal: number - // original_tax_total: number - - // total: number - // subtotal: number - // tax_total: number - // discount_total: number - // discount_tax_total: number - - // shipping_total: number - // shipping_subtotal: number - // shipping_tax_total: number - - // original_shipping_total: number - // original_shipping_subtotal: number - // original_shipping_tax_total: number - - /** COMPUTED PROPERTIES - END */ - @Property({ onCreate: () => new Date(), columnType: "timestamptz", defaultRaw: "now()", }) - created_at?: Date + created_at: Date @Property({ onCreate: () => new Date(), @@ -136,10 +105,10 @@ export default class Cart { columnType: "timestamptz", defaultRaw: "now()", }) - updated_at?: Date + updated_at: Date @Property({ columnType: "timestamptz", nullable: true }) - deleted_at?: Date + deleted_at: Date | null = null @BeforeCreate() onCreate() { diff --git a/packages/cart/src/models/line-item-adjustment.ts b/packages/cart/src/models/line-item-adjustment.ts index 341b22b793de4..a1134a13936a6 100644 --- a/packages/cart/src/models/line-item-adjustment.ts +++ b/packages/cart/src/models/line-item-adjustment.ts @@ -1,6 +1,7 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Check, Entity, ManyToOne, @@ -15,12 +16,12 @@ import LineItem from "./line-item" expression: (columns) => `${columns.amount} >= 0`, }) export default class LineItemAdjustment extends AdjustmentLine { - @ManyToOne(() => LineItem, { - onDelete: "cascade", - nullable: true, + @ManyToOne({ + entity: () => LineItem, index: "IDX_adjustment_item_id", + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) - item?: LineItem | null + item: LineItem @Property({ columnType: "text" }) item_id: string diff --git a/packages/cart/src/models/line-item-tax-line.ts b/packages/cart/src/models/line-item-tax-line.ts index 149d767fec51a..59a37dad20909 100644 --- a/packages/cart/src/models/line-item-tax-line.ts +++ b/packages/cart/src/models/line-item-tax-line.ts @@ -1,6 +1,7 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Entity, ManyToOne, OnInit, @@ -11,12 +12,12 @@ import TaxLine from "./tax-line" @Entity({ tableName: "cart_line_item_tax_line" }) export default class LineItemTaxLine extends TaxLine { - @ManyToOne(() => LineItem, { - onDelete: "cascade", - nullable: true, + @ManyToOne({ + entity: () => LineItem, index: "IDX_tax_line_item_id", + cascade: [Cascade.REMOVE, Cascade.PERSIST] }) - item: LineItem | null + item: LineItem @Property({ columnType: "text" }) item_id: string diff --git a/packages/cart/src/models/line-item.ts b/packages/cart/src/models/line-item.ts index 19a152d070c87..5ab63a65651a4 100644 --- a/packages/cart/src/models/line-item.ts +++ b/packages/cart/src/models/line-item.ts @@ -35,21 +35,22 @@ export default class LineItem { @Property({ columnType: "text" }) cart_id: string - @ManyToOne(() => Cart, { + @ManyToOne({ + entity: () => Cart, onDelete: "cascade", index: "IDX_line_item_cart_id", - nullable: true, + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) - cart?: Cart | null + cart: Cart @Property({ columnType: "text" }) title: string @Property({ columnType: "text", nullable: true }) - subtitle: string | null + subtitle: string | null = null @Property({ columnType: "text", nullable: true }) - thumbnail?: string | null + thumbnail: string | null = null @Property({ columnType: "integer" }) quantity: number @@ -59,44 +60,44 @@ export default class LineItem { nullable: true, index: "IDX_line_item_variant_id", }) - variant_id?: string | null + variant_id: string | null = null @Property({ columnType: "text", nullable: true, index: "IDX_line_item_product_id", }) - product_id?: string | null + product_id: string | null = null @Property({ columnType: "text", nullable: true }) - product_title?: string | null + product_title: string | null = null @Property({ columnType: "text", nullable: true }) - product_description?: string | null + product_description: string | null = null @Property({ columnType: "text", nullable: true }) - product_subtitle?: string | null + product_subtitle: string | null = null @Property({ columnType: "text", nullable: true }) - product_type?: string | null + product_type: string | null = null @Property({ columnType: "text", nullable: true }) - product_collection?: string | null + product_collection: string | null = null @Property({ columnType: "text", nullable: true }) - product_handle?: string | null + product_handle: string | null = null @Property({ columnType: "text", nullable: true }) - variant_sku?: string | null + variant_sku: string | null = null @Property({ columnType: "text", nullable: true }) - variant_barcode?: string | null + variant_barcode: string | null = null @Property({ columnType: "text", nullable: true }) - variant_title?: string | null + variant_title: string | null = null @Property({ columnType: "jsonb", nullable: true }) - variant_option_values?: Record | null + variant_option_values: Record | null = null @Property({ columnType: "boolean" }) requires_shipping = true @@ -108,8 +109,9 @@ export default class LineItem { is_tax_inclusive = false @Property({ columnType: "numeric", nullable: true }) - compare_at_unit_price?: number + compare_at_unit_price: number | null = null + // TODO: Rework when BigNumber has been introduced @Property({ columnType: "numeric", serializer: Number }) @Check({ expression: "unit_price >= 0" }) // TODO: Validate that numeric types work with the expression unit_price: number @@ -124,34 +126,12 @@ export default class LineItem { }) adjustments = new Collection(this) - /** COMPUTED PROPERTIES - START */ - - // compare_at_total?: number - // compare_at_subtotal?: number - // compare_at_tax_total?: number - - // original_total: number - // original_subtotal: number - // original_tax_total: number - - // item_total: number - // item_subtotal: number - // item_tax_total: number - - // total: number - // subtotal: number - // tax_total: number - // discount_total: number - // discount_tax_total: number - - /** COMPUTED PROPERTIES - END */ - @Property({ onCreate: () => new Date(), columnType: "timestamptz", defaultRaw: "now()", }) - created_at?: Date + created_at: Date @Property({ onCreate: () => new Date(), @@ -159,7 +139,7 @@ export default class LineItem { columnType: "timestamptz", defaultRaw: "now()", }) - updated_at?: Date + updated_at: Date @BeforeCreate() onCreate() { diff --git a/packages/cart/src/models/shipping-method-adjustment.ts b/packages/cart/src/models/shipping-method-adjustment.ts index 63b647379165d..16a49fdb431de 100644 --- a/packages/cart/src/models/shipping-method-adjustment.ts +++ b/packages/cart/src/models/shipping-method-adjustment.ts @@ -1,6 +1,7 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Entity, ManyToOne, OnInit, @@ -11,12 +12,12 @@ import ShippingMethod from "./shipping-method" @Entity({ tableName: "cart_shipping_method_adjustment" }) export default class ShippingMethodAdjustment extends AdjustmentLine { - @ManyToOne(() => ShippingMethod, { - onDelete: "cascade", - nullable: true, + @ManyToOne({ + entity: () => ShippingMethod, index: "IDX_adjustment_shipping_method_id", + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) - shipping_method: ShippingMethod | null + shipping_method: ShippingMethod @Property({ columnType: "text" }) shipping_method_id: string diff --git a/packages/cart/src/models/shipping-method-tax-line.ts b/packages/cart/src/models/shipping-method-tax-line.ts index e5a022ab78daf..4673d19a43c37 100644 --- a/packages/cart/src/models/shipping-method-tax-line.ts +++ b/packages/cart/src/models/shipping-method-tax-line.ts @@ -1,6 +1,7 @@ import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Entity, ManyToOne, OnInit, @@ -11,12 +12,12 @@ import TaxLine from "./tax-line" @Entity({ tableName: "cart_shipping_method_tax_line" }) export default class ShippingMethodTaxLine extends TaxLine { - @ManyToOne(() => ShippingMethod, { - onDelete: "cascade", - nullable: true, + @ManyToOne({ + entity: () => ShippingMethod, index: "IDX_tax_line_shipping_method_id", + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) - shipping_method: ShippingMethod | null + shipping_method: ShippingMethod @Property({ columnType: "text" }) shipping_method_id: string diff --git a/packages/cart/src/models/shipping-method.ts b/packages/cart/src/models/shipping-method.ts index ec41b17b91a32..c3a2ed448a421 100644 --- a/packages/cart/src/models/shipping-method.ts +++ b/packages/cart/src/models/shipping-method.ts @@ -24,18 +24,18 @@ export default class ShippingMethod { @Property({ columnType: "text" }) cart_id: string - @ManyToOne(() => Cart, { - onDelete: "cascade", + @ManyToOne({ + entity: () => Cart, index: "IDX_shipping_method_cart_id", - nullable: true, + cascade: [Cascade.REMOVE, Cascade.PERSIST], }) - cart?: Cart | null + cart: Cart @Property({ columnType: "text" }) name: string @Property({ columnType: "jsonb", nullable: true }) - description?: string | null + description: string | null = null @Property({ columnType: "numeric", serializer: Number }) amount: number @@ -48,13 +48,13 @@ export default class ShippingMethod { nullable: true, index: "IDX_shipping_method_option_id", }) - shipping_option_id?: string | null + shipping_option_id: string | null = null @Property({ columnType: "jsonb", nullable: true }) - data?: Record | null + data: Record | null = null @Property({ columnType: "jsonb", nullable: true }) - metadata?: Record | null + metadata: Record | null = null @OneToMany( () => ShippingMethodTaxLine, @@ -74,20 +74,6 @@ export default class ShippingMethod { ) adjustments = new Collection(this) - /** COMPUTED PROPERTIES - START */ - - // original_total: number - // original_subtotal: number - // original_tax_total: number - - // total: number - // subtotal: number - // tax_total: number - // discount_total: number - // discount_tax_total: number - - /** COMPUTED PROPERTIES - END */ - @Property({ onCreate: () => new Date(), columnType: "timestamptz", diff --git a/packages/core-flows/src/customer/steps/delete-customers.ts b/packages/core-flows/src/customer/steps/delete-customers.ts index f94027ddb3085..35b6947c73d1e 100644 --- a/packages/core-flows/src/customer/steps/delete-customers.ts +++ b/packages/core-flows/src/customer/steps/delete-customers.ts @@ -16,8 +16,8 @@ export const deleteCustomerStep = createStep( return new StepResponse(void 0, ids) }, - async (prevCustomers, { container }) => { - if (!prevCustomers) { + async (prevCustomerIds, { container }) => { + if (!prevCustomerIds?.length) { return } @@ -25,6 +25,6 @@ export const deleteCustomerStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.restore(prevCustomers) + await service.restore(prevCustomerIds) } ) diff --git a/packages/core-flows/src/customer/steps/update-customers.ts b/packages/core-flows/src/customer/steps/update-customers.ts index 4ed128cbc285e..c0e7c90f3b4a9 100644 --- a/packages/core-flows/src/customer/steps/update-customers.ts +++ b/packages/core-flows/src/customer/steps/update-customers.ts @@ -36,7 +36,7 @@ export const updateCustomersStep = createStep( return new StepResponse(customers, prevCustomers) }, async (prevCustomers, { container }) => { - if (!prevCustomers) { + if (!prevCustomers?.length) { return } diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 223ef58dfa8aa..ee50bf7ee8613 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -8,6 +8,7 @@ import { CustomerTypes, SoftDeleteReturn, RestoreReturn, + CustomerUpdatableFields, } from "@medusajs/types" import { diff --git a/packages/medusa/src/api-v2/admin/customers/[id]/route.ts b/packages/medusa/src/api-v2/admin/customers/[id]/route.ts index a6400ec96e9aa..fbf8c8cc6e8a5 100644 --- a/packages/medusa/src/api-v2/admin/customers/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/customers/[id]/route.ts @@ -23,8 +23,8 @@ export const GET = async (req: MedusaRequest, res: MedusaResponse) => { } export const POST = async (req: MedusaRequest, res: MedusaResponse) => { - const updateCampaigns = updateCustomersWorkflow(req.scope) - const { result, errors } = await updateCampaigns.run({ + const updateCustomers = updateCustomersWorkflow(req.scope) + const { result, errors } = await updateCustomers.run({ input: { selector: { id: req.params.id }, update: req.validatedBody as CustomerUpdatableFields, @@ -41,9 +41,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { const id = req.params.id - const deleteCampaigns = deleteCustomersWorkflow(req.scope) + const deleteCustomers = deleteCustomersWorkflow(req.scope) - const { errors } = await deleteCampaigns.run({ + const { errors } = await deleteCustomers.run({ input: { ids: [id] }, throwOnError: false, }) @@ -54,7 +54,7 @@ export const DELETE = async (req: MedusaRequest, res: MedusaResponse) => { res.status(200).json({ id, - object: "campaign", + object: "customer", deleted: true, }) } diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 59b5884fed05b..34fabd658f811 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,8 +1,9 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" -import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" -import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" +import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" +import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" +import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" export const config: MiddlewaresConfig = { routes: [ @@ -10,5 +11,6 @@ export const config: MiddlewaresConfig = { ...adminCustomerRoutesMiddlewares, ...adminPromotionRoutesMiddlewares, ...adminCampaignRoutesMiddlewares, + ...storeCartRoutesMiddlewares, ], } diff --git a/packages/medusa/src/api-v2/store/carts/[id]/route.ts b/packages/medusa/src/api-v2/store/carts/[id]/route.ts new file mode 100644 index 0000000000000..e0bbedacf8d09 --- /dev/null +++ b/packages/medusa/src/api-v2/store/carts/[id]/route.ts @@ -0,0 +1,30 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ICartModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const cartModuleService: ICartModuleService = req.scope.resolve( + ModuleRegistrationName.CART + ) + + // TODO: Replace with remoteQuery + const cart = await cartModuleService.retrieve(req.params.id, { + select: req.retrieveConfig.select, + relations: req.retrieveConfig.relations, + }) + + // const remoteQuery = req.scope.resolve("remoteQuery") + + // const variables = { id: req.params.id } + + // const query = { + // cart: { + // __args: variables, + // ...defaultStoreCartRemoteQueryObject, + // }, + // } + + // const [cart] = await remoteQuery(query) + + res.json({ cart }) +} diff --git a/packages/medusa/src/api-v2/store/carts/middlewares.ts b/packages/medusa/src/api-v2/store/carts/middlewares.ts new file mode 100644 index 0000000000000..214770119181a --- /dev/null +++ b/packages/medusa/src/api-v2/store/carts/middlewares.ts @@ -0,0 +1,17 @@ +import { transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { StoreGetCartsCartParams } from "./validators" + +export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/store/carts/:id", + middlewares: [ + transformQuery( + StoreGetCartsCartParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/store/carts/query-config.ts b/packages/medusa/src/api-v2/store/carts/query-config.ts new file mode 100644 index 0000000000000..a77526002a5c6 --- /dev/null +++ b/packages/medusa/src/api-v2/store/carts/query-config.ts @@ -0,0 +1,64 @@ +export const defaultStoreCartFields = [ + "id", + "currency_code", + "email", + "created_at", + "updated_at", + "deleted_at", +] + +export const defaultStoreCartRelations = [ + "items", + "shipping_address", + "billing_address", + "shipping_methods", +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultStoreCartFields, + defaultRelations: defaultStoreCartRelations, + isList: false, +} + +export const defaultStoreCartRemoteQueryObject = { + fields: defaultStoreCartFields, + line_items: { + fields: [ + "id", + "created_at", + "updated_at", + "deleted_at", + "title", + "quantity", + "unit_price", + ], + }, + shipping_address: { + fields: [ + "id", + "first_name", + "last_name", + "address_1", + "address_2", + "city", + "postal_code", + "country_code", + "region_code", + "phone", + ], + }, + billing_address: { + fields: [ + "id", + "first_name", + "last_name", + "address_1", + "address_2", + "city", + "postal_code", + "country_code", + "region_code", + "phone", + ], + }, +} diff --git a/packages/medusa/src/api-v2/store/carts/validators.ts b/packages/medusa/src/api-v2/store/carts/validators.ts new file mode 100644 index 0000000000000..3dff03e7e899b --- /dev/null +++ b/packages/medusa/src/api-v2/store/carts/validators.ts @@ -0,0 +1,3 @@ +import { FindParams } from "../../../types/common" + +export class StoreGetCartsCartParams extends FindParams {} diff --git a/packages/medusa/src/joiner-configs/cart-service.ts b/packages/medusa/src/joiner-configs/cart-service.ts index 878cfe2cdd3af..f236208ebfe6e 100644 --- a/packages/medusa/src/joiner-configs/cart-service.ts +++ b/packages/medusa/src/joiner-configs/cart-service.ts @@ -4,7 +4,7 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { Cart } from "../models" export default { - serviceName: Modules.CART, + serviceName: "cartService", primaryKeys: ["id"], linkableKeys: { cart_id: "Cart" }, alias: { diff --git a/packages/medusa/src/joiner-configs/index.ts b/packages/medusa/src/joiner-configs/index.ts index fb21643b5d92d..4fccd04161ad5 100644 --- a/packages/medusa/src/joiner-configs/index.ts +++ b/packages/medusa/src/joiner-configs/index.ts @@ -1,5 +1,6 @@ export * as cart from "./cart-service" export * as customer from "./customer-service" +export * as publishableApiKey from "./publishable-api-key-service" export * as region from "./region-service" export * as shippingProfile from "./shipping-profile-service" -export * as publishableApiKey from "./publishable-api-key-service" + diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 54b0051f0b1d3..a524e8a7c188c 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -6,6 +6,7 @@ import type { MedusaContainer } from "./global" export interface MedusaRequest extends Request { user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer + auth_user?: { id: string; app_metadata: Record; scope: string } } export type MedusaResponse = Response diff --git a/packages/medusa/src/utils/authenticate-middleware.ts b/packages/medusa/src/utils/authenticate-middleware.ts new file mode 100644 index 0000000000000..0417017ef4ce3 --- /dev/null +++ b/packages/medusa/src/utils/authenticate-middleware.ts @@ -0,0 +1,79 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { AuthUserDTO, IAuthModuleService } from "@medusajs/types" +import { NextFunction, RequestHandler } from "express" +import { MedusaRequest, MedusaResponse } from "../types/routing" + +const SESSION_AUTH = "session" +const BEARER_AUTH = "bearer" + +type MedusaSession = { + auth: { + [authScope: string]: { + user_id: string + } + } +} + +type AuthType = "session" | "bearer" + +export default ( + authScope: string, + authType: AuthType | AuthType[], + options: { allowUnauthenticated?: boolean } = {} +): RequestHandler => { + return async ( + req: MedusaRequest, + res: MedusaResponse, + next: NextFunction + ): Promise => { + const authTypes = Array.isArray(authType) ? authType : [authType] + const authModule = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + // @ts-ignore + const session: MedusaSession = req.session || {} + + let authUser: AuthUserDTO | null = null + if (authTypes.includes(SESSION_AUTH)) { + if (session.auth && session.auth[authScope]) { + authUser = await authModule + .retrieveAuthUser(session.auth[authScope].user_id) + .catch(() => null) + } + } + + if (authTypes.includes(BEARER_AUTH)) { + const authHeader = req.headers.authorization + if (authHeader) { + const re = /(\S+)\s+(\S+)/ + const matches = authHeader.match(re) + + if (matches) { + const tokenType = matches[1] + const token = matches[2] + if (tokenType.toLowerCase() === "bearer") { + authUser = await authModule + .retrieveAuthUserFromJwtToken(token, authScope) + .catch(() => null) + } + } + } + } + + if (authUser) { + req.auth_user = { + id: authUser.id, + app_metadata: authUser.app_metadata, + scope: authScope, + } + return next() + } + + if (options.allowUnauthenticated) { + return next() + } + + res.status(401).json({ message: "Unauthorized" }) + } +} diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index 4b97bdd521a1a..09c7cdcaaf68d 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -38,44 +38,40 @@ describe("Promotion Service: computeActions", () => { }) describe("when code is not present in database", () => { - it("should throw error when code in promotions array does not exist", async () => { - const error = await service - .computeActions(["DOES_NOT_EXIST"], { - customer: { - customer_group: { - id: "VIP", + it("should return empty array when promotion does not exist", async () => { + const response = await service.computeActions(["DOES_NOT_EXIST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + unit_price: 100, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", }, }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, + { + id: "item_cotton_sweater", + quantity: 5, + unit_price: 150, + product_category: { + id: "catg_cotton", }, - { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, + product: { + id: "prod_sweater", }, - ], - }) - .catch((e) => e) + }, + ], + }) - expect(error.message).toContain( - "Promotion for code (DOES_NOT_EXIST) not found" - ) + expect(response).toEqual([]) }) it("should throw error when code in items adjustment does not exist", async () => { @@ -2315,4 +2311,366 @@ describe("Promotion Service: computeActions", () => { ]) }) }) + + describe("when promotion of type buyget", () => { + it("should compute adjustment when target and buy rules match", async () => { + const context = { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + unit_price: 500, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_1", + }, + }, + { + id: "item_cotton_tshirt2", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_2", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_sweater", + }, + product: { + id: "prod_sweater_1", + }, + }, + ], + } + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + max_quantity: 1, + apply_to_quantity: 1, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_tshirt"], + }, + ], + buy_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_sweater"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], context) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt2", + amount: 1000, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should return empty array when conditions for minimum qty aren't met", async () => { + const context = { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + unit_price: 500, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_1", + }, + }, + { + id: "item_cotton_tshirt2", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_2", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_sweater", + }, + product: { + id: "prod_sweater_1", + }, + }, + ], + } + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + max_quantity: 1, + apply_to_quantity: 1, + buy_rules_min_quantity: 4, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_tshirt"], + }, + ], + buy_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_sweater"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], context) + + expect(result).toEqual([]) + }) + + it("should compute actions for multiple items when conditions for target qty exceed one item", async () => { + const context = { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + unit_price: 500, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_1", + }, + }, + { + id: "item_cotton_tshirt2", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_2", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_sweater", + }, + product: { + id: "prod_sweater_1", + }, + }, + ], + } + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + max_quantity: 1, + apply_to_quantity: 4, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_tshirt"], + }, + ], + buy_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_sweater"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], context) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt2", + amount: 2000, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 1000, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should return empty array when target rules arent met with context", async () => { + const context = { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + unit_price: 500, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_1", + }, + }, + { + id: "item_cotton_tshirt2", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_tshirt", + }, + product: { + id: "prod_tshirt_2", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + unit_price: 1000, + product_category: { + id: "catg_sweater", + }, + product: { + id: "prod_sweater_1", + }, + }, + ], + } + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.BUYGET, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + max_quantity: 1, + apply_to_quantity: 4, + buy_rules_min_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_not-found"], + }, + ], + buy_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_sweater"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], context) + + expect(result).toEqual([]) + }) + }) }) diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 1756901800076..ecdc1b5113ef9 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -263,6 +263,8 @@ export default class PromotionModuleService< "application_method", "application_method.target_rules", "application_method.target_rules.values", + "application_method.buy_rules", + "application_method.buy_rules.values", "rules", "rules.values", "campaign", @@ -271,6 +273,10 @@ export default class PromotionModuleService< } ) + const sortedPermissionsToApply = promotions + .filter((p) => promotionCodesToApply.includes(p.code!)) + .sort(ComputeActionUtils.sortByBuyGetType) + const existingPromotionsMap = new Map( promotions.map((promotion) => [promotion.code!, promotion]) ) @@ -306,15 +312,8 @@ export default class PromotionModuleService< } } - for (const promotionCode of promotionCodesToApply) { - const promotion = existingPromotionsMap.get(promotionCode) - - if (!promotion) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Promotion for code (${promotionCode}) not found` - ) - } + for (const promotionToApply of sortedPermissionsToApply) { + const promotion = existingPromotionsMap.get(promotionToApply.code!)! const { application_method: applicationMethod, @@ -334,20 +333,9 @@ export default class PromotionModuleService< continue } - if (applicationMethod.target_type === ApplicationMethodTargetType.ORDER) { - const computedActionsForItems = - ComputeActionUtils.getComputedActionsForOrder( - promotion, - applicationContext, - methodIdPromoValueMap - ) - - computedActions.push(...computedActionsForItems) - } - - if (applicationMethod.target_type === ApplicationMethodTargetType.ITEMS) { + if (promotion.type === PromotionType.BUYGET) { const computedActionsForItems = - ComputeActionUtils.getComputedActionsForItems( + ComputeActionUtils.getComputedActionsForBuyGet( promotion, applicationContext[ApplicationMethodTargetType.ITEMS], methodIdPromoValueMap @@ -356,18 +344,46 @@ export default class PromotionModuleService< computedActions.push(...computedActionsForItems) } - if ( - applicationMethod.target_type === - ApplicationMethodTargetType.SHIPPING_METHODS - ) { - const computedActionsForShippingMethods = - ComputeActionUtils.getComputedActionsForShippingMethods( - promotion, - applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS], - methodIdPromoValueMap - ) + if (promotion.type === PromotionType.STANDARD) { + if ( + applicationMethod.target_type === ApplicationMethodTargetType.ORDER + ) { + const computedActionsForItems = + ComputeActionUtils.getComputedActionsForOrder( + promotion, + applicationContext, + methodIdPromoValueMap + ) + + computedActions.push(...computedActionsForItems) + } + + if ( + applicationMethod.target_type === ApplicationMethodTargetType.ITEMS + ) { + const computedActionsForItems = + ComputeActionUtils.getComputedActionsForItems( + promotion, + applicationContext[ApplicationMethodTargetType.ITEMS], + methodIdPromoValueMap + ) + + computedActions.push(...computedActionsForItems) + } - computedActions.push(...computedActionsForShippingMethods) + if ( + applicationMethod.target_type === + ApplicationMethodTargetType.SHIPPING_METHODS + ) { + const computedActionsForShippingMethods = + ComputeActionUtils.getComputedActionsForShippingMethods( + promotion, + applicationContext[ApplicationMethodTargetType.SHIPPING_METHODS], + methodIdPromoValueMap + ) + + computedActions.push(...computedActionsForShippingMethods) + } } } diff --git a/packages/promotion/src/utils/compute-actions/buy-get.ts b/packages/promotion/src/utils/compute-actions/buy-get.ts new file mode 100644 index 0000000000000..cc6eaa44e114f --- /dev/null +++ b/packages/promotion/src/utils/compute-actions/buy-get.ts @@ -0,0 +1,101 @@ +import { PromotionTypes } from "@medusajs/types" +import { + ApplicationMethodTargetType, + ComputedActions, + MedusaError, + PromotionType, +} from "@medusajs/utils" +import { areRulesValidForContext } from "../validations/promotion-rule" +import { computeActionForBudgetExceeded } from "./usage" + +export function getComputedActionsForBuyGet( + promotion: PromotionTypes.PromotionDTO, + itemsContext: PromotionTypes.ComputeActionContext[ApplicationMethodTargetType.ITEMS], + methodIdPromoValueMap: Map +): PromotionTypes.ComputeActions[] { + const buyRulesMinQuantity = + promotion.application_method?.buy_rules_min_quantity + const applyToQuantity = promotion.application_method?.apply_to_quantity + const buyRules = promotion.application_method?.buy_rules + const targetRules = promotion.application_method?.target_rules + const computedActions: PromotionTypes.ComputeActions[] = [] + + if (!itemsContext) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `"items" should be present as an array in the context to compute actions` + ) + } + + if (!Array.isArray(buyRules) || !Array.isArray(targetRules)) { + return [] + } + + const validQuantity = itemsContext + .filter((item) => areRulesValidForContext(buyRules, item)) + .reduce((acc, next) => acc + next.quantity, 0) + + if ( + !buyRulesMinQuantity || + !applyToQuantity || + buyRulesMinQuantity > validQuantity + ) { + return [] + } + + const validItemsForTargetRules = itemsContext + .filter((item) => areRulesValidForContext(targetRules, item)) + .sort((a, b) => { + return b.unit_price - a.unit_price + }) + + let remainingQtyToApply = applyToQuantity + + for (const method of validItemsForTargetRules) { + const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const multiplier = Math.min(method.quantity, remainingQtyToApply) + const amount = method.unit_price * multiplier + const newRemainingQtyToApply = remainingQtyToApply - multiplier + + if (newRemainingQtyToApply < 0 || amount <= 0) { + break + } else { + remainingQtyToApply = newRemainingQtyToApply + } + + const budgetExceededAction = computeActionForBudgetExceeded( + promotion, + amount + ) + + if (budgetExceededAction) { + computedActions.push(budgetExceededAction) + + continue + } + + methodIdPromoValueMap.set(method.id, appliedPromoValue + amount) + + computedActions.push({ + action: ComputedActions.ADD_ITEM_ADJUSTMENT, + item_id: method.id, + amount, + code: promotion.code!, + }) + } + + return computedActions +} + +export function sortByBuyGetType(a, b) { + if (a.type === PromotionType.BUYGET && b.type !== PromotionType.BUYGET) { + return -1 + } else if ( + a.type !== PromotionType.BUYGET && + b.type === PromotionType.BUYGET + ) { + return 1 + } else { + return 0 + } +} diff --git a/packages/promotion/src/utils/compute-actions/index.ts b/packages/promotion/src/utils/compute-actions/index.ts index 33d690935a0d4..de8317a075ca0 100644 --- a/packages/promotion/src/utils/compute-actions/index.ts +++ b/packages/promotion/src/utils/compute-actions/index.ts @@ -1,3 +1,4 @@ +export * from "./buy-get" export * from "./items" export * from "./order" export * from "./shipping-methods" diff --git a/packages/types/src/auth/service.ts b/packages/types/src/auth/service.ts index 4f4bed7ecc6c1..88b72362db24b 100644 --- a/packages/types/src/auth/service.ts +++ b/packages/types/src/auth/service.ts @@ -15,6 +15,10 @@ import { Context } from "../shared-context" import { FindConfig } from "../common" import { IModuleService } from "../modules-sdk" +export type JWTGenerationOptions = { + expiresIn?: string | number +} + export interface IAuthModuleService extends IModuleService { authenticate( provider: string, @@ -72,6 +76,16 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise + generateJwtToken( + authUserId: string, + scope: string, + options?: JWTGenerationOptions + ): Promise + retrieveAuthUserFromJwtToken( + token: string, + scope: string + ): Promise + listAuthUsers( filters?: FilterableAuthProviderProps, config?: FindConfig,