diff --git a/.changeset/angry-ghosts-taste.md b/.changeset/angry-ghosts-taste.md new file mode 100644 index 0000000000000..40700194778ab --- /dev/null +++ b/.changeset/angry-ghosts-taste.md @@ -0,0 +1,7 @@ +--- +"@medusajs/workflows": patch +"@medusajs/pricing": patch +"@medusajs/medusa": patch +--- + +fix(workflows, pricing, medusa): update region variants fix + pricing module migration scripts diff --git a/.changeset/breezy-readers-shout.md b/.changeset/breezy-readers-shout.md new file mode 100644 index 0000000000000..d22d66c39cc63 --- /dev/null +++ b/.changeset/breezy-readers-shout.md @@ -0,0 +1,7 @@ +--- +"@medusajs/workflows": patch +"@medusajs/product": patch +"@medusajs/types": patch +--- + +fix(workflows, product, types): Fix issues relating to update-variant workflow and options diff --git a/.changeset/khaki-eyes-fetch.md b/.changeset/khaki-eyes-fetch.md new file mode 100644 index 0000000000000..5c20b29f3256f --- /dev/null +++ b/.changeset/khaki-eyes-fetch.md @@ -0,0 +1,5 @@ +--- +"@medusajs/admin-ui": patch +--- + +fix(admin-ui): delete edit variant prices in favor of bulk editor diff --git a/.changeset/kind-dolphins-speak.md b/.changeset/kind-dolphins-speak.md new file mode 100644 index 0000000000000..0b3b53f44e004 --- /dev/null +++ b/.changeset/kind-dolphins-speak.md @@ -0,0 +1,6 @@ +--- +"@medusajs/pricing": patch +"@medusajs/types": patch +--- + +fix(pricing,types): remove is_dynamic from model + types diff --git a/.changeset/small-apes-own.md b/.changeset/small-apes-own.md new file mode 100644 index 0000000000000..38fbbbbe16b2a --- /dev/null +++ b/.changeset/small-apes-own.md @@ -0,0 +1,5 @@ +--- +"@medusajs/workflows": patch +--- + +Fix(workflows): compensation handling diff --git a/.changeset/wet-crews-sneeze.md b/.changeset/wet-crews-sneeze.md new file mode 100644 index 0000000000000..fb5151f9131f7 --- /dev/null +++ b/.changeset/wet-crews-sneeze.md @@ -0,0 +1,8 @@ +--- +"@medusajs/orchestration": minor +"@medusajs/workflows": minor +"@medusajs/link-modules": patch +"@medusajs/medusa": patch +--- + +Workflows composer api diff --git a/.changeset/witty-waves-rhyme.md b/.changeset/witty-waves-rhyme.md new file mode 100644 index 0000000000000..db4889d661784 --- /dev/null +++ b/.changeset/witty-waves-rhyme.md @@ -0,0 +1,5 @@ +--- +"@medusajs/ui": patch +--- + +fix(ui): Update the styling of CodeBlock diff --git a/.changeset/young-items-drop.md b/.changeset/young-items-drop.md new file mode 100644 index 0000000000000..9e522b10a0d01 --- /dev/null +++ b/.changeset/young-items-drop.md @@ -0,0 +1,10 @@ +--- +"@medusajs/workflows": patch +"@medusajs/product": patch +"@medusajs/pricing": patch +"@medusajs/medusa": patch +"@medusajs/types": patch +"@medusajs/utils": patch +--- + +feat(medusa,types,workflows,utils,product,pricing): PricingModule Integration of PriceLists into Core diff --git a/.eslintignore b/.eslintignore index 0469b3263f8bf..dbe62ec0c90a2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,6 +21,8 @@ packages/* !packages/cache-inmemory !packages/create-medusa-app !packages/product +!packages/orchestration +!packages/workflows **/models/* diff --git a/.eslintrc.js b/.eslintrc.js index 1f757251da28a..83745e590b6bb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -90,14 +90,14 @@ module.exports = { "./packages/event-bus-redis/tsconfig.spec.json", "./packages/medusa-plugin-meilisearch/tsconfig.spec.json", "./packages/medusa-plugin-algolia/tsconfig.spec.json", - "./packages/admin-ui/tsconfig.json", "./packages/inventory/tsconfig.spec.json", "./packages/stock-location/tsconfig.spec.json", "./packages/cache-redis/tsconfig.spec.json", "./packages/cache-inmemory/tsconfig.spec.json", - "./packages/admin-ui/tsconfig.json", "./packages/create-medusa-app/tsconfig.json", "./packages/product/tsconfig.json", + "./packages/orchestration/tsconfig.json", + "./packages/workflows/tsconfig.spec.json", ], }, rules: { diff --git a/.github/workflows/action.yml b/.github/workflows/action.yml index 5abe7f362b4cf..ba8afeb43725c 100644 --- a/.github/workflows/action.yml +++ b/.github/workflows/action.yml @@ -125,7 +125,6 @@ jobs: env: DB_PASSWORD: postgres DB_USERNAME: postgres - SPLIT: ${{ steps['split-tests'].outputs['split'] }} integration-tests-api: needs: setup @@ -186,7 +185,8 @@ jobs: run: yarn test:integration:api env: DB_PASSWORD: postgres - SPLIT: ${{ steps['split-tests'].outputs['split'] }} + DB_USERNAME: postgres + integration-tests-plugins: needs: setup @@ -237,6 +237,7 @@ jobs: - name: Run plugin integration tests run: yarn test:integration:plugins env: + DB_USERNAME: postgres DB_PASSWORD: postgres NODE_OPTIONS: "--max_old_space_size=4096" @@ -286,4 +287,5 @@ jobs: - name: Run repository integration tests run: yarn test:integration:repositories env: + DB_USERNAME: postgres DB_PASSWORD: postgres diff --git a/docs-util/packages/typedoc-config/_base.js b/docs-util/packages/typedoc-config/_base.js index ee874baf8355b..aafaa94e69ea3 100644 --- a/docs-util/packages/typedoc-config/_base.js +++ b/docs-util/packages/typedoc-config/_base.js @@ -14,6 +14,7 @@ module.exports = { ), pluginsResolvePath: path.join(pathPrefix, "www"), exclude: [path.join(pathPrefix, "node_modules/**")], + excludeInternal: true, // Uncomment this when debugging // showConfig: true, } diff --git a/docs-util/packages/typedoc-config/extended-tsconfig/tsdoc.json b/docs-util/packages/typedoc-config/extended-tsconfig/tsdoc.json index 06df94198f8a7..e7fc48182354a 100644 --- a/docs-util/packages/typedoc-config/extended-tsconfig/tsdoc.json +++ b/docs-util/packages/typedoc-config/extended-tsconfig/tsdoc.json @@ -17,6 +17,14 @@ { "tagName": "@apiIgnore", "syntaxKind": "modifier" + }, + { + "tagName": "@mainSignature", + "syntaxKind": "modifier" + }, + { + "tagName": "@docHideSignature", + "syntaxKind": "modifier" } ] } \ No newline at end of file diff --git a/docs-util/packages/typedoc-config/extended-tsconfig/workflows.json b/docs-util/packages/typedoc-config/extended-tsconfig/workflows.json new file mode 100644 index 0000000000000..35d3b4171b784 --- /dev/null +++ b/docs-util/packages/typedoc-config/extended-tsconfig/workflows.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": [ + "../../../../packages/workflows/tsconfig.json" + ] +} \ No newline at end of file diff --git a/docs-util/packages/typedoc-config/workflows.js b/docs-util/packages/typedoc-config/workflows.js new file mode 100644 index 0000000000000..199ea0b5dbf7d --- /dev/null +++ b/docs-util/packages/typedoc-config/workflows.js @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const path = require("path") +const globalTypedocOptions = require("./_base") + +const pathPrefix = path.join(__dirname, "..", "..", "..") + +module.exports = { + ...globalTypedocOptions, + entryPoints: [ + path.join(pathPrefix, "packages/workflows/src/utils/composer/index.ts"), + ], + out: [path.join(pathPrefix, "www/apps/docs/content/references/workflows")], + tsconfig: path.join(__dirname, "extended-tsconfig", "workflows.json"), + name: "Workflows Reference", + indexTitle: "Workflows Reference", + entryDocument: "index.mdx", + hideInPageTOC: true, + hideBreadcrumbs: true, + formatting: { + "*": { + expandMembers: true, + showCommentsAsHeader: true, + sections: { + member_sources_definedIn: false, + reflection_hierarchy: false, + member_sources_inheritedFrom: false, + member_sources_implementationOf: false, + reflection_implementedBy: false, + member_signature_sources: false, + reflection_callable: false, + reflection_indexable: false, + member_signature_title: false, + member_signature_returns: false, + member_getterSetter: false, + }, + parameterStyle: "component", + parameterComponent: "ParameterTypes", + mdxImports: [ + `import ParameterTypes from "@site/src/components/ParameterTypes"`, + ], + frontmatterData: { + displayed_sidebar: "workflowsSidebar", + }, + }, + "index\\.mdx": { + reflectionGroups: { + Namespaces: false, + Enumerations: false, + Classes: false, + Interfaces: false, + "Type Aliases": false, + Variables: false, + "Enumeration Members": false, + }, + }, + functions: { + maxLevel: 1, + }, + "classes/StepResponse": { + reflectionGroups: { + Properties: false, + }, + }, + transform: { + reflectionGroups: { + "Type Parameters": false, + }, + }, + }, + objectLiteralTypeDeclarationStyle: "component", + mdxOutput: true, + maxLevel: 2, + allReflectionsHaveOwnDocument: true, + excludeExternals: true, +} diff --git a/docs-util/packages/typedoc-plugin-custom/src/index.ts b/docs-util/packages/typedoc-plugin-custom/src/index.ts index 7c4bf3348191c..8525ebdd9451b 100644 --- a/docs-util/packages/typedoc-plugin-custom/src/index.ts +++ b/docs-util/packages/typedoc-plugin-custom/src/index.ts @@ -4,6 +4,7 @@ import { load as frontmatterPlugin } from "./frontmatter-plugin" import { load as parseOasSchemaPlugin } from "./parse-oas-schema-plugin" import { load as apiIgnorePlugin } from "./api-ignore" import { load as eslintExamplePlugin } from "./eslint-example" +import { load as signatureModifierPlugin } from "./signature-modifier" export function load(app: Application) { resolveReferencesPluginLoad(app) @@ -11,4 +12,5 @@ export function load(app: Application) { parseOasSchemaPlugin(app) apiIgnorePlugin(app) eslintExamplePlugin(app) + signatureModifierPlugin(app) } diff --git a/docs-util/packages/typedoc-plugin-custom/src/signature-modifier.ts b/docs-util/packages/typedoc-plugin-custom/src/signature-modifier.ts new file mode 100644 index 0000000000000..e335ad47e9c0e --- /dev/null +++ b/docs-util/packages/typedoc-plugin-custom/src/signature-modifier.ts @@ -0,0 +1,21 @@ +import { + Application, + Context, + Converter, + ProjectReflection, + SignatureReflection, +} from "typedoc" + +export function load(app: Application) { + app.converter.on( + Converter.EVENT_CREATE_SIGNATURE, + ( + context: Context, + signature: SignatureReflection | ProjectReflection | undefined + ) => { + if (signature?.comment?.hasModifier("@hideSignature")) { + context.project.removeReflection(signature) + } + } + ) +} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/returns.ts b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/returns.ts index 2c3845fa4414d..557b105917220 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/returns.ts +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/returns.ts @@ -31,13 +31,13 @@ function getReturnFromType( return "" } - const componentItems = returnReflectionComponentFormatter( - reflection.type, - reflection.project || theme.project, - reflection.comment, - 1, - maxLevel - ) + const componentItems = returnReflectionComponentFormatter({ + reflectionType: reflection.type, + project: reflection.project || theme.project, + comment: reflection.comment, + level: 1, + maxLevel, + }) if (parameterStyle === "component") { return `<${parameterComponent} parameters={${JSON.stringify( diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/signature-title.ts b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/signature-title.ts index 3793ff45e173d..0bfebe6f1ebd9 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/signature-title.ts +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/helpers/signature-title.ts @@ -15,7 +15,7 @@ export default function (theme: MarkdownTheme) { theme.getFormattingOptionsForLocation() if (sections && sections.member_signature_title === false) { // only show title if there are more than one signatures - if (!this.parent.signatures || this.parent.signatures?.length <= 1) { + if (!this.parent.signatures || this.parent.signatures.length <= 1) { return "" } } diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.declaration.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.declaration.hbs index cf79ca107117a..2a339b5737a75 100755 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.declaration.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.declaration.hbs @@ -12,7 +12,7 @@ {{#if (sectionEnabled "member_declaration_example")}} -{{{example this 3}}} +{{{example this 4}}} {{/if}} @@ -20,7 +20,7 @@ {{#if typeParameters}} -{{titleLevel 3}} Type parameters +{{{titleLevel 4}}} Type Parameters {{#with typeParameters}} @@ -40,7 +40,7 @@ {{#with type.declaration.indexSignature}} -{{titleLevel 3}} Index signature +{{titleLevel 4}} Index signature {{{indexSignatureTitle}}} @@ -58,17 +58,17 @@ {{#if type.declaration.children}} -{{titleLevel 3}} Call signature +{{{titleLevel 4}}} Call signature {{else}} -{{titleLevel 3}} Type declaration +{{{titleLevel 4}}} Type declaration {{/if}} {{#each type.declaration.signatures}} -{{> member.signature showSources=false }} +{{> member.signature showSources=false commentLevel=5 }} {{/each}} @@ -82,7 +82,7 @@ {{#with type.declaration}} -{{titleLevel 3}} Type declaration +{{{titleLevel 4}}} Type declaration {{/with}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.getterSetter.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.getterSetter.hbs index 0657dc1fbda8e..746961b44ccb5 100755 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.getterSetter.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.getterSetter.hbs @@ -4,7 +4,7 @@ {{#with getSignature}} -{{> member.signature accessor="get" showSources=true }} +{{> member.signature accessor="get" showSources=true commentLevel=4 }} {{/with}} @@ -18,7 +18,7 @@ {{#with setSignature}} -{{> member.signature accessor="set" showSources=true }} +{{> member.signature accessor="set" showSources=true commentLevel=4 }} {{/with}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.hbs index 5062e71e6c834..91c0a4f0dd17d 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.hbs @@ -2,7 +2,7 @@ {{#if name}} -{{titleLevel 3}} {{#ifNamedAnchors}} {{/ifNamedAnchors}}{{ escape name }} +{{titleLevel 4}} {{#ifNamedAnchors}} {{/ifNamedAnchors}}{{ escape name }} {{/if}} @@ -14,7 +14,7 @@ {{#each signatures}} -{{> member.signature showSources=true }} +{{> member.signature showSources=true commentLevel=../commentLevel }} {{/each}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature.hbs index b8d21bba97fa2..7a50c41def3a3 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/member.signature.hbs @@ -14,10 +14,18 @@ {{/if}} +{{#if (sectionEnabled "member_signature_example")}} + +{{{example this commentLevel}}} + +{{/if}} + {{#if (sectionEnabled "member_signature_typeParameters")}} {{#if typeParameters}} +{{{titleLevel commentLevel}}} Type Parameters + {{#with typeParameters}} {{{typeParameter}}} @@ -28,25 +36,11 @@ {{/if}} -{{#if (sectionEnabled "member_signature_example")}} - -{{{example this 4}}} - -{{/if}} - {{#if (sectionEnabled "member_signature_parameters")}} {{#if parameters}} -{{#if showSources}} - -{{{titleLevel 4}}} Parameters - -{{else}} - -{{{titleLevel 5}}} Parameters - -{{/if}} +{{{titleLevel commentLevel}}} Parameters {{#with parameters}} @@ -62,15 +56,7 @@ {{#if type}} -{{#if showSources}} - -{{{titleLevel 4}}} Returns - -{{else}} - -{{{titleLevel 5}}} Returns - -{{/if}} +{{{titleLevel commentLevel}}} Returns {{#if (sectionEnabled "member_signature_returns")}} @@ -92,7 +78,7 @@ {{#each declaration.signatures}} -{{> member.signature showSources=false }} +{{> member.signature showSources=false commentLevel=commentLevel }} {{/each}} @@ -126,7 +112,7 @@ {{#if hasVisibleComponent}} -{{{comments this false true 4 ..}}} +{{{comments this false true commentLevel ..}}} {{/if}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.group.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.group.hbs index 89b24733a98d1..b32fb78432f79 100755 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.group.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.group.hbs @@ -26,7 +26,7 @@ ___ {{#each children}} -{{> member}} +{{> member commentLevel=5}} {{/each}} @@ -54,7 +54,7 @@ ___ {{#each children}} -{{> member}} +{{> member commentLevel=5}} {{/each}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.hbs index 08f7e8134a8b5..c00601347a3ba 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/members.hbs @@ -16,7 +16,7 @@ {{#unless hasOwnDocument}} -{{> member}} +{{> member commentLevel=4}} {{/unless}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/section-title.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/section-title.hbs new file mode 100644 index 0000000000000..e863067f41ab3 --- /dev/null +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/partials/section-title.hbs @@ -0,0 +1,9 @@ +{{#if showSources}} + +{{{titleLevel 4}}} {{{title}}} + +{{else}} + +{{{titleLevel 5}}} {{{title}}} + +{{/if}} \ No newline at end of file diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.hbs index 7ae033c57476d..6c63ea1f9cb46 100755 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.hbs @@ -94,7 +94,7 @@ ### {{name}} -{{> member.signature showSources=true }} +{{> member.signature showSources=true commentLevel=4 }} {{/each}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.member.hbs b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.member.hbs index 78129be63b6a5..8b2a6554b242f 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.member.hbs +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/resources/templates/reflection.member.hbs @@ -4,6 +4,6 @@ {{#with model}} -{{> member}} +{{> member showSources=false commentLevel=4}} {{/with}} diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/theme.ts b/docs-util/packages/typedoc-plugin-markdown-medusa/src/theme.ts index 6a501e56e42af..1b739b1efbb17 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/theme.ts +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/theme.ts @@ -5,7 +5,6 @@ import { PageEvent, ProjectReflection, Reflection, - ReflectionGroup, ReflectionKind, RenderTemplate, Renderer, @@ -359,22 +358,45 @@ export class MarkdownTheme extends Theme { this.location = page.url this.reflection = page.model instanceof DeclarationReflection ? page.model : undefined - const options = this.getFormattingOptionsForLocation() - if (this.reflection && this.reflection.groups) { - // filter out unwanted groups - const tempGroups: ReflectionGroup[] = [] - this.reflection.groups.forEach((reflectionGroup) => { - if ( - !options.reflectionGroups || - !(reflectionGroup.title in options.reflectionGroups) || - options.reflectionGroups[reflectionGroup.title] - ) { - tempGroups.push(reflectionGroup) - } - }) - this.reflection.groups = tempGroups + if ( + page.model instanceof DeclarationReflection || + page.model instanceof ProjectReflection + ) { + this.removeGroups(page.model) + } + + if ( + this.reflection instanceof DeclarationReflection && + this.reflection.signatures + ) { + // check if any of its signature has the `@mainSignature` tag + // and if so remove other signatures + const mainSignatureIndex = this.reflection.signatures.findIndex( + (signature) => signature.comment?.hasModifier("@mainSignature") + ) + + if (mainSignatureIndex !== -1) { + const mainSignature = this.reflection.signatures[mainSignatureIndex] + this.reflection.signatures = [mainSignature] + } + } + } + + protected removeGroups(model?: DeclarationReflection | ProjectReflection) { + if (!model?.groups) { + return } + + const options = this.getFormattingOptionsForLocation() + + model.groups = model.groups.filter((reflectionGroup) => { + return ( + !options.reflectionGroups || + !(reflectionGroup.title in options.reflectionGroups) || + options.reflectionGroups[reflectionGroup.title] + ) + }) } get globalsFile() { diff --git a/docs-util/packages/typedoc-plugin-markdown-medusa/src/utils/return-reflection-formatter.ts b/docs-util/packages/typedoc-plugin-markdown-medusa/src/utils/return-reflection-formatter.ts index 1155fe28526df..eb930e1cdf2cc 100644 --- a/docs-util/packages/typedoc-plugin-markdown-medusa/src/utils/return-reflection-formatter.ts +++ b/docs-util/packages/typedoc-plugin-markdown-medusa/src/utils/return-reflection-formatter.ts @@ -4,6 +4,7 @@ import { ProjectReflection, ReflectionFlags, SomeType, + TypeParameterReflection, } from "typedoc" import * as Handlebars from "handlebars" import getType from "./type-utils" @@ -14,19 +15,27 @@ import { } from "./reflection-formatter" import { MarkdownTheme } from "../theme" -export function returnReflectionComponentFormatter( - reflectionType: SomeType, - project: ProjectReflection, - comment?: Comment, - level = 1, +type ReturnReflectionComponentFormatterParams = { + reflectionType: SomeType + project: ProjectReflection + comment?: Comment + level: number maxLevel?: number | undefined -): Parameter[] { +} + +export function returnReflectionComponentFormatter({ + reflectionType, + project, + comment, + level = 1, + maxLevel, +}: ReturnReflectionComponentFormatterParams): Parameter[] { const typeName = getType(reflectionType, "object", false, true) const type = getType(reflectionType, "object") const componentItem: Parameter[] = [] + const canRetrieveChildren = level + 1 <= (maxLevel || MarkdownTheme.MAX_LEVEL) if (reflectionType.type === "reference") { - // put type name as a title and its referenced items as children. - if (reflectionType.typeArguments) { + if (reflectionType.typeArguments || reflectionType.refersToTypeParameter) { const parentKey = componentItem.push({ name: "name" in reflectionType ? reflectionType.name : typeName, type, @@ -45,21 +54,30 @@ export function returnReflectionComponentFormatter( featureFlag: Handlebars.helpers.featureFlag(comment), children: [], }) + const typeArgs = reflectionType.typeArguments + ? reflectionType.typeArguments + : "typeParameters" in reflectionType + ? (reflectionType.typeParameters as TypeParameterReflection[]) + : undefined if ( - !isOnlyVoid(reflectionType.typeArguments) && - level + 1 <= (maxLevel || MarkdownTheme.MAX_LEVEL) + typeArgs && + !isOnlyVoid(typeArgs as unknown as SomeType[]) && + canRetrieveChildren ) { - reflectionType.typeArguments.forEach((typeArg) => { - const typeArgComponent = returnReflectionComponentFormatter( - typeArg, - project, - undefined, - level + 1, - maxLevel - ) - if (typeArgComponent.length) { - componentItem[parentKey - 1].children?.push(...typeArgComponent) + typeArgs.forEach((typeArg) => { + const reflectionTypeArg = + typeArg instanceof TypeParameterReflection ? typeArg.type : typeArg + if (!reflectionTypeArg) { + return } + const typeArgComponent = returnReflectionComponentFormatter({ + reflectionType: reflectionTypeArg, + project, + level: level + 1, + maxLevel, + }) + + componentItem[parentKey - 1].children?.push(...typeArgComponent) }) } } else { @@ -107,17 +125,14 @@ export function returnReflectionComponentFormatter( featureFlag: Handlebars.helpers.featureFlag(comment), children: [], }) - if (level + 1 <= (maxLevel || MarkdownTheme.MAX_LEVEL)) { - const elementTypeItem = returnReflectionComponentFormatter( - reflectionType.elementType, + if (canRetrieveChildren) { + const elementTypeItem = returnReflectionComponentFormatter({ + reflectionType: reflectionType.elementType, project, - undefined, - level + 1, - maxLevel - ) - if (elementTypeItem.length) { - componentItem[parentKey - 1].children?.push(...elementTypeItem) - } + level: level + 1, + maxLevel, + }) + componentItem[parentKey - 1].children?.push(...elementTypeItem) } } else if (reflectionType.type === "tuple") { let pushTo: Parameter[] = [] @@ -145,18 +160,15 @@ export function returnReflectionComponentFormatter( } else { pushTo = componentItem } - if (level + 1 <= (maxLevel || MarkdownTheme.MAX_LEVEL)) { + if (canRetrieveChildren) { reflectionType.elements.forEach((element) => { - const elementTypeItem = returnReflectionComponentFormatter( - element, + const elementTypeItem = returnReflectionComponentFormatter({ + reflectionType: element, project, - undefined, - level + 1, - maxLevel - ) - if (elementTypeItem.length) { - pushTo.push(...elementTypeItem) - } + level: level + 1, + maxLevel, + }) + pushTo.push(...elementTypeItem) }) } } else { diff --git a/docs-util/packages/utils/src/get-type-children.ts b/docs-util/packages/utils/src/get-type-children.ts index bfe4b143f76cc..e3e79dcb25c19 100644 --- a/docs-util/packages/utils/src/get-type-children.ts +++ b/docs-util/packages/utils/src/get-type-children.ts @@ -65,7 +65,15 @@ export function getTypeChildren( children = getTypeChildren(reflectionType.elementType, project) } - return children + return filterChildren(children) +} + +const REJECTED_CHILDREN_NAMES = ["__type"] + +function filterChildren(children: DeclarationReflection[]) { + return children.filter( + (child) => !REJECTED_CHILDREN_NAMES.includes(child.name) + ) } function removeChild(name: unknown, children: DeclarationReflection[]) { diff --git a/integration-tests/environment-helpers/use-db.js b/integration-tests/environment-helpers/use-db.js index b5ccf47ff0e03..261ccfac95521 100644 --- a/integration-tests/environment-helpers/use-db.js +++ b/integration-tests/environment-helpers/use-db.js @@ -32,6 +32,8 @@ const keepTables = [ "payment_provider", "country", "currency", + "migrations", + "mikro_orm_migrations", ] const DbTestUtil = { @@ -52,21 +54,23 @@ const DbTestUtil = { teardown: async function ({ forceDelete } = {}) { forceDelete = forceDelete || [] - const entities = this.db_.entityMetadatas const manager = this.db_.manager await manager.query(`SET session_replication_role = 'replica';`) + const tableNames = await manager.query(`SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public';`) - for (const entity of entities) { + for (const { table_name } of tableNames) { if ( - keepTables.includes(entity.tableName) && - !forceDelete.includes(entity.tableName) + keepTables.includes(table_name) && + !forceDelete.includes(table_name) ) { continue } await manager.query(`DELETE - FROM "${entity.tableName}";`) + FROM "${table_name}";`) } await manager.query(`SET session_replication_role = 'origin';`) diff --git a/integration-tests/factories/index.ts b/integration-tests/factories/index.ts index 40a248a49467f..5240f082972c3 100644 --- a/integration-tests/factories/index.ts +++ b/integration-tests/factories/index.ts @@ -2,6 +2,7 @@ export * from "./simple-batch-job-factory" export * from "./simple-cart-factory" export * from "./simple-custom-shipping-option-factory" export * from "./simple-customer-factory" +export * from "./simple-customer-group-factory" export * from "./simple-discount-factory" export * from "./simple-gift-card-factory" export * from "./simple-line-item-factory" @@ -22,5 +23,5 @@ export * from "./simple-shipping-method-factory" export * from "./simple-shipping-option-factory" export * from "./simple-shipping-profile-factory" export * from "./simple-shipping-tax-rate-factory" -export * from "./simple-tax-rate-factory" export * from "./simple-store-factory" +export * from "./simple-tax-rate-factory" diff --git a/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts new file mode 100644 index 0000000000000..1caaef078d067 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/add-price-list-price-batch.spec.ts @@ -0,0 +1,191 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists/:id/prices/batch", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update money amounts if variant id is present in prices", async () => { + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + }, + ]) + + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + prices: [ + { + variant_id: variant.id, + amount: 5000, + currency_code: "usd", + }, + ], + } + + await api.post( + `admin/price-lists/${priceList.id}/prices/batch`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts new file mode 100644 index 0000000000000..831251c1752e5 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/create-price-list.spec.ts @@ -0,0 +1,191 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + await simpleCustomerGroupFactory(dbConnection, { + id: "customer-group-1", + name: "Test Group", + }) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create price list and money amounts", async () => { + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [{ id: "customer-group-1" }], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + + let response = await api.get( + `/admin/price-lists/${result.data.price_list.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: null, + ends_at: null, + customer_groups: [ + { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "Test Group", + metadata: null, + }, + ], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 400, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts new file mode 100644 index 0000000000000..ec8e17fd03fd2 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-product.ts @@ -0,0 +1,181 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" +import { AxiosInstance } from "axios" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id/products/:productId/batch", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant1 + let priceSet + let priceListId + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + const api = useApi()! as AxiosInstance + + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant1 = product.variants[0] + + priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant1.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant1.id, + currency_code: "usd", + }, + ], + } + + const priceListResult = await api.post( + `admin/price-lists`, + data, + adminHeaders + ) + priceListId = priceListResult.data.price_list.id + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete prices in batch based on product ids", async () => { + const api = useApi()! as AxiosInstance + + let priceSetMoneyAmounts = + await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + expect(priceSetMoneyAmounts.length).toEqual(2) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/products/prices/batch`, + { + headers: adminHeaders.headers, + data: { + product_ids: [product.id], + }, + } + ) + expect(deleteRes.status).toEqual(200) + + priceSetMoneyAmounts = await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + + expect(priceSetMoneyAmounts.length).toEqual(1) + expect(priceSetMoneyAmounts).toEqual([ + expect.objectContaining({ + price_list: null, + }), + ]) + }) + + it("should delete prices based on single product id", async () => { + const api = useApi()! as AxiosInstance + + let priceSetMoneyAmounts = + await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + expect(priceSetMoneyAmounts.length).toEqual(2) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/products/${product.id}/prices`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + priceSetMoneyAmounts = await pricingModuleService.listPriceSetMoneyAmounts({ + price_set_id: [priceSet.id], + }) + + expect(priceSetMoneyAmounts.length).toEqual(1) + expect(priceSetMoneyAmounts).toEqual([ + expect.objectContaining({ + price_list: null, + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts new file mode 100644 index 0000000000000..cc0639a075d98 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices-by-variant.ts @@ -0,0 +1,132 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id/variants/:variantId/prices", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete all prices based on product variant ids", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + const priceListId = result.data.price_list.id + + let psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/variants/${variant.id}/prices`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(0) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts new file mode 100644 index 0000000000000..9924193e3e8d0 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-prices.ts @@ -0,0 +1,146 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete price list prices by money amount ids", async () => { + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + { + amount: 4000, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const res = await api.post(`admin/price-lists`, data, adminHeaders) + + const priceListId = res.data.price_list.id + let psmas = await pricingModuleService.listPriceSetMoneyAmounts( + { + price_list_id: [priceListId], + }, + { relations: ["money_amount"] } + ) + + expect(psmas.length).toEqual(2) + + const deletePrice = psmas[0].money_amount + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}/prices/batch`, + { + data: { + price_ids: [deletePrice?.id], + }, + ...adminHeaders, + } + ) + expect(deleteRes.status).toEqual(200) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts new file mode 100644 index 0000000000000..47064fed15a20 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/delete-price-list-spec.ts @@ -0,0 +1,145 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { IPricingModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("DELETE /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should delete price list and money amounts", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "test price list", + description: "test", + type: "override", + customer_groups: [], + status: "active", + prices: [ + { + amount: 400, + variant_id: variant.id, + currency_code: "usd", + }, + ], + } + + const result = await api.post(`admin/price-lists`, data, adminHeaders) + const priceListId = result.data.price_list.id + + const getResponse = await api.get( + `/admin/price-lists/${priceListId}`, + adminHeaders + ) + expect(getResponse.status).toEqual(200) + + let psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(1) + + const deleteRes = await api.delete( + `/admin/price-lists/${priceListId}`, + adminHeaders + ) + expect(deleteRes.status).toEqual(200) + + const afterDelete = await api + .get(`/admin/price-lists/${priceListId}`, adminHeaders) + .catch((err) => { + return err + }) + expect(afterDelete.response.status).toEqual(404) + + psmas = await pricingModuleService.listPriceSetMoneyAmounts({ + price_list_id: [priceListId], + }) + expect(psmas.length).toEqual(0) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts new file mode 100644 index 0000000000000..9eed1dd60766c --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/get-price-list.spec.ts @@ -0,0 +1,204 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get price list and its money amounts with variants", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + await pricingModuleService.createPriceLists([ + { + title: "test price list 1", + description: "test 1", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }) + ) + }) + + it("should throw an error when price list is not found", async () => { + const api = useApi() as any + + const error = await api + .get(`/admin/price-lists/does-not-exist`, adminHeaders) + .catch((e) => e) + + expect(error.response.status).toBe(404) + expect(error.response.data).toEqual({ + type: "not_found", + message: "Price list with id: does-not-exist was not found", + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts new file mode 100644 index 0000000000000..1395fa75d854d --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list-products.spec.ts @@ -0,0 +1,274 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists/:id/products", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let product2 + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + title: "uniquely fun product", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + + product2 = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant-2", + title: "uniquely fun product 2", + variants: [ + { + options: [{ option_id: "test-product-option-2", value: "test 2" }], + }, + ], + options: [ + { + id: "test-product-option-2", + title: "Test option 2", + }, + ], + }) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should list all products in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + let response = await api.get( + `/admin/price-lists/${priceList.id}/products`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + handle: expect.any(String), + subtitle: null, + description: null, + is_giftcard: false, + status: "draft", + thumbnail: null, + weight: null, + length: null, + height: null, + width: null, + origin_country: null, + hs_code: null, + mid_code: null, + material: null, + collection_id: null, + collection: null, + type_id: null, + type: null, + discountable: true, + external_id: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + metadata: null, + }), + ]) + + response = await api.get( + `/admin/products?price_list_id[]=${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + title: expect.any(String), + handle: expect.any(String), + subtitle: null, + description: null, + is_giftcard: false, + status: "draft", + thumbnail: null, + weight: null, + length: null, + height: null, + width: null, + origin_country: null, + hs_code: null, + mid_code: null, + material: null, + collection_id: null, + collection: null, + type_id: null, + type: null, + discountable: true, + external_id: null, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + metadata: null, + }), + ]) + }) + + it("should list all products constrained by search query in a price list", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + let response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=shouldnotreturnanything`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(0) + expect(response.data.products).toEqual([]) + + response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=uniquely`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + }), + ]) + + response = await api.get( + `/admin/price-lists/${priceList.id}/products?q=`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: expect.any(String), + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts new file mode 100644 index 0000000000000..fbe8a9830f2c6 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/list-price-list.spec.ts @@ -0,0 +1,170 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { simpleProductFactory } from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /admin/price-lists", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get price list and its money amounts with variants", async () => { + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 5000, + currency_code: "usd", + price_set_id: priceSet.id, + }, + ], + }, + ]) + + const api = useApi() as any + + const response = await api.get(`/admin/price-lists`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.price_lists).toEqual([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "test price list", + description: "test", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [], + prices: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: expect.any(String), + region_id: null, + variant: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + title: expect.any(String), + product_id: expect.any(String), + sku: null, + barcode: null, + ean: null, + upc: null, + variant_rank: 0, + inventory_quantity: 10, + allow_backorder: false, + manage_inventory: true, + hs_code: null, + origin_country: null, + mid_code: null, + material: null, + weight: null, + length: null, + height: null, + width: null, + metadata: null, + }), + variant_id: expect.any(String), + }), + ], + }), + ]) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts b/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts new file mode 100644 index 0000000000000..20dfecf877b24 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/admin/update-price-list.spec.ts @@ -0,0 +1,226 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/price-lists/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let variant2 + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + await simpleCustomerGroupFactory(dbConnection, { + id: "customer-group-2", + name: "Test Group 2", + }) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + { + options: [{ option_id: "test-product-option-2", value: "test 2" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + { + id: "test-product-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + variant2 = product.variants[1] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should update price lists successfully with prices", async () => { + const var2PriceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant2.id, + prices: [], + }) + + const [priceList] = await pricingModuleService.createPriceLists([ + { + title: "test price list", + description: "test", + ends_at: new Date(), + starts_at: new Date(), + status: PriceListStatus.ACTIVE, + type: PriceListType.OVERRIDE, + prices: [ + { + amount: 3000, + currency_code: "usd", + price_set_id: var2PriceSet.id, + }, + ], + }, + ]) + + await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + ], + }) + + const api = useApi() as any + const data = { + name: "new price list name", + description: "new price list description", + customer_groups: [{ id: "customer-group-2" }], + prices: [ + { + variant_id: variant.id, + amount: 5000, + currency_code: "usd", + }, + { + id: priceList?.price_set_money_amounts?.[0].money_amount?.id, + amount: 6000, + currency_code: "usd", + variant_id: variant2.id, + }, + ], + } + + await api.post(`admin/price-lists/${priceList.id}`, data, adminHeaders) + + const response = await api.get( + `/admin/price-lists/${priceList.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.price_list).toEqual( + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "new price list name", + description: "new price list description", + type: "override", + status: "active", + starts_at: expect.any(String), + ends_at: expect.any(String), + customer_groups: [ + { + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + name: "Test Group 2", + metadata: null, + }, + ], + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 5000, + min_quantity: null, + max_quantity: null, + price_list_id: priceList.id, + region_id: null, + variant: expect.objectContaining({ + id: variant.id, + }), + variant_id: variant.id, + }), + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + currency_code: "usd", + amount: 6000, + min_quantity: null, + max_quantity: null, + price_list_id: priceList.id, + region_id: null, + variant: expect.objectContaining({ + id: variant2.id, + }), + variant_id: variant2.id, + }), + ]), + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/price-lists/store/get-product.ts b/integration-tests/plugins/__tests__/price-lists/store/get-product.ts new file mode 100644 index 0000000000000..ad5915f910e02 --- /dev/null +++ b/integration-tests/plugins/__tests__/price-lists/store/get-product.ts @@ -0,0 +1,289 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleCustomerFactory, + simpleCustomerGroupFactory, + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { + IPricingModuleService, + PriceListStatus, + PriceListType, +} from "@medusajs/types" +import { AxiosInstance } from "axios" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" +import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("GET /store/products/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + let priceSetId + let pricingModuleService: IPricingModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + pricingModuleService = appContainer.resolve("pricingModuleService") + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + status: "published", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + + const priceSet = await createVariantPriceSet({ + container: appContainer, + variantId: variant.id, + prices: [ + { + amount: 3000, + currency_code: "usd", + rules: {}, + }, + { + amount: 4000, + currency_code: "usd", + rules: {}, + }, + ], + rules: [], + }) + + priceSetId = priceSet.id + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get product and its prices from price-list created through the price list workflow", async () => { + const api = useApi()! as AxiosInstance + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd` + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toHaveLength(2) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + expect.objectContaining({ + currency_code: "usd", + amount: 2500, + min_quantity: null, + max_quantity: null, + price_list_id: priceListResponse.data.price_list.id, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 2500, + calculated_price_type: "sale", + }) + ) + }) + + it("should not list prices from price-list with customer groups if not logged in", async () => { + const api = useApi()! as AxiosInstance + + const { id: customerGroupId } = await simpleCustomerGroupFactory( + dbConnection + ) + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + customer_groups: [{ id: customerGroupId }], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd` + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 3000, + calculated_price_type: null, + }) + ) + }) + + it("should list prices from price-list with customer groups", async () => { + const api = useApi()! as AxiosInstance + + await simpleCustomerFactory(dbConnection, { + id: "test-customer-5-pl", + email: "test5@email-pl.com", + first_name: "John", + last_name: "Deere", + password_hash: + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: true, + groups: [{ id: "customer-group-1" }], + }) + + const authResponse = await api.post("/store/auth", { + email: "test5@email-pl.com", + password: "test", + }) + + const [authCookie] = authResponse.headers["set-cookie"][0].split(";") + + const priceListResponse = await api.post( + `/admin/price-lists`, + { + name: "test price list", + description: "test", + status: PriceListStatus.ACTIVE, + type: PriceListType.SALE, + prices: [ + { + amount: 2500, + currency_code: "usd", + variant_id: variant.id, + }, + ], + customer_groups: [{ id: "customer-group-1" }], + }, + adminHeaders + ) + + let response = await api.get( + `/store/products/${product.id}?currency_code=usd`, + { + headers: { + Cookie: authCookie, + }, + } + ) + + expect(response.status).toEqual(200) + expect(response.data.product.variants[0].prices).toHaveLength(2) + expect(response.data.product.variants[0].prices).toEqual([ + expect.objectContaining({ + currency_code: "usd", + amount: 3000, + min_quantity: null, + max_quantity: null, + price_list_id: null, + }), + expect.objectContaining({ + currency_code: "usd", + amount: 2500, + min_quantity: null, + max_quantity: null, + price_list_id: priceListResponse.data.price_list.id, + }), + ]) + expect(response.data.product.variants[0]).toEqual( + expect.objectContaining({ + original_price: 3000, + calculated_price: 2500, + calculated_price_type: "sale", + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/pricing/get-product.ts b/integration-tests/plugins/__tests__/pricing/get-product.ts index 0e7a95912867b..2d68c82f4dd07 100644 --- a/integration-tests/plugins/__tests__/pricing/get-product.ts +++ b/integration-tests/plugins/__tests__/pricing/get-product.ts @@ -8,6 +8,7 @@ import path from "path" import { startBootstrapApp } from "../../../environment-helpers/bootstrap-app" import { getContainer } from "../../../environment-helpers/use-container" import adminSeeder from "../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../helpers/create-default-rule-types" jest.setTimeout(5000000) @@ -46,6 +47,7 @@ describe("Link Modules", () => { }) beforeEach(async () => { + await createDefaultRuleTypes(medusaContainer) await adminSeeder(dbConnection) await simpleRegionFactory(dbConnection, { id: "region-1", diff --git a/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts new file mode 100644 index 0000000000000..b4482f482a540 --- /dev/null +++ b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts @@ -0,0 +1,205 @@ +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleRegionFactory, +} from "../../../../factories" + +import { PricingModuleService } from "@medusajs/pricing" +import { ProductModuleService } from "@medusajs/product" +import { AxiosInstance } from "axios" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import adminSeeder from "../../../../helpers/admin-seeder" +import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" + +jest.setTimeout(50000) + +const adminHeaders = { + headers: { + "x-medusa-access-token": "test_token", + }, +} + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("POST /admin/products/:id/variants", () => { + let dbConnection + let appContainer + let shutdownServer + let product + let variant + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + await createDefaultRuleTypes(appContainer) + + await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-variant", + variants: [ + { + options: [{ option_id: "test-product-option-1", value: "test" }], + }, + ], + options: [ + { + id: "test-product-option-1", + title: "Test option 1", + }, + ], + }) + + variant = product.variants[0] + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should create a product variant with its price sets and prices through the workflow", async () => { + const api = useApi()! as AxiosInstance + const data = { + title: "test variant create", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + material: "boo", + mid_code: "234asdfadsf", + hs_code: "asdfasdf234", + origin_country: "DE", + sku: "asdf", + ean: "234", + upc: "234", + barcode: "asdf", + inventory_quantity: 234, + manage_inventory: true, + allow_backorder: true, + weight: 234, + width: 234, + height: 234, + length: 234, + metadata: { asdf: "asdf" }, + options: [{ option_id: "test-product-option-1", value: "test option" }], + } + + let response = await api.post( + `/admin/products/${product.id}/variants`, + data, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + title: "test variant create", + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 66600, + currency_code: "usd", + region_id: "test-region", + }), + expect.objectContaining({ + amount: 55500, + currency_code: "usd", + }), + ]), + }), + ]), + }) + ) + }) + + it("should compensate creating product variants when error throws in future step", async () => { + jest + .spyOn(PricingModuleService.prototype, "create") + .mockImplementation(() => { + throw new Error("Random Error") + }) + + const productSpy = jest.spyOn( + ProductModuleService.prototype, + "deleteVariants" + ) + + const api = useApi()! as AxiosInstance + const data = { + title: "test variant create", + prices: [ + { + amount: 66600, + region_id: "test-region", + }, + { + amount: 55500, + currency_code: "usd", + region_id: null, + }, + ], + material: "boo", + mid_code: "234asdfadsf", + hs_code: "asdfasdf234", + origin_country: "DE", + sku: "asdf", + ean: "234", + upc: "234", + barcode: "asdf", + inventory_quantity: 234, + manage_inventory: true, + allow_backorder: true, + weight: 234, + width: 234, + height: 234, + length: 234, + metadata: { asdf: "asdf" }, + options: [{ option_id: "test-product-option-1", value: "test option" }], + } + + await api + .post(`/admin/products/${product.id}/variants`, data, adminHeaders) + .catch((e) => e) + + expect(productSpy).toBeCalledWith([expect.any(String)]) + + const getProductResponse = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + expect(getProductResponse.data.product.variants).toHaveLength(1) + }) +}) diff --git a/integration-tests/plugins/__tests__/product/admin/create-product.ts b/integration-tests/plugins/__tests__/product/admin/create-product.ts index 7471d5ee45a65..a286b03e77f5e 100644 --- a/integration-tests/plugins/__tests__/product/admin/create-product.ts +++ b/integration-tests/plugins/__tests__/product/admin/create-product.ts @@ -1,11 +1,13 @@ import { initDb, useDb } from "../../../../environment-helpers/use-db" import { Region } from "@medusajs/medusa" +import { IPricingModuleService } from "@medusajs/types" import { AxiosInstance } from "axios" 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 { simpleSalesChannelFactory } from "../../../../factories" import adminSeeder from "../../../../helpers/admin-seeder" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" @@ -21,7 +23,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products", () => { +describe("POST /admin/products", () => { let dbConnection let appContainer let shutdownServer @@ -50,6 +52,8 @@ describe("[Product & Pricing Module] POST /admin/products", () => { currency_code: "usd", tax_rate: 0, }) + + await simpleSalesChannelFactory(dbConnection, { is_default: true }) }) afterEach(async () => { @@ -110,5 +114,12 @@ describe("[Product & Pricing Module] POST /admin/products", () => { ]), }), }) + + const pricingModuleService: IPricingModuleService = appContainer.resolve( + "pricingModuleService" + ) + + const [_, count] = await pricingModuleService.listAndCount() + expect(count).toEqual(1) }) }) diff --git a/integration-tests/plugins/__tests__/product/admin/index.ts b/integration-tests/plugins/__tests__/product/admin/index.ts index c46ace8b44d0f..3d388e03aba65 100644 --- a/integration-tests/plugins/__tests__/product/admin/index.ts +++ b/integration-tests/plugins/__tests__/product/admin/index.ts @@ -441,6 +441,7 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await createDefaultRuleTypes(medusaContainer) await simpleSalesChannelFactory(dbConnection, { name: "Default channel", diff --git a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts index 590d340457107..2f76bdd06c6d6 100644 --- a/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/update-product-variant.spec.ts @@ -1,5 +1,3 @@ -import { useApi } from "../../../../environment-helpers/use-api" -import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" import { simpleProductFactory, @@ -7,11 +5,13 @@ import { } from "../../../../factories" import { AxiosInstance } from "axios" -import path from "path" -import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import adminSeeder from "../../../../helpers/admin-seeder" import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types" import { createVariantPriceSet } from "../../../helpers/create-variant-price-set" +import { getContainer } from "../../../../environment-helpers/use-container" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" jest.setTimeout(50000) @@ -25,7 +25,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", () => { +describe("POST /admin/products/:id/variants/:id", () => { let dbConnection let appContainer let shutdownServer @@ -62,6 +62,9 @@ describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", () { options: [{ option_id: "test-product-option-1", value: "test" }], }, + { + options: [{ option_id: "test-product-option-1", value: "test 2" }], + }, ], options: [ { @@ -250,4 +253,231 @@ describe("[Product & Pricing Module] POST /admin/products/:id/variants/:id", () }) ) }) + + it("should update variant option value", async () => { + const api = useApi()! as AxiosInstance + + const data = { + options: [ + { + option_id: "test-product-option-1", + value: "updated", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-option-1", + value: "updated", + }), + ], + }), + expect.objectContaining({ + id: product.variants[1].id, + options: [ + expect.objectContaining({ + option_id: "test-product-option-1", + value: "test 2", + }), + ], + }), + ]), + }) + ) + }) + + it("should update variant metadata", async () => { + const api = useApi()! as AxiosInstance + + const data = { + metadata: { + test: "string", + }, + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + id: variant.id, + metadata: { + test: "string", + }, + }), + ]), + }) + ) + }) + + it("should remove options not present in update", async () => { + const api = useApi()! as AxiosInstance + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-multiple-options", + variants: [ + { + options: [ + { option_id: "test-product-multi-option-1", value: "test" }, + { option_id: "test-product-multi-option-2", value: "test value" }, + ], + }, + ], + options: [ + { + id: "test-product-multi-option-1", + title: "Test option 1", + }, + { + id: "test-product-multi-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + + const data = { + options: [ + { + option_id: "test-product-multi-option-1", + value: "updated", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: [ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-multi-option-1", + value: "updated", + }), + ], + }), + ], + }) + ) + }) + + it("should update several options in the same api call", async () => { + const api = useApi()! as AxiosInstance + + product = await simpleProductFactory(dbConnection, { + id: "test-product-with-multiple-options", + variants: [ + { + options: [ + { option_id: "test-product-multi-option-1", value: "test" }, + { option_id: "test-product-multi-option-2", value: "test value" }, + ], + }, + ], + options: [ + { + id: "test-product-multi-option-1", + title: "Test option 1", + }, + { + id: "test-product-multi-option-2", + title: "Test option 2", + }, + ], + }) + + variant = product.variants[0] + + const data = { + options: [ + { + option_id: "test-product-multi-option-1", + value: "updated", + }, + { + option_id: "test-product-multi-option-2", + value: "updated 2", + }, + ], + } + + await api.post( + `/admin/products/${product.id}/variants/${variant.id}`, + data, + adminHeaders + ) + + const response = await api.get( + `/admin/products/${product.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + id: expect.any(String), + variants: [ + expect.objectContaining({ + id: variant.id, + options: [ + expect.objectContaining({ + option_id: "test-product-multi-option-1", + value: "updated", + }), + expect.objectContaining({ + option_id: "test-product-multi-option-2", + value: "updated 2", + }), + ], + }), + ], + }) + ) + }) }) diff --git a/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts index b631eb0a1564b..db9750f26429d 100644 --- a/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/update-product.spec.ts @@ -23,7 +23,7 @@ const env = { MEDUSA_FF_MEDUSA_V2: true, } -describe("[Product & Pricing Module] POST /admin/products/:id", () => { +describe("POST /admin/products/:id", () => { let dbConnection let appContainer let shutdownServer @@ -109,10 +109,11 @@ describe("[Product & Pricing Module] POST /admin/products/:id", () => { ) expect(response.status).toEqual(200) + expect(response.data.product.variants).toHaveLength(1) expect(response.data.product).toEqual( expect.objectContaining({ id: expect.any(String), - variants: expect.arrayContaining([ + variants: [ expect.objectContaining({ id: variant.id, title: "test variant update", @@ -128,7 +129,7 @@ describe("[Product & Pricing Module] POST /admin/products/:id", () => { }), ]), }), - ]), + ], }) ) }) diff --git a/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts b/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts new file mode 100644 index 0000000000000..0bec8edef9cc0 --- /dev/null +++ b/integration-tests/plugins/__tests__/workflows/utils/composer/compose.ts @@ -0,0 +1,1882 @@ +import { promiseAll } from "@medusajs/utils" +import { + createStep, + createWorkflow, + hook, + parallelize, + StepResponse, + transform, +} from "@medusajs/workflows" + +jest.setTimeout(30000) + +describe("Workflow composer", function () { + describe("Using steps returning plain values", function () { + afterEach(async () => { + jest.clearAllMocks() + }) + + it("should compose a new workflow and execute it", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [workflowInput], + obj: "return from 1", + }, + two: { + inputs: [ + { + inputs: [workflowInput], + obj: "return from 1", + }, + ], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [workflowInput], + obj: "return from 1", + }, + two: { + inputs: [ + { + inputs: [workflowInput], + obj: "return from 1", + }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows sequentially and execute them sequentially", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflow2 = createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + const workflow2Input = { test: "payload2" } + const { result: workflow2Result } = await workflow2().run({ + input: workflow2Input, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [{ test: "payload1" }], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [{ test: "payload2" }], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows concurrently and execute them sequentially", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const [workflow, workflow2] = await promiseAll([ + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + + createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + ]) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + const workflow2Input = { test: "payload2" } + const { result: workflow2Result } = await workflow2().run({ + input: workflow2Input, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [workflow2Input], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows concurrently and execute them concurrently", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const [workflow, workflow2] = await promiseAll([ + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + + createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + ]) + + const workflowInput = { test: "payload1" } + const workflow2Input = { test: "payload2" } + + const [{ result: workflowResult }, { result: workflow2Result }] = + await promiseAll([ + workflow().run({ + input: workflowInput, + }), + workflow2().run({ + input: workflow2Input, + }), + ]) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + expect(mockStep1Fn.mock.calls[1]).toHaveLength(2) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [workflow2Input], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose a new workflow and execute it multiple times concurrently", async () => { + const mockStep1Fn = jest + .fn() + .mockImplementation(function (input, context) { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation(function (...inputs) { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation(function (...inputs) { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const workflowInput2 = { test: "payload2" } + + const [{ result: workflowResult }, { result: workflowResult2 }] = + await promiseAll([ + workflow().run({ + input: workflowInput, + }), + workflow().run({ + input: workflowInput2, + }), + ]) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1]).toHaveLength(2) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflowResult2).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose a new workflow with parallelize steps", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + const mockStep4Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 4", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + const step4 = createStep("step4", mockStep4Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const [ret2, ret3] = parallelize(step2(returnStep1), step3(returnStep1)) + return step4({ one: ret2, two: ret3 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep4Fn).toHaveBeenCalledTimes(1) + expect(mockStep4Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep4Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 3", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 3", + }, + }, + ], + obj: "return from 4", + }) + }) + + it("should overwrite existing workflows if the same name is used", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return { inputs: [input], obj: "return from 1" } + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 2", + } + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 3", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const overriddenWorkflow = createWorkflow("workflow1", function (input) { + const ret2 = step2(input) + const returnStep1 = step1(ret2) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await overriddenWorkflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 2", + }) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 2" }], + obj: "return from 1", + }, + two: { inputs: [{ test: "payload1" }], obj: "return from 2" }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 2" }, + ], + obj: "return from 1", + }, + two: { inputs: [{ test: "payload1" }], obj: "return from 2" }, + }, + ], + obj: "return from 3", + }) + }) + + it("should transform the values before forward them to the next step", async () => { + const mockStep1Fn = jest.fn().mockImplementation((obj, context) => { + const ret = { + property: "property", + } + return ret + }) + + const mockStep2Fn = jest.fn().mockImplementation((obj, context) => { + const ret = { + ...obj, + sum: "sum = " + obj.sum, + } + + return ret + }) + + const mockStep3Fn = jest.fn().mockImplementation((param, context) => { + const ret = { + avg: "avg = " + param.avg, + ...param, + } + return ret + }) + + const transform1Fn = jest + .fn() + .mockImplementation(({ input, step1Result }) => { + const newObj = { + ...step1Result, + ...input, + sum: input.a + input.b, + } + return { + input: newObj, + } + }) + + const transform2Fn = jest + .fn() + .mockImplementation(async ({ input }, context) => { + input.another_prop = "another_prop" + return input + }) + + const transform3Fn = jest.fn().mockImplementation(({ obj }) => { + obj.avg = (obj.a + obj.b) / 2 + + return obj + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const mainFlow = createWorkflow("test_", function (input) { + const step1Result = step1(input) + + const sum = transform( + { input, step1Result }, + transform1Fn, + transform2Fn + ) + + const ret2 = step2(sum) + + const avg = transform({ obj: ret2 }, transform3Fn) + + return step3(avg) + }) + + const workflowInput = { a: 1, b: 2 } + await mainFlow().run({ input: workflowInput }) + + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + property: "property", + a: 1, + b: 2, + sum: 3, + another_prop: "another_prop", + }) + + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + sum: "sum = 3", + property: "property", + a: 1, + b: 2, + another_prop: "another_prop", + avg: 1.5, + }) + + expect(transform1Fn).toHaveBeenCalledTimes(1) + expect(transform2Fn).toHaveBeenCalledTimes(1) + expect(transform3Fn).toHaveBeenCalledTimes(1) + }) + + it("should compose a new workflow and access properties from steps", async () => { + const mockStep1Fn = jest.fn().mockImplementation(({ input }, context) => { + return { id: input, product: "product_1", variant: "variant_2" } + }) + const mockStep2Fn = jest.fn().mockImplementation(({ product }) => { + return { + product: "Saved product - " + product, + } + }) + const mockStep3Fn = jest.fn().mockImplementation(({ variant }) => { + return { + variant: "Saved variant - " + variant, + } + }) + + const getData = createStep("step1", mockStep1Fn) + const saveProduct = createStep("step2", mockStep2Fn) + const saveVariant = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const data: any = getData(input) + parallelize( + saveProduct({ product: data.product }), + saveVariant({ variant: data.variant }) + ) + }) + + const workflowInput = "id_123" + await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ product: "product_1" }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ variant: "variant_2" }) + }) + + it("should compose a new workflow exposing hooks and log warns if multiple handlers are registered for the same hook", async () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}) + + const mockStep1Fn = jest.fn().mockImplementation(({ input }) => { + return { id: input, product: "product_1", variant: "variant_2" } + }) + + const mockStep2Fn = jest.fn().mockImplementation(({ product }) => { + product.product = "Saved product - " + product.product + return product + }) + + const getData = createStep("step1", mockStep1Fn) + const saveProduct = createStep("step2", mockStep2Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const data = getData({ input }) + + const hookReturn = hook("changeProduct", { + opinionatedPropertyName: data, + }) + const transformedData = transform( + { data, hookReturn }, + ({ data, hookReturn }: { data: any; hookReturn: any }) => { + return { + ...data, + ...hookReturn, + } + } + ) + + return saveProduct({ product: transformedData }) + }) + + workflow.changeProduct(({ opinionatedPropertyName }) => { + return { + newProperties: "new properties", + prod: opinionatedPropertyName.product + "**", + var: opinionatedPropertyName.variant + "**", + other: [1, 2, 3], + nested: { + a: { + b: "c", + }, + }, + moreProperties: "more properties", + } + }) + + workflow.changeProduct((theReturnOfThePreviousHook) => { + return { + ...theReturnOfThePreviousHook, + moreProperties: "2nd hook update", + } + }) + + const workflowInput = "id_123" + const { result: final } = await workflow().run({ + input: workflowInput, + }) + + expect(warn).toHaveBeenCalledTimes(1) + expect(final).toEqual({ + id: "id_123", + prod: "product_1**", + var: "variant_2**", + variant: "variant_2", + product: "Saved product - product_1", + newProperties: "new properties", + other: [1, 2, 3], + nested: { + a: { + b: "c", + }, + }, + moreProperties: "more properties", + }) + }) + }) + + describe("Using steps returning StepResponse", function () { + afterEach(async () => { + jest.clearAllMocks() + }) + + it("should compose a new workflow and execute it", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [workflowInput], + obj: "return from 1", + }, + two: { + inputs: [ + { + inputs: [workflowInput], + obj: "return from 1", + }, + ], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [workflowInput], + obj: "return from 1", + }, + two: { + inputs: [ + { + inputs: [workflowInput], + obj: "return from 1", + }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows sequentially and execute them sequentially", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflow2 = createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + const workflow2Input = { test: "payload2" } + const { result: workflow2Result } = await workflow2().run({ + input: workflow2Input, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [{ test: "payload1" }], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [{ test: "payload2" }], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows concurrently and execute them sequentially", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const [workflow, workflow2] = await promiseAll([ + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + + createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + ]) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + const workflow2Input = { test: "payload2" } + const { result: workflow2Result } = await workflow2().run({ + input: workflow2Input, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [workflow2Input], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose two new workflows concurrently and execute them concurrently", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const [workflow, workflow2] = await promiseAll([ + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + + createWorkflow("workflow2", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }), + ]) + + const workflowInput = { test: "payload1" } + const workflow2Input = { test: "payload2" } + + const [{ result: workflowResult }, { result: workflow2Result }] = + await promiseAll([ + workflow().run({ + input: workflowInput, + }), + workflow2().run({ + input: workflow2Input, + }), + ]) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflow2Input) + expect(mockStep1Fn.mock.calls[1]).toHaveLength(2) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + expect(mockStep2Fn.mock.calls[1][0]).toEqual({ + inputs: [workflow2Input], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + expect(mockStep3Fn.mock.calls[1][0]).toEqual({ + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload2" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflow2Result).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose a new workflow and execute it multiple times concurrently", async () => { + const mockStep1Fn = jest + .fn() + .mockImplementation(function (input, context) { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation(function (...inputs) { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation(function (...inputs) { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const workflowInput2 = { test: "payload2" } + + const [{ result: workflowResult }, { result: workflowResult2 }] = + await promiseAll([ + workflow().run({ + input: workflowInput, + }), + workflow().run({ + input: workflowInput2, + }), + ]) + + expect(mockStep1Fn).toHaveBeenCalledTimes(2) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockStep1Fn.mock.calls[1]).toHaveLength(2) + + expect(mockStep2Fn).toHaveBeenCalledTimes(2) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(2) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload1" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + expect(workflowResult2).toEqual({ + inputs: [ + { + one: { inputs: [{ test: "payload2" }], obj: "return from 1" }, + two: { + inputs: [ + { inputs: [{ test: "payload2" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + }, + ], + obj: "return from 3", + }) + }) + + it("should compose a new workflow with parallelize steps", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + const mockStep4Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return { + inputs, + obj: "return from 4", + } + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + const step4 = createStep("step4", mockStep4Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const [ret2, ret3] = parallelize(step2(returnStep1), step3(returnStep1)) + return step4({ one: ret2, two: ret3 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 1", + }) + + expect(mockStep4Fn).toHaveBeenCalledTimes(1) + expect(mockStep4Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep4Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 2", + }, + two: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 1" }], + obj: "return from 3", + }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 2", + }, + two: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 1" }, + ], + obj: "return from 3", + }, + }, + ], + obj: "return from 4", + }) + }) + + it("should overwrite existing workflows if the same name is used", async () => { + const mockStep1Fn = jest.fn().mockImplementation((input, context) => { + return new StepResponse({ inputs: [input], obj: "return from 1" }) + }) + const mockStep2Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 2", + }) + }) + const mockStep3Fn = jest.fn().mockImplementation((...inputs) => { + const context = inputs.pop() + return new StepResponse({ + inputs, + obj: "return from 3", + }) + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + createWorkflow("workflow1", function (input) { + const returnStep1 = step1(input) + const ret2 = step2(returnStep1) + return step3({ one: returnStep1, two: ret2 }) + }) + + const overriddenWorkflow = createWorkflow("workflow1", function (input) { + const ret2 = step2(input) + const returnStep1 = step1(ret2) + return step3({ one: returnStep1, two: ret2 }) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await overriddenWorkflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual({ + inputs: [workflowInput], + obj: "return from 2", + }) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + one: { + inputs: [{ inputs: [{ test: "payload1" }], obj: "return from 2" }], + obj: "return from 1", + }, + two: { inputs: [{ test: "payload1" }], obj: "return from 2" }, + }) + + expect(workflowResult).toEqual({ + inputs: [ + { + one: { + inputs: [ + { inputs: [{ test: "payload1" }], obj: "return from 2" }, + ], + obj: "return from 1", + }, + two: { inputs: [{ test: "payload1" }], obj: "return from 2" }, + }, + ], + obj: "return from 3", + }) + }) + + it("should transform the values before forward them to the next step", async () => { + const mockStep1Fn = jest.fn().mockImplementation((obj, context) => { + const ret = new StepResponse({ + property: "property", + }) + return ret + }) + + const mockStep2Fn = jest.fn().mockImplementation((obj, context) => { + const ret = new StepResponse({ + ...obj, + sum: "sum = " + obj.sum, + }) + + return ret + }) + + const mockStep3Fn = jest.fn().mockImplementation((param, context) => { + const ret = new StepResponse({ + avg: "avg = " + param.avg, + ...param, + }) + return ret + }) + + const transform1Fn = jest + .fn() + .mockImplementation(({ input, step1Result }) => { + const newObj = { + ...step1Result, + ...input, + sum: input.a + input.b, + } + return { + input: newObj, + } + }) + + const transform2Fn = jest + .fn() + .mockImplementation(async ({ input }, context) => { + input.another_prop = "another_prop" + return input + }) + + const transform3Fn = jest.fn().mockImplementation(({ obj }) => { + obj.avg = (obj.a + obj.b) / 2 + + return obj + }) + + const step1 = createStep("step1", mockStep1Fn) + const step2 = createStep("step2", mockStep2Fn) + const step3 = createStep("step3", mockStep3Fn) + + const mainFlow = createWorkflow("test_", function (input) { + const step1Result = step1(input) + + const sum = transform( + { input, step1Result }, + transform1Fn, + transform2Fn + ) + + const ret2 = step2(sum) + + const avg = transform({ obj: ret2 }, transform3Fn) + + return step3(avg) + }) + + const workflowInput = { a: 1, b: 2 } + await mainFlow().run({ input: workflowInput }) + + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ + property: "property", + a: 1, + b: 2, + sum: 3, + another_prop: "another_prop", + }) + + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ + sum: "sum = 3", + property: "property", + a: 1, + b: 2, + another_prop: "another_prop", + avg: 1.5, + }) + + expect(transform1Fn).toHaveBeenCalledTimes(1) + expect(transform2Fn).toHaveBeenCalledTimes(1) + expect(transform3Fn).toHaveBeenCalledTimes(1) + }) + + it("should compose a new workflow and access properties from steps", async () => { + const mockStep1Fn = jest.fn().mockImplementation(({ input }, context) => { + return new StepResponse({ + id: input, + product: "product_1", + variant: "variant_2", + }) + }) + const mockStep2Fn = jest.fn().mockImplementation(({ product }) => { + return new StepResponse({ + product: "Saved product - " + product, + }) + }) + const mockStep3Fn = jest.fn().mockImplementation(({ variant }) => { + return new StepResponse({ + variant: "Saved variant - " + variant, + }) + }) + + const getData = createStep("step1", mockStep1Fn) + const saveProduct = createStep("step2", mockStep2Fn) + const saveVariant = createStep("step3", mockStep3Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const data: any = getData(input) + parallelize( + saveProduct({ product: data.product }), + saveVariant({ variant: data.variant }) + ) + }) + + const workflowInput = "id_123" + await workflow().run({ + input: workflowInput, + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockStep1Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + + expect(mockStep2Fn).toHaveBeenCalledTimes(1) + expect(mockStep2Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep2Fn.mock.calls[0][0]).toEqual({ product: "product_1" }) + + expect(mockStep3Fn).toHaveBeenCalledTimes(1) + expect(mockStep3Fn.mock.calls[0]).toHaveLength(2) + expect(mockStep3Fn.mock.calls[0][0]).toEqual({ variant: "variant_2" }) + }) + + it("should compose a new workflow exposing hooks and log warns if multiple handlers are registered for the same hook", async () => { + const warn = jest.spyOn(console, "warn").mockImplementation(() => {}) + + const mockStep1Fn = jest.fn().mockImplementation(({ input }) => { + return new StepResponse({ + id: input, + product: "product_1", + variant: "variant_2", + }) + }) + + const mockStep2Fn = jest.fn().mockImplementation(({ product }) => { + product.product = "Saved product - " + product.product + return new StepResponse(product) + }) + + const getData = createStep("step1", mockStep1Fn) + const saveProduct = createStep("step2", mockStep2Fn) + + const workflow = createWorkflow("workflow1", function (input) { + const data = getData({ input }) + + const hookReturn = hook("changeProduct", { + opinionatedPropertyName: data, + }) + const transformedData = transform( + { data, hookReturn }, + ({ data, hookReturn }: { data: any; hookReturn: any }) => { + return { + ...data, + ...hookReturn, + } + } + ) + + return saveProduct({ product: transformedData }) + }) + + workflow.changeProduct(({ opinionatedPropertyName }) => { + return { + newProperties: "new properties", + prod: opinionatedPropertyName.product + "**", + var: opinionatedPropertyName.variant + "**", + other: [1, 2, 3], + nested: { + a: { + b: "c", + }, + }, + moreProperties: "more properties", + } + }) + + workflow.changeProduct((theReturnOfThePreviousHook) => { + return { + ...theReturnOfThePreviousHook, + moreProperties: "2nd hook update", + } + }) + + const workflowInput = "id_123" + const { result: final } = await workflow().run({ + input: workflowInput, + }) + + expect(warn).toHaveBeenCalledTimes(1) + expect(final).toEqual({ + id: "id_123", + prod: "product_1**", + var: "variant_2**", + variant: "variant_2", + product: "Saved product - product_1", + newProperties: "new properties", + other: [1, 2, 3], + nested: { + a: { + b: "c", + }, + }, + moreProperties: "more properties", + }) + }) + }) + + it("should compose a workflow that throws without crashing and the compensation will receive undefined for the step that fails", async () => { + const mockStep1Fn = jest.fn().mockImplementation(function (input) { + throw new Error("invoke fail") + }) + + const mockCompensateSte1 = jest.fn().mockImplementation(function (input) { + return input + }) + + const step1 = createStep("step1", mockStep1Fn, mockCompensateSte1) + + const workflow = createWorkflow("workflow1", function (input) { + return step1(input) + }) + + const workflowInput = { test: "payload1" } + const { errors } = await workflow().run({ + input: workflowInput, + throwOnError: false, + }) + + expect(errors).toHaveLength(1) + expect(errors[0]).toEqual({ + action: "step1", + handlerType: "invoke", + error: expect.objectContaining({ + message: "invoke fail", + }), + }) + + expect(mockStep1Fn).toHaveBeenCalledTimes(1) + expect(mockCompensateSte1).toHaveBeenCalledTimes(1) + + expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput) + expect(mockCompensateSte1.mock.calls[0][0]).toEqual(undefined) + }) +}) diff --git a/integration-tests/plugins/helpers/create-default-rule-types.ts b/integration-tests/plugins/helpers/create-default-rule-types.ts index e063d642bb78c..6db9fb25ef3e4 100644 --- a/integration-tests/plugins/helpers/create-default-rule-types.ts +++ b/integration-tests/plugins/helpers/create-default-rule-types.ts @@ -10,5 +10,9 @@ export const createDefaultRuleTypes = async (container) => { name: "region_id", rule_attribute: "region_id", }, + { + name: "customer_group_id", + rule_attribute: "customer_group_id", + }, ]) } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index e3c07aa393c05..35a2ab67ecce5 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -5,7 +5,7 @@ "license": "MIT", "private": true, "scripts": { - "test:integration": "node --expose-gc ./../../node_modules/.bin/jest --silent=false --runInBand --bail --detectOpenHandles --logHeapUsage --clearMocks --no-compilation-cache --forceExit", + "test:integration": "node --expose-gc ./../../node_modules/.bin/jest --silent=false --runInBand --bail --detectOpenHandles --logHeapUsage --clearMocks --forceExit", "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { diff --git a/packages/admin-ui/ui/src/components/forms/general/prices-form/index.tsx b/packages/admin-ui/ui/src/components/forms/general/prices-form/index.tsx deleted file mode 100644 index c89b4bc55d028..0000000000000 --- a/packages/admin-ui/ui/src/components/forms/general/prices-form/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useAdminRegions, useAdminStore } from "medusa-react" -import { useEffect, useMemo } from "react" -import { FieldArrayWithId, useFieldArray } from "react-hook-form" -import { NestedForm } from "../../../../utils/nested-form" -import NestedPrice from "./nested-price" - -type PricePayload = { - id: string | null - amount: number | null - currency_code: string - region_id: string | null - includes_tax?: boolean -} - -type PriceObject = FieldArrayWithId< - { - __nested__: PricesFormType - }, - "__nested__.prices", - "id" -> & { index: number } - -export type PricesFormType = { - prices: PricePayload[] -} - -export type NestedPriceObject = { - currencyPrice: PriceObject - regionPrices: (PriceObject & { regionName: string })[] -} - -type Props = { - form: NestedForm - required?: boolean -} - -/** - * Re-usable nested form used to submit pricing information for products and their variants. - * Fetches store currencies and regions from the backend, and allows the user to specify both - * currency and region specific prices. - * @example - * - */ -const PricesForm = ({ form }: Props) => { - const { store } = useAdminStore() - const { regions } = useAdminRegions() - - const { control, path } = form - - const { append, update, fields } = useFieldArray({ - control, - name: path("prices"), - }) - - useEffect(() => { - if (!regions || !store || !fields) { - return - } - - regions.forEach((reg) => { - if (!fields.some((field) => field.region_id === reg.id)) { - append({ - id: null, - region_id: reg.id, - amount: null, - currency_code: reg.currency_code, - includes_tax: reg.includes_tax, - }) - } - }) - - store.currencies.forEach((cur) => { - if (!fields.some((field) => field.currency_code === cur.code)) { - append({ - id: null, - currency_code: cur.code, - amount: null, - region_id: null, - includes_tax: cur.includes_tax, - }) - } - }) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [regions, store, fields]) - - // Ensure that prices are up to date with their respective tax inclusion setting - useEffect(() => { - if (!regions || !fields || !store) { - return - } - - regions.forEach((reg) => { - const regionPrice = fields.findIndex( - (field) => !!field && field.region_id === reg.id - ) - - if ( - regionPrice !== -1 && - fields[regionPrice].includes_tax !== reg.includes_tax - ) { - update(regionPrice, { - ...fields[regionPrice], - includes_tax: reg.includes_tax, - }) - } - }) - - store.currencies.forEach((cur) => { - const currencyPrice = fields.findIndex( - (field) => - !!field && !field.region_id && field.currency_code === cur.code - ) - - if ( - currencyPrice !== -1 && - fields[currencyPrice].includes_tax !== cur.includes_tax - ) { - update(currencyPrice, { - ...fields[currencyPrice], - includes_tax: cur.includes_tax, - }) - } - }) - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [regions, store]) - - const priceObj = useMemo(() => { - const obj: Record = {} - - const currencyPrices = fields.filter((field) => field.region_id === null) - const regionPrices = fields.filter((field) => field.region_id !== null) - - currencyPrices.forEach((price) => { - obj[price.currency_code!] = { - currencyPrice: { - ...price, - index: fields.indexOf(price), - }, - regionPrices: regionPrices - .filter( - (regionPrice) => regionPrice.currency_code === price.currency_code - ) - .map((rp) => ({ - ...rp, - regionName: regions?.find((r) => r.id === rp.region_id)?.name || "", - index: fields.indexOf(rp), - })), - } - }) - - return obj - }, [fields, regions]) - - return ( - - - {Object.values(priceObj).map((po) => { - return ( - - ) - })} - - - ) -} - -export default PricesForm diff --git a/packages/admin-ui/ui/src/components/forms/general/prices-form/nested-price.tsx b/packages/admin-ui/ui/src/components/forms/general/prices-form/nested-price.tsx deleted file mode 100644 index 8a073b5abd52c..0000000000000 --- a/packages/admin-ui/ui/src/components/forms/general/prices-form/nested-price.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { NestedPriceObject, PricesFormType } from "." - -import CoinsIcon from "../../../fundamentals/icons/coins-icon" -import { Controller } from "react-hook-form" -import IncludesTaxTooltip from "../../../atoms/includes-tax-tooltip" -import MapPinIcon from "../../../fundamentals/icons/map-pin-icon" -import { NestedForm } from "../../../../utils/nested-form" -import PriceFormInput from "./price-form-input" -import TriangleRightIcon from "../../../fundamentals/icons/triangle-right-icon" -import clsx from "clsx" -import { currencies } from "../../../../utils/currencies" -import useToggleState from "../../../../hooks/use-toggle-state" - -type Props = { - form: NestedForm - nestedPrice: NestedPriceObject -} - -const NestedPrice = ({ form, nestedPrice }: Props) => { - const { state, toggle } = useToggleState() - - const { control, path } = form - const { currencyPrice, regionPrices } = nestedPrice - return ( - - - - - - - - - - - - {currencyPrice.currency_code.toUpperCase()} - - - {currencies[currencyPrice.currency_code.toUpperCase()].name} - - - - - { - return ( - - ) - }} - /> - - - {regionPrices.map((rp) => { - return ( - - - - - - - - {rp.regionName} - - - - - { - return ( - - ) - }} - /> - - ) - })} - - - ) -} - -export default NestedPrice diff --git a/packages/admin-ui/ui/src/components/forms/product/variant-form/create-flow-variant-form/index.tsx b/packages/admin-ui/ui/src/components/forms/product/variant-form/create-flow-variant-form/index.tsx index c4a758ae8f791..1ac5bfd99e9cf 100644 --- a/packages/admin-ui/ui/src/components/forms/product/variant-form/create-flow-variant-form/index.tsx +++ b/packages/admin-ui/ui/src/components/forms/product/variant-form/create-flow-variant-form/index.tsx @@ -1,28 +1,26 @@ -import { UseFormReturn } from "react-hook-form" -import { nestedForm } from "../../../../../utils/nested-form" -import InputError from "../../../../atoms/input-error" -import IconTooltip from "../../../../molecules/icon-tooltip" -import Accordion from "../../../../organisms/accordion" -import { PricesFormType } from "../../../general/prices-form" import CustomsForm, { CustomsFormType } from "../../customs-form" import DimensionsForm, { DimensionsFormType } from "../../dimensions-form" import VariantGeneralForm, { VariantGeneralFormType, } from "../variant-general-form" -import VariantPricesForm from "../variant-prices-form" import VariantSelectOptionsForm, { VariantOptionValueType, VariantSelectOptionsFormType, } from "../variant-select-options-form" import VariantStockForm, { VariantStockFormType } from "../variant-stock-form" +import Accordion from "../../../../organisms/accordion" +import IconTooltip from "../../../../molecules/icon-tooltip" +import InputError from "../../../../atoms/input-error" +import { UseFormReturn } from "react-hook-form" +import { nestedForm } from "../../../../../utils/nested-form" + export type CreateFlowVariantFormType = { /** * Used to identify the variant during product create flow. Will not be submitted to the backend. */ _internal_id?: string general: VariantGeneralFormType - prices: PricesFormType stock: VariantStockFormType options: VariantSelectOptionsFormType customs: CustomsFormType @@ -77,9 +75,6 @@ const CreateFlowVariantForm = ({ form, options, onCreateOption }: Props) => { - - - diff --git a/packages/admin-ui/ui/src/components/forms/product/variant-form/edit-flow-variant-form/index.tsx b/packages/admin-ui/ui/src/components/forms/product/variant-form/edit-flow-variant-form/index.tsx index 1059aa5e31e92..9ab784d243bf3 100644 --- a/packages/admin-ui/ui/src/components/forms/product/variant-form/edit-flow-variant-form/index.tsx +++ b/packages/admin-ui/ui/src/components/forms/product/variant-form/edit-flow-variant-form/index.tsx @@ -1,19 +1,18 @@ -import { useFieldArray, UseFormReturn } from "react-hook-form" import CustomsForm, { CustomsFormType } from "../../customs-form" import DimensionsForm, { DimensionsFormType } from "../../dimensions-form" +import MetadataForm, { MetadataFormType } from "../../../general/metadata-form" +import { UseFormReturn, useFieldArray } from "react-hook-form" import VariantGeneralForm, { VariantGeneralFormType, } from "../variant-general-form" import VariantStockForm, { VariantStockFormType } from "../variant-stock-form" -import { useFeatureFlag } from "../../../../../providers/feature-flag-provider" -import { nestedForm } from "../../../../../utils/nested-form" +import Accordion from "../../../../organisms/accordion" import IconTooltip from "../../../../molecules/icon-tooltip" import InputField from "../../../../molecules/input" -import Accordion from "../../../../organisms/accordion" -import MetadataForm, { MetadataFormType } from "../../../general/metadata-form" import { PricesFormType } from "../../../general/prices-form" -import VariantPricesForm from "../variant-prices-form" +import { nestedForm } from "../../../../../utils/nested-form" +import { useFeatureFlag } from "../../../../../providers/feature-flag-provider" export type EditFlowVariantFormType = { /** @@ -97,9 +96,6 @@ const EditFlowVariantForm = ({ form, isEdit }: Props) => { - - - {showStockAndInventory && ( diff --git a/packages/admin-ui/ui/src/components/forms/product/variant-form/variant-prices-form/index.tsx b/packages/admin-ui/ui/src/components/forms/product/variant-form/variant-prices-form/index.tsx deleted file mode 100644 index 87a558c6a1b31..0000000000000 --- a/packages/admin-ui/ui/src/components/forms/product/variant-form/variant-prices-form/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { NestedForm } from "../../../../../utils/nested-form" -import PricesForm, { PricesFormType } from "../../../general/prices-form" - -type Props = { - form: NestedForm -} - -const VariantPricesForm = ({ form }: Props) => { - return ( - - - Configure the pricing for this variant. - - - - - - ) -} - -export default VariantPricesForm diff --git a/packages/admin-ui/ui/src/components/organisms/product-variants-section/edit-prices-modal/index.tsx b/packages/admin-ui/ui/src/components/organisms/product-variants-section/edit-prices-modal/index.tsx index 1ce14aab7e19c..1879a88e2c3d3 100644 --- a/packages/admin-ui/ui/src/components/organisms/product-variants-section/edit-prices-modal/index.tsx +++ b/packages/admin-ui/ui/src/components/organisms/product-variants-section/edit-prices-modal/index.tsx @@ -1,27 +1,26 @@ import { MoneyAmount, Product } from "@medusajs/client-types" -import mapKeys from "lodash/mapKeys" -import pick from "lodash/pick" -import pickBy from "lodash/pickBy" +import { + getAllProductPricesCurrencies, + getAllProductPricesRegions, + getCurrencyPricesOnly, + getRegionPricesOnly, +} from "./utils" import { useAdminRegions, useAdminUpdateVariant } from "medusa-react" import { useEffect, useMemo, useRef, useState } from "react" -import { currencies as CURRENCY_MAP } from "../../../../utils/currencies" - -import useNotification from "../../../../hooks/use-notification" -import Fade from "../../../atoms/fade-wrapper" import Button from "../../../fundamentals/button" +import { currencies as CURRENCY_MAP } from "../../../../utils/currencies" import CrossIcon from "../../../fundamentals/icons/cross-icon" -import Modal from "../../../molecules/modal" import DeletePrompt from "../../delete-prompt" import EditPricesActions from "./edit-prices-actions" import EditPricesTable from "./edit-prices-table" +import Fade from "../../../atoms/fade-wrapper" +import Modal from "../../../molecules/modal" import SavePrompt from "./save-prompt" -import { - getAllProductPricesCurrencies, - getAllProductPricesRegions, - getCurrencyPricesOnly, - getRegionPricesOnly, -} from "./utils" +import mapKeys from "lodash/mapKeys" +import pick from "lodash/pick" +import pickBy from "lodash/pickBy" +import useNotification from "../../../../hooks/use-notification" type EditPricesModalProps = { close: () => void diff --git a/packages/design-system/ui/src/components/code-block/code-block.tsx b/packages/design-system/ui/src/components/code-block/code-block.tsx index b617159822be0..8371c15487b96 100644 --- a/packages/design-system/ui/src/components/code-block/code-block.tsx +++ b/packages/design-system/ui/src/components/code-block/code-block.tsx @@ -138,22 +138,62 @@ const Body = ({ ...themes.palenight, plain: { color: "rgba(249, 250, 251, 1)", - backgroundColor: "#111827", + backgroundColor: "rgb(17,24,39)", }, styles: [ + ...themes.palenight.styles, { types: ["keyword"], style: { - color: "var(--fg-on-color)", + fontStyle: "normal", + color: "rgb(187,160,255)", + }, + }, + { + types: ["punctuation", "operator"], + style: { + fontStyle: "normal", + color: "rgb(255,255,255)", + }, + }, + { + types: ["constant", "boolean"], + style: { + fontStyle: "normal", + color: "rgb(187,77,96)", + }, + }, + { + types: ["function"], + style: { + fontStyle: "normal", + color: "rgb(27,198,242)", + }, + }, + { + types: ["number"], + style: { + color: "rgb(247,208,25)", }, }, { types: ["maybe-class-name"], style: { - color: "rgb(255, 203, 107)", + color: "rgb(255,203,107)", + }, + }, + { + types: ["string"], + style: { + color: "rgb(73,209,110)", + }, + }, + { + types: ["comment"], + style: { + color: "rgb(52,211,153)", }, }, - ...themes.palenight.styles, ], }} code={active.code} diff --git a/packages/link-modules/src/index.ts b/packages/link-modules/src/index.ts index 3acb2f2c61577..4bbfce7a57d4d 100644 --- a/packages/link-modules/src/index.ts +++ b/packages/link-modules/src/index.ts @@ -2,3 +2,4 @@ export * from "./initialize" export * from "./types" export * from "./loaders" export * from "./services" +export * from "./utils/compose-link-name" diff --git a/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts index 64bd889064e14..7e2b51109dc1e 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/add-prices-batch.ts @@ -1,12 +1,15 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { updatePriceLists } from "@medusajs/workflows" +import { Type } from "class-transformer" import { IsArray, IsBoolean, IsOptional, ValidateNested } from "class-validator" +import { EntityManager } from "typeorm" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." - -import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" -import { Type } from "class-transformer" +import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { validator } from "../../../../utils/validator" -import { EntityManager } from "typeorm" +import { MedusaContainer } from "@medusajs/types" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists/{id}/prices/batch @@ -85,23 +88,48 @@ import { EntityManager } from "typeorm" */ export default async (req, res) => { const { id } = req.params + let priceList + const featureFlagRouter = req.scope.resolve("featureFlagRouter") + const manager: EntityManager = req.scope.resolve("manager") + const priceListService: PriceListService = + req.scope.resolve("priceListService") const validated = await validator(AdminPostPriceListPricesPricesReq, req.body) - const priceListService: PriceListService = - req.scope.resolve("priceListService") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const updatePriceListWorkflow = updatePriceLists(req.scope) - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .addPrices(id, validated.prices, validated.override) - }) + const input = { + price_lists: [ + { + id, + ...validated, + }, + ], + } + + await updatePriceListWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + await manager.transaction(async (transactionManager) => { + await priceListService + .withTransaction(transactionManager) + .addPrices(id, validated.prices, validated.override) + }) - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts index fc9fc6e7126e3..1d1cbaf1d9271 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/create-price-list.ts @@ -1,3 +1,12 @@ +import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types" +import { + FlagRouter, + MedusaV2Flag, + PriceListStatus, + PriceListType, +} from "@medusajs/utils" +import { createPriceLists } from "@medusajs/workflows" +import { Type } from "class-transformer" import { IsArray, IsBoolean, @@ -6,23 +15,18 @@ import { IsString, ValidateNested, } from "class-validator" -import { - AdminPriceListPricesCreateReq, - CreatePriceListInput, -} from "../../../../types/price-list" - -import { PriceListStatus, PriceListType } from "@medusajs/utils" -import { Type } from "class-transformer" import { Request } from "express" import { EntityManager } from "typeorm" +import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing" import { PriceList } from "../../../../models" import PriceListService from "../../../../services/price-list" -import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { - defaultAdminPriceListFields, - defaultAdminPriceListRelations, -} from "./index" + AdminPriceListPricesCreateReq, + CreatePriceListInput, +} from "../../../../types/price-list" +import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists @@ -109,16 +113,57 @@ export default async (req: Request, res) => { req.scope.resolve("priceListService") const manager: EntityManager = req.scope.resolve("manager") - let priceList = await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .create(req.validatedBody as CreatePriceListInput) - }) - - priceList = await priceListService.retrieve(priceList.id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + let priceList + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const createPriceListWorkflow = createPriceLists(req.scope) + const validatedInput = req.validatedBody as CreatePriceListInput + const rules: PricingTypes.CreatePriceListRules = {} + const customerGroups = validatedInput?.customer_groups || [] + delete validatedInput.customer_groups + + if (customerGroups.length) { + rules["customer_group_id"] = customerGroups.map((cg) => cg.id) + } + + const input = { + price_lists: [ + { + ...validatedInput, + rules, + }, + ], + } as WorkflowTypes.PriceListWorkflow.CreatePriceListWorkflowInputDTO + + const { result } = await createPriceListWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = result[0]!.priceList + + priceList = await getPriceListPricingModule(priceList.id, { + container: req.scope as MedusaContainer, + }) + } else { + priceList = await manager.transaction(async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .create(req.validatedBody as CreatePriceListInput) + }) + + priceList = await priceListService.retrieve(priceList.id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts index 54af5cfaafe6c..94db28f0d6772 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-price-list.ts @@ -1,3 +1,6 @@ +import { WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceLists } from "@medusajs/workflows" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" @@ -56,12 +59,33 @@ import PriceListService from "../../../../services/price-list" export default async (req, res) => { const { id } = req.params - const priceListService: PriceListService = - req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService.withTransaction(transactionManager).delete(id) - }) + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const removePriceListsWorkflow = removePriceLists(req.scope) + + const input = { + price_lists: [id], + } as WorkflowTypes.PriceListWorkflow.RemovePriceListWorkflowInputDTO + + await removePriceListsWorkflow.run({ + input, + context: { + manager, + }, + }) + } else { + const priceListService: PriceListService = + req.scope.resolve("priceListService") + await manager.transaction(async (transactionManager) => { + await priceListService.withTransaction(transactionManager).delete(id) + }) + } res.json({ id, diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts index 5de013889f5ff..96fa6f8d09473 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-prices-batch.ts @@ -1,8 +1,10 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-price-list-prices" import { ArrayNotEmpty, IsString } from "class-validator" - import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" import { validator } from "../../../../utils/validator" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/prices/batch @@ -81,13 +83,37 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deletePrices(id, validated.price_ids) - }) + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key + ) + + if (isMedusaV2FlagEnabled) { + const deletePriceListPricesWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + price_list_id: id, + money_amount_ids: validated.price_ids, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListPricesWorkflowInputDTO + + await deletePriceListPricesWorkflow.run({ + input, + context: { + manager, + }, + }) + } else { + await manager.transaction(async (transactionManager) => { + await priceListService + .withTransaction(transactionManager) + .deletePrices(id, validated.price_ids) + }) + } res.json({ ids: validated.price_ids, object: "money-amount", deleted: true }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts index 6c424c9624cbb..d12de2e6e461f 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-product-prices.ts @@ -1,3 +1,6 @@ +import { WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows/dist/definition/price-list/remove-product-prices" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" @@ -60,15 +63,44 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteProductPrices(id, [product_id]) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + product_ids: [product_id], + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteProductPrices(id, [product_id]) + } + ) + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts index 4209879087187..129a21ccfc74a 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-products-prices-batch.ts @@ -1,8 +1,11 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListProductPrices } from "@medusajs/workflows" import { ArrayNotEmpty, IsString } from "class-validator" import { Request, Response } from "express" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" import { validator } from "../../../../utils/validator" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/products/prices/batch @@ -79,15 +82,44 @@ export default async (req: Request, res: Response) => { const priceListService: PriceListService = req.scope.resolve("priceListService") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteProductPrices(id, validated.product_ids) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListProductPrices( + req.scope + ) + + const input = { + product_ids: validated.product_ids, + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteProductPrices(id, validated.product_ids) + } + ) + + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts index 4ba20c9750146..45a61d643fb3d 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/delete-variant-prices.ts @@ -1,5 +1,8 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { removePriceListVariantPrices } from "@medusajs/workflows" import { EntityManager } from "typeorm" import PriceListService from "../../../../services/price-list" +import { WorkflowTypes } from "@medusajs/types" /** * @oas [delete] /admin/price-lists/{id}/variants/{variant_id}/prices @@ -59,16 +62,44 @@ export default async (req, res) => { const priceListService: PriceListService = req.scope.resolve("priceListService") - + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const manager: EntityManager = req.scope.resolve("manager") - const [deletedPriceIds] = await manager.transaction( - async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .deleteVariantPrices(id, [variant_id]) - } + + const isMedusaV2FlagEnabled = featureFlagRouter.isFeatureEnabled( + MedusaV2Flag.key ) + let deletedPriceIds: string[] = [] + + if (isMedusaV2FlagEnabled) { + const deletePriceListProductsWorkflow = removePriceListVariantPrices( + req.scope + ) + + const input = { + variant_ids: [variant_id], + price_list_id: id, + } as WorkflowTypes.PriceListWorkflow.RemovePriceListVariantsWorkflowInputDTO + + const { result } = await deletePriceListProductsWorkflow.run({ + input, + context: { + manager, + }, + }) + deletedPriceIds = result + } else { + const [deletedIds] = await manager.transaction( + async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .deleteVariantPrices(id, [variant_id]) + } + ) + + deletedPriceIds = deletedIds + } + return res.json({ ids: deletedPriceIds, object: "money-amount", diff --git a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts index 0939d06b0d32e..9faa088f55b58 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/get-price-list.ts @@ -1,7 +1,9 @@ +import { MedusaContainer } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." - import { PriceList } from "../../../.." import PriceListService from "../../../../services/price-list" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists/{id} @@ -58,13 +60,22 @@ import PriceListService from "../../../../services/price-list" export default async (req, res) => { const { id } = req.params + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const priceListService: PriceListService = req.scope.resolve("priceListService") - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + let priceList + + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.status(200).json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/price-lists/index.ts b/packages/medusa/src/api/routes/admin/price-lists/index.ts index f4ff52921a4a8..bc324b09a71cd 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/index.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/index.ts @@ -9,6 +9,7 @@ import middlewares, { import { defaultAdminProductFields, defaultAdminProductRelations, + defaultAdminProductRemoteQueryObject, } from "../products" import { FlagRouter } from "@medusajs/utils" @@ -86,6 +87,50 @@ export default (app, featureFlagRouter: FlagRouter) => { return app } +export const defaultAdminPriceListRemoteQueryObject = { + fields: [ + "created_at", + "deleted_at", + "description", + "ends_at", + "id", + "title", + "starts_at", + "status", + "type", + "updated_at", + ], + price_list_rules: { + price_list_rule_values: { + fields: ["value"], + }, + rule_type: { + fields: ["rule_attribute"], + }, + }, + price_set_money_amounts: { + money_amount: { + fields: [ + "id", + "currency_code", + "amount", + "min_quantity", + "max_quantity", + "created_at", + "deleted_at", + "updated_at", + ], + }, + price_set: { + variant_link: { + variant: { + fields: defaultAdminProductRemoteQueryObject.variants.fields, + }, + }, + }, + }, +} + export const defaultAdminPriceListFields = [ "id", "name", diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts index 6fc229b553d21..ec36d3838e90c 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-list-products.ts @@ -1,7 +1,3 @@ -import { - DateComparisonOperator, - extendedFindParamsMixin, -} from "../../../../types/common" import { IsArray, IsBoolean, @@ -10,14 +6,20 @@ import { IsString, ValidateNested, } from "class-validator" -import { MedusaError, isDefined } from "medusa-core-utils" +import { isDefined } from "medusa-core-utils" +import { + DateComparisonOperator, + extendedFindParamsMixin, +} from "../../../../types/common" -import { FilterableProductProps } from "../../../../types/product" -import PriceListService from "../../../../services/price-list" -import { ProductStatus } from "../../../../models" -import { Request } from "express" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" import { Type } from "class-transformer" +import { Request } from "express" import { pickBy } from "lodash" +import { ProductStatus } from "../../../../models" +import PriceListService from "../../../../services/price-list" +import { FilterableProductProps } from "../../../../types/product" +import { listAndCountProductWithIsolatedProductModule } from "../products/list-products" /** * @oas [get] /admin/price-lists/{id}/products @@ -181,6 +183,9 @@ import { pickBy } from "lodash" export default async (req: Request, res) => { const { id } = req.params const { offset, limit } = req.validatedQuery + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + let products + let count const priceListService: PriceListService = req.scope.resolve("priceListService") @@ -190,11 +195,19 @@ export default async (req: Request, res) => { price_list_id: [id], } - const [products, count] = await priceListService.listProducts( - id, - pickBy(filterableFields, (val) => isDefined(val)), - req.listConfig - ) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + ;[products, count] = await listAndCountProductWithIsolatedProductModule( + req, + filterableFields, + req.listConfig + ) + } else { + ;[products, count] = await priceListService.listProducts( + id, + pickBy(filterableFields, (val) => isDefined(val)), + req.listConfig + ) + } res.json({ products, diff --git a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts index d868794fb8809..b1d3013cc3a12 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/list-price-lists.ts @@ -1,9 +1,11 @@ +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { Type } from "class-transformer" import { IsNumber, IsOptional, IsString } from "class-validator" - -import { FilterablePriceListProps } from "../../../../types/price-list" -import PriceListService from "../../../../services/price-list" import { Request } from "express" -import { Type } from "class-transformer" +import PriceListService from "../../../../services/price-list" +import { FilterablePriceListProps } from "../../../../types/price-list" +import { MedusaContainer } from "@medusajs/types" +import { listAndCountPriceListPricingModule } from "./modules-queries" /** * @oas [get] /admin/price-lists @@ -161,18 +163,30 @@ import { Type } from "class-transformer" * $ref: "#/components/responses/500_error" */ export default async (req: Request, res) => { + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") + const validated = req.validatedQuery + let priceLists + let count - const priceListService: PriceListService = - req.scope.resolve("priceListService") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + ;[priceLists, count] = await listAndCountPriceListPricingModule({ + filters: req.filterableFields, + listConfig: req.listConfig, + container: req.scope as MedusaContainer, + }) + } else { + const priceListService: PriceListService = + req.scope.resolve("priceListService") - const [price_lists, count] = await priceListService.listAndCount( - req.filterableFields, - req.listConfig - ) + ;[priceLists, count] = await priceListService.listAndCount( + req.filterableFields, + req.listConfig + ) + } res.json({ - price_lists, + price_lists: priceLists, count, offset: validated.offset, limit: validated.limit, diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts new file mode 100644 index 0000000000000..3f13f6fb47f26 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/get-price-list.ts @@ -0,0 +1,29 @@ +import { MedusaContainer } from "@medusajs/types" +import { PriceList } from "../../../../../models" +import { MedusaError } from "medusa-core-utils" +import { listAndCountPriceListPricingModule } from "./list-and-count-price-lists" + +export async function getPriceListPricingModule( + id: string, + { + container, + }: { + container: MedusaContainer + } +): Promise { + const [priceLists, count] = await listAndCountPriceListPricingModule({ + filters: { + id: [id], + }, + container, + }) + + if (count === 0) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Price list with id: ${id} was not found` + ) + } + + return priceLists[0] +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts new file mode 100644 index 0000000000000..f97cec663cbee --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/index.ts @@ -0,0 +1,2 @@ +export * from "./get-price-list" +export * from "./list-and-count-price-lists" diff --git a/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts new file mode 100644 index 0000000000000..179d160445c40 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/price-lists/modules-queries/list-and-count-price-lists.ts @@ -0,0 +1,104 @@ +import { FilterablePriceListProps, MedusaContainer } from "@medusajs/types" +import { FindConfig } from "../../../../../types/common" +import { CustomerGroup, MoneyAmount, PriceList } from "../../../../../models" +import { CustomerGroupService } from "../../../../../services" +import { defaultAdminPriceListRemoteQueryObject } from "../index" + +export async function listAndCountPriceListPricingModule({ + filters, + listConfig = { skip: 0 }, + container, +}: { + container: MedusaContainer + filters?: FilterablePriceListProps + listConfig?: FindConfig +}): Promise<[PriceList[], number]> { + const remoteQuery = container.resolve("remoteQuery") + const customerGroupService: CustomerGroupService = container.resolve( + "customerGroupService" + ) + + const query = { + price_list: { + __args: { filters, ...listConfig }, + ...defaultAdminPriceListRemoteQueryObject, + }, + } + + const { + rows: priceLists, + metadata: { count }, + } = await remoteQuery(query) + + if (!count) { + return [[], 0] + } + + const customerGroupIds: string[] = priceLists + .map((priceList) => + priceList.price_list_rules + .filter((rule) => rule.rule_type.rule_attribute === "customer_group_id") + .map((rule) => + rule.price_list_rule_values.map((rule_value) => rule_value.value) + ) + ) + .flat(2) + + const priceListCustomerGroups = await customerGroupService.list( + { id: customerGroupIds }, + {} + ) + + const customerGroupIdCustomerGroupMap = new Map( + priceListCustomerGroups.map((customerGroup) => [ + customerGroup.id, + customerGroup, + ]) + ) + + for (const priceList of priceLists) { + const priceSetMoneyAmounts = priceList.price_set_money_amounts || [] + const priceListRulesData = priceList.price_list_rules || [] + delete priceList.price_set_money_amounts + delete priceList.price_list_rules + + priceList.prices = priceSetMoneyAmounts.map((priceSetMoneyAmount) => { + const productVariant = priceSetMoneyAmount.price_set.variant_link.variant + + return { + ...(priceSetMoneyAmount.money_amount as MoneyAmount), + price_list_id: priceList.id, + variant_id: productVariant?.id ?? null, + variant: productVariant ?? null, + region_id: null, + } + }) + + priceList.name = priceList.title + delete priceList.title + + const customerGroupPriceListRule = priceListRulesData.find( + (plr) => plr.rule_type.rule_attribute === "customer_group_id" + ) + + if ( + customerGroupPriceListRule && + customerGroupPriceListRule?.price_list_rule_values + ) { + priceList.customer_groups = + customerGroupPriceListRule?.price_list_rule_values + .map((customerGroupRule) => + customerGroupIdCustomerGroupMap.get(customerGroupRule.value) + ) + .filter( + ( + customerGroup: CustomerGroup | undefined + ): customerGroup is CustomerGroup => !!customerGroup + ) + } else { + priceList.customer_groups = [] + } + } + + return [priceLists, count] +} diff --git a/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts b/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts index 9ba8c3a561d5f..b00b582659dd2 100644 --- a/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts +++ b/packages/medusa/src/api/routes/admin/price-lists/update-price-list.ts @@ -1,4 +1,5 @@ -import { PriceListStatus, PriceListType } from "@medusajs/utils" +import { MedusaContainer, PricingTypes, WorkflowTypes } from "@medusajs/types" +import { MedusaV2Flag, PriceListStatus, PriceListType } from "@medusajs/utils" import { IsArray, IsBoolean, @@ -8,15 +9,17 @@ import { ValidateNested, } from "class-validator" import { defaultAdminPriceListFields, defaultAdminPriceListRelations } from "." -import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" +import { updatePriceLists } from "@medusajs/workflows" import { Type } from "class-transformer" import { EntityManager } from "typeorm" import { PriceList } from "../../../.." import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing" import PriceListService from "../../../../services/price-list" +import { AdminPriceListPricesUpdateReq } from "../../../../types/price-list" import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators" import { validator } from "../../../../utils/validator" +import { getPriceListPricingModule } from "./modules-queries" /** * @oas [post] /admin/price-lists/{id} @@ -83,26 +86,59 @@ import { validator } from "../../../../utils/validator" */ export default async (req, res) => { const { id } = req.params + let priceList + const featureFlagRouter = req.scope.resolve("featureFlagRouter") + const manager: EntityManager = req.scope.resolve("manager") + const priceListService: PriceListService = + req.scope.resolve("priceListService") const validated = await validator( AdminPostPriceListsPriceListPriceListReq, req.body ) - const priceListService: PriceListService = - req.scope.resolve("priceListService") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const updateVariantsWorkflow = updatePriceLists(req.scope) + const rules: PricingTypes.CreatePriceListRules = {} + const customerGroups = validated.customer_groups || [] + delete validated.customer_groups - const manager: EntityManager = req.scope.resolve("manager") - await manager.transaction(async (transactionManager) => { - return await priceListService - .withTransaction(transactionManager) - .update(id, validated) - }) - - const priceList = await priceListService.retrieve(id, { - select: defaultAdminPriceListFields as (keyof PriceList)[], - relations: defaultAdminPriceListRelations, - }) + if (customerGroups.length) { + rules["customer_group_id"] = customerGroups.map((group) => group.id) + } + + const input = { + price_lists: [ + { + id, + ...validated, + rules, + }, + ], + } as WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowInputDTO + + await updateVariantsWorkflow.run({ + input, + context: { + manager, + }, + }) + + priceList = await getPriceListPricingModule(id, { + container: req.scope as MedusaContainer, + }) + } else { + await manager.transaction(async (transactionManager) => { + return await priceListService + .withTransaction(transactionManager) + .update(id, validated) + }) + + priceList = await priceListService.retrieve(id, { + select: defaultAdminPriceListFields as (keyof PriceList)[], + relations: defaultAdminPriceListRelations, + }) + } res.json({ price_list: priceList }) } diff --git a/packages/medusa/src/api/routes/admin/products/create-variant.ts b/packages/medusa/src/api/routes/admin/products/create-variant.ts index ffddb375fb306..f394c73db2fe7 100644 --- a/packages/medusa/src/api/routes/admin/products/create-variant.ts +++ b/packages/medusa/src/api/routes/admin/products/create-variant.ts @@ -1,7 +1,6 @@ -import { - CreateProductVariantInput, - ProductVariantPricesCreateReq, -} from "../../../../types/product-variant" +import { IInventoryService, WorkflowTypes } from "@medusajs/types" +import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" +import { CreateProductVariants } from "@medusajs/workflows" import { IsArray, IsBoolean, @@ -11,19 +10,22 @@ import { IsString, ValidateNested, } from "class-validator" +import { defaultAdminProductFields, defaultAdminProductRelations } from "." import { PricingService, ProductService, ProductVariantInventoryService, ProductVariantService, } from "../../../../services" -import { defaultAdminProductFields, defaultAdminProductRelations } from "." - -import { EntityManager } from "typeorm" -import { IInventoryService } from "@medusajs/types" +import { + CreateProductVariantInput, + ProductVariantPricesCreateReq, +} from "../../../../types/product-variant" import { Type } from "class-transformer" -import { createVariantsTransaction } from "./transaction/create-product-variant" +import { EntityManager } from "typeorm" import { validator } from "../../../../utils/validator" +import { getProductWithIsolatedProductModule } from "./get-product" +import { createVariantsTransaction } from "./transaction/create-product-variant" /** * @oas [post] /admin/products/{id}/variants @@ -122,6 +124,8 @@ export default async (req, res) => { req.body ) + const manager: EntityManager = req.scope.resolve("manager") + const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter") const inventoryService: IInventoryService | undefined = req.scope.resolve("inventoryService") const productVariantInventoryService: ProductVariantInventoryService = @@ -129,29 +133,56 @@ export default async (req, res) => { const productVariantService: ProductVariantService = req.scope.resolve( "productVariantService" ) + const pricingService: PricingService = req.scope.resolve("pricingService") + let rawProduct - const manager: EntityManager = req.scope.resolve("manager") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const createVariantsWorkflow = CreateProductVariants.createProductVariants( + req.scope + ) - await manager.transaction(async (transactionManager) => { - await createVariantsTransaction( - { - manager: transactionManager, - inventoryService, - productVariantInventoryService, - productVariantService, + const input = { + productVariants: [ + { + product_id: id, + ...validated, + }, + ] as WorkflowTypes.ProductWorkflow.CreateProductVariantsInputDTO[], + } + + await createVariantsWorkflow.run({ + input, + context: { + manager, }, + }) + + rawProduct = await getProductWithIsolatedProductModule( + req, id, - [validated as CreateProductVariantInput] + req.retrieveConfig ) - }) - - const productService: ProductService = req.scope.resolve("productService") - const pricingService: PricingService = req.scope.resolve("pricingService") - - const rawProduct = await productService.retrieve(id, { - select: defaultAdminProductFields, - relations: defaultAdminProductRelations, - }) + } else { + await manager.transaction(async (transactionManager) => { + await createVariantsTransaction( + { + manager: transactionManager, + inventoryService, + productVariantInventoryService, + productVariantService, + }, + id, + [validated as CreateProductVariantInput] + ) + }) + + const productService: ProductService = req.scope.resolve("productService") + + rawProduct = await productService.retrieve(id, { + select: defaultAdminProductFields, + relations: defaultAdminProductRelations, + }) + } const [product] = await pricingService.setAdminProductPricing([rawProduct]) diff --git a/packages/medusa/src/api/routes/admin/products/get-product.ts b/packages/medusa/src/api/routes/admin/products/get-product.ts index 9028a0ab9b50d..b1a238a79e2df 100644 --- a/packages/medusa/src/api/routes/admin/products/get-product.ts +++ b/packages/medusa/src/api/routes/admin/products/get-product.ts @@ -118,7 +118,11 @@ export default async (req, res) => { res.json({ product }) } -async function getProductWithIsolatedProductModule(req, id, retrieveConfig) { +export async function getProductWithIsolatedProductModule( + req, + id, + retrieveConfig +) { // TODO: Add support for fields/expands const remoteQuery = req.scope.resolve("remoteQuery") diff --git a/packages/medusa/src/api/routes/admin/products/list-products.ts b/packages/medusa/src/api/routes/admin/products/list-products.ts index d63809b4231ce..549b2603981b0 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.ts +++ b/packages/medusa/src/api/routes/admin/products/list-products.ts @@ -7,7 +7,11 @@ import { SalesChannelService, } from "../../../../services" -import { IInventoryService } from "@medusajs/types" +import { + IInventoryService, + IPricingModuleService, + IProductModuleService, +} from "@medusajs/types" import { MedusaV2Flag, promiseAll } from "@medusajs/utils" import { Type } from "class-transformer" import { Product } from "../../../../models" @@ -301,7 +305,53 @@ export default async (req, res) => { }) } -async function listAndCountProductWithIsolatedProductModule( +async function getVariantsFromPriceList(req, priceListId) { + const remoteQuery = req.scope.resolve("remoteQuery") + const pricingModuleService: IPricingModuleService = req.scope.resolve( + "pricingModuleService" + ) + const productModuleService: IProductModuleService = req.scope.resolve( + "productModuleService" + ) + + const [priceList] = await pricingModuleService.listPriceLists( + { id: [priceListId] }, + { + relations: [ + "price_set_money_amounts", + "price_set_money_amounts.price_set", + ], + select: ["price_set_money_amounts.price_set.id"], + } + ) + + const priceSetIds = priceList.price_set_money_amounts?.map( + (psma) => psma.price_set?.id + ) + + const query = { + product_variant_price_set: { + __args: { + price_set_id: priceSetIds, + }, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + const variantIds = variantPriceSets.map((vps) => vps.variant_id) + + return await productModuleService.listVariants( + { + id: variantIds, + }, + { + select: ["product_id"], + } + ) +} + +export async function listAndCountProductWithIsolatedProductModule( req, filterableFields, listConfig @@ -309,6 +359,7 @@ async function listAndCountProductWithIsolatedProductModule( // TODO: Add support for fields/expands const remoteQuery = req.scope.resolve("remoteQuery") + const featureFlagRouter = req.scope.resolve("featureFlagRouter") const productIdsFilter: Set = new Set() const variantIdsFilter: Set = new Set() @@ -352,22 +403,28 @@ async function listAndCountProductWithIsolatedProductModule( delete filterableFields.price_list_id if (priceListId) { - // TODO: it is working but validate the behaviour. - // e.g pricing context properly set. - // At the moment filtering by price list but not having any customer id or - // include discount forces the query to filter with price list id is null - const priceListService = req.scope.resolve( - "priceListService" - ) as PriceListService - promises.push( - priceListService - .listPriceListsVariantIdsMap(priceListId) - .then((priceListVariantIdsMap) => { - priceListVariantIdsMap[priceListId].map((variantId) => - variantIdsFilter.add(variantId) - ) - }) - ) + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + const variants = await getVariantsFromPriceList(req, priceListId) + + variants.forEach((pv) => variantIdsFilter.add(pv.id)) + } else { + // TODO: it is working but validate the behaviour. + // e.g pricing context properly set. + // At the moment filtering by price list but not having any customer id or + // include discount forces the query to filter with price list id is null + const priceListService = req.scope.resolve( + "priceListService" + ) as PriceListService + promises.push( + priceListService + .listPriceListsVariantIdsMap(priceListId) + .then((priceListVariantIdsMap) => { + priceListVariantIdsMap[priceListId].map((variantId) => + variantIdsFilter.add(variantId) + ) + }) + ) + } } const discountConditionId = filterableFields.discount_condition_id diff --git a/packages/medusa/src/commands/migrate.js b/packages/medusa/src/commands/migrate.js index 981808b5a473a..f335104a193cf 100644 --- a/packages/medusa/src/commands/migrate.js +++ b/packages/medusa/src/commands/migrate.js @@ -5,7 +5,7 @@ import getMigrations, { runIsolatedModulesMigration, } from "./utils/get-migrations" -import { MedusaV2Flag, createMedusaContainer } from "@medusajs/utils" +import { createMedusaContainer } from "@medusajs/utils" import configModuleLoader from "../loaders/config" import databaseLoader from "../loaders/database" import featureFlagLoader from "../loaders/feature-flags" @@ -38,6 +38,11 @@ const getDataSource = async (directory) => { const runLinkMigrations = async (directory) => { const configModule = configModuleLoader(directory) const container = createMedusaContainer() + const featureFlagRouter = featureFlagLoader(configModule) + + container.register({ + featureFlagRouter: asValue(featureFlagRouter), + }) await pgConnectionLoader({ configModule, container }) @@ -63,16 +68,13 @@ const main = async function ({ directory }) { const configModule = configModuleLoader(directory) const dataSource = await getDataSource(directory) - const featureFlagRouter = featureFlagLoader(configModule) if (args[0] === "run") { await dataSource.runMigrations() await dataSource.destroy() await runIsolatedModulesMigration(configModule) - if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - await runLinkMigrations(directory) - } + await runLinkMigrations(directory) process.exit() Logger.info("Migrations completed.") diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index 356c5d3f5ce3c..a5db6bc287931 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -96,6 +96,7 @@ export default async ({ ) registerCoreRouters(pluginDetails, container) await registerSubscribers(pluginDetails, container, activityId) + await registerWorkflows(pluginDetails) }) ) @@ -634,6 +635,15 @@ function registerRepositories( }) } +/** + * import files from the workflows directory to run the registration of the wofklows + * @param pluginDetails + */ +async function registerWorkflows(pluginDetails: PluginDetails): Promise { + const files = glob.sync(`${pluginDetails.resolve}/workflows/*.js`, {}) + await Promise.all(files.map(async (file) => import(file))) +} + /** * Registers a plugin's models at the right location in our container. Models * must inherit from BaseModel. Models are registered directly in the container. diff --git a/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts b/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts new file mode 100644 index 0000000000000..be2a73a515c8b --- /dev/null +++ b/packages/medusa/src/migrations/1699371074198-drop-non-null-constraint-price-list.ts @@ -0,0 +1,20 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { MigrationInterface, QueryRunner } from "typeorm" + +export const featureFlag = MedusaV2Flag.key + +export class DropNonNullConstraintPriceList1699371074198 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE IF EXISTS price_list ALTER COLUMN name DROP NOT NULL; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE IF EXISTS price_list ALTER COLUMN name SET NOT NULL; + `) + } +} diff --git a/packages/medusa/src/models/money-amount.ts b/packages/medusa/src/models/money-amount.ts index db172c17d9707..0ecece7eb8c56 100644 --- a/packages/medusa/src/models/money-amount.ts +++ b/packages/medusa/src/models/money-amount.ts @@ -69,7 +69,7 @@ export class MoneyAmount extends SoftDeletableEntity { @Index("idx_money_amount_region_id") @Column({ nullable: true }) - region_id: string + region_id: string | null @ManyToOne(() => Region) @JoinColumn({ name: "region_id" }) diff --git a/packages/medusa/src/scripts/create-default-rule-types.ts b/packages/medusa/src/scripts/create-default-rule-types.ts index 66e26996b6978..b5d2612c36cea 100644 --- a/packages/medusa/src/scripts/create-default-rule-types.ts +++ b/packages/medusa/src/scripts/create-default-rule-types.ts @@ -1,29 +1,10 @@ -import { AwilixContainer } from "awilix" -import { IPricingModuleService } from "@medusajs/types" +import { createDefaultRuleTypes } from "./utils/create-default-rule-types" import dotenv from "dotenv" import express from "express" import loaders from "../loaders" dotenv.config() -export const createDefaultRuleTypes = async (container: AwilixContainer) => { - const pricingModuleService: IPricingModuleService = container.resolve( - "pricingModuleService" - ) - const existing = await pricingModuleService.listRuleTypes( - { rule_attribute: ["region_id"] }, - { take: 1 } - ) - - if (existing.length) { - return - } - - await pricingModuleService.createRuleTypes([ - { name: "region_id", rule_attribute: "region_id" }, - ]) -} - const migrate = async function ({ directory }) { const app = express() const { container } = await loaders({ @@ -38,7 +19,9 @@ const migrate = async function ({ directory }) { migrate({ directory: process.cwd() }) .then(() => { console.log("Created default rule types") + process.exit() }) .catch(() => { console.log("Failed to create rule types") + process.exit(1) }) diff --git a/packages/medusa/src/scripts/migrate-to-pricing-module.ts b/packages/medusa/src/scripts/migrate-to-pricing-module.ts new file mode 100644 index 0000000000000..46e503b2b3503 --- /dev/null +++ b/packages/medusa/src/scripts/migrate-to-pricing-module.ts @@ -0,0 +1,250 @@ +import { IPricingModuleService, PricingTypes } from "@medusajs/types" +import { promiseAll } from "@medusajs/utils" +import { AwilixContainer } from "awilix" +import dotenv from "dotenv" +import express from "express" +import loaders from "../loaders" +import Logger from "../loaders/logger" +import { PriceList } from "../models" +import { CurrencyService, PriceListService } from "../services" +import { createDefaultRuleTypes } from "./utils/create-default-rule-types" +import { migrateProductVariantPricing } from "./utils/migrate-money-amounts-to-pricing-module" + +dotenv.config() + +const BATCH_SIZE = 1000 + +const migratePriceLists = async (container: AwilixContainer) => { + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + let offset = 0 + + const priceListCoreService: PriceListService = + container.resolve("priceListService") + + const remoteQuery = container.resolve("remoteQuery") + + const [_, totalCount] = await priceListCoreService.listAndCount( + {}, + { select: ["id"] } + ) + + while (offset < totalCount) { + const corePriceLists = await priceListCoreService.list( + {}, + { + take: BATCH_SIZE, + skip: offset, + relations: ["customer_groups", "prices", "prices.variants"], + } + ) + + const pricingModulePriceLists = await pricingModuleService.listPriceLists( + { id: corePriceLists.map(({ id }) => id) }, + { + take: BATCH_SIZE, + skip: offset, + select: ["id"], + } + ) + + const priceListIdsToUpdateSet = new Set( + pricingModulePriceLists.map(({ id }) => id) + ) + + const priceListsToCreate: PriceList[] = [] + const priceListsToUpdate: PriceList[] = [] + const variantIds: string[] = [] + + for (const corePriceList of corePriceLists) { + if (priceListIdsToUpdateSet.has(corePriceList.id)) { + priceListsToCreate.push(corePriceList) + } else { + priceListsToUpdate.push(corePriceList) + } + + const corePrices = corePriceList.prices || [] + + variantIds.push( + ...corePrices.map((corePrice) => corePrice.variants?.[0]?.id) + ) + } + + const query = { + product_variant_price_set: { + __args: { + variant_id: variantIds, + }, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + const variantIdPriceSetIdMap = new Map( + variantPriceSets.map((vps) => [vps.variant_id, vps.price_set_id]) + ) + + const promises: Promise[] = [] + + if (priceListsToUpdate.length) { + await pricingModuleService.updatePriceLists( + priceListsToUpdate.map((priceList) => { + const updateData: PricingTypes.UpdatePriceListDTO = { + id: priceList.id, + title: priceList.name, + } + + if (priceList?.customer_groups?.length) { + updateData.rules = { + customer_group_id: priceList.customer_groups.map(({ id }) => id), + } + } + + return updateData + }) + ) + + promises.push( + pricingModuleService.addPriceListPrices( + priceListsToUpdate.map((priceList) => { + return { + priceListId: priceList.id, + prices: priceList.prices + .filter((price) => + variantIdPriceSetIdMap.has(price.variants?.[0]?.id) + ) + .map((price) => { + return { + price_set_id: variantIdPriceSetIdMap.get( + price.variants?.[0]?.id + )!, + currency_code: price.currency_code, + amount: price.amount, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + } + }), + } + }) + ) + ) + } + + if (priceListsToCreate.length) { + promises.push( + pricingModuleService.createPriceLists( + priceListsToCreate.map( + ({ name: title, prices, customer_groups, ...priceList }) => { + const createData: PricingTypes.CreatePriceListDTO = { + ...priceList, + starts_at: priceList.starts_at?.toISOString(), + ends_at: priceList.ends_at?.toISOString(), + title, + } + + if (customer_groups?.length) { + createData.rules = { + customer_group_id: customer_groups.map(({ id }) => id), + } + } + + if (prices?.length) { + createData.prices = prices.map((price) => { + return { + price_set_id: variantIdPriceSetIdMap.get( + price.variants?.[0]?.id + )!, + currency_code: price.currency_code, + amount: price.amount, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + } + }) + } + + return createData + } + ) + ) + ) + } + + await promiseAll(promises) + + offset += corePriceLists.length + + Logger.info(`Processed ${offset} of ${totalCount}`) + } +} + +const ensureCurrencies = async (container: AwilixContainer) => { + const currenciesService: CurrencyService = + container.resolve("currencyService") + + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + const [coreCurrencies, totalCurrencies] = + await currenciesService.listAndCount({}, {}) + + const moduleCurrencies = await pricingModuleService.listCurrencies( + {}, + { take: 100000 } + ) + + const moduleCurrenciesSet = new Set(moduleCurrencies.map(({ code }) => code)) + + const currenciesToCreate = coreCurrencies + .filter(({ code }) => { + return !moduleCurrenciesSet.has(code) + }) + .map(({ includes_tax, ...currency }) => currency) + + await pricingModuleService.createCurrencies(currenciesToCreate) +} + +const migrate = async function ({ directory }) { + const app = express() + + const { container } = await loaders({ + directory, + expressApp: app, + isTest: false, + }) + + Logger.info("-----------------------------------------------") + Logger.info("------------- Creating currencies -------------") + Logger.info("-----------------------------------------------") + await ensureCurrencies(container) + + Logger.info("-----------------------------------------------") + Logger.info("--------- Creating default rule types ---------") + Logger.info("-----------------------------------------------") + await createDefaultRuleTypes(container) + + Logger.info("-----------------------------------------------") + Logger.info("---------- Migrating Variant Prices -----------") + Logger.info("-----------------------------------------------") + + await migrateProductVariantPricing(container) + + Logger.info("-----------------------------------------------") + Logger.info("----------- Migrating Price Lists -------------") + Logger.info("-----------------------------------------------") + + return await migratePriceLists(container) +} + +migrate({ directory: process.cwd() }) + .then(() => { + Logger.info("Migrated price lists") + process.exit(0) + }) + .catch((error) => { + console.warn(error) + Logger.info("Failed to migrate price lists") + process.exit(1) + }) diff --git a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts b/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts deleted file mode 100644 index 3ff98ac98b0dd..0000000000000 --- a/packages/medusa/src/scripts/money-amount-pricing-module-migration.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { IPricingModuleService, MedusaContainer } from "@medusajs/types" -import { - FlagRouter, - MedusaError, - MedusaV2Flag, - promiseAll, -} from "@medusajs/utils" -import dotenv from "dotenv" -import express from "express" -import { EntityManager } from "typeorm" -import loaders from "../loaders" -import loadMedusaApp from "../loaders/medusa-app" -import { ProductVariant } from "../models" -import { ProductVariantService } from "../services" -import { createDefaultRuleTypes } from "./create-default-rule-types" - -dotenv.config() - -const BATCH_SIZE = 100 - -const migrateProductVariant = async ( - variant: ProductVariant, - { - container, - }: { container: MedusaContainer; transactionManager: EntityManager } -) => { - const pricingService: IPricingModuleService = container.resolve( - "pricingModuleService" - ) - - const configModule = await container.resolve("configModule") - const { link } = await loadMedusaApp( - { configModule, container }, - { registerInContainer: false } - ) - - if (!link) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Can't migrate money_amounts: Pricing module is not configured correctly" - ) - } - - const priceSet = await pricingService.create({ - rules: [{ rule_attribute: "region_id" }], - prices: variant.prices.map((price) => ({ - rules: { - region_id: price.region_id, - }, - currency_code: price.currency_code, - min_quantity: price.min_quantity, - max_quantity: price.max_quantity, - amount: price.amount, - })), - }) - - await link.create({ - productService: { - variant_id: variant.id, - }, - pricingService: { - price_set_id: priceSet.id, - }, - }) -} - -const processBatch = async ( - variants: ProductVariant[], - container: MedusaContainer -) => { - const manager = container.resolve("manager") - return await manager.transaction(async (transactionManager) => { - await promiseAll( - variants.map(async (variant) => { - await migrateProductVariant(variant, { - container, - transactionManager, - }) - }) - ) - }) -} - -const migrate = async function ({ directory }) { - const app = express() - const { container } = await loaders({ - directory, - expressApp: app, - isTest: false, - }) - - const variantService: ProductVariantService = await container.resolve( - "productVariantService" - ) - const featureFlagRouter: FlagRouter = await container.resolve( - "featureFlagRouter" - ) - - if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { - throw new MedusaError( - MedusaError.Types.NOT_ALLOWED, - "Pricing module not enabled" - ) - } - - await createDefaultRuleTypes(container) - - const [variants, totalCount] = await variantService.listAndCount( - {}, - { take: BATCH_SIZE, order: { id: "ASC" }, relations: ["prices"] } - ) - - await processBatch(variants, container) - - let processedCount = variants.length - - console.log(`Processed ${processedCount} of ${totalCount}`) - - while (processedCount < totalCount) { - const nextBatch = await variantService.list( - {}, - { - skip: processedCount, - take: BATCH_SIZE, - order: { id: "ASC" }, - relations: ["prices"], - } - ) - - await processBatch(nextBatch, container) - - processedCount += nextBatch.length - console.log(`Processed ${processedCount} of ${totalCount}`) - } - - console.log("Done") - process.exit(0) -} - -migrate({ directory: process.cwd() }) diff --git a/packages/medusa/src/scripts/utils/create-default-rule-types.ts b/packages/medusa/src/scripts/utils/create-default-rule-types.ts new file mode 100644 index 0000000000000..6403b98d03e52 --- /dev/null +++ b/packages/medusa/src/scripts/utils/create-default-rule-types.ts @@ -0,0 +1,31 @@ +import { AwilixContainer } from "awilix" +import { IPricingModuleService } from "@medusajs/types" + +export const createDefaultRuleTypes = async (container: AwilixContainer) => { + const pricingModuleService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + const existing = await pricingModuleService.listRuleTypes( + { rule_attribute: ["region_id", "customer_group_id"] }, + { take: 2 } + ) + + if (existing.length === 2) { + return + } + + if (existing.length === 0) { + await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + { name: "customer_group_id", rule_attribute: "customer_group_id" }, + ]) + } else if (existing[0].rule_attribute === "region_id") { + await pricingModuleService.createRuleTypes([ + { name: "customer_group_id", rule_attribute: "customer_group_id" }, + ]) + } else { + await pricingModuleService.createRuleTypes([ + { name: "region_id", rule_attribute: "region_id" }, + ]) + } +} diff --git a/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts b/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts new file mode 100644 index 0000000000000..4a1e26076acc0 --- /dev/null +++ b/packages/medusa/src/scripts/utils/migrate-money-amounts-to-pricing-module.ts @@ -0,0 +1,83 @@ +import { IPricingModuleService, MedusaContainer } from "@medusajs/types" +import { MedusaError, promiseAll } from "@medusajs/utils" + +import { ProductVariantService } from "../../services" +import dotenv from "dotenv" + +dotenv.config() + +const BATCH_SIZE = 100 + +export const migrateProductVariantPricing = async function ( + container: MedusaContainer +) { + const variantService: ProductVariantService = await container.resolve( + "productVariantService" + ) + + const pricingService: IPricingModuleService = container.resolve( + "pricingModuleService" + ) + + const link = await container.resolve("remoteLink") + + if (!link) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Can't migrate money_amounts: Pricing module is not configured correctly" + ) + } + + const [_, totalCount] = await variantService.listAndCount( + {}, + { take: BATCH_SIZE, order: { id: "ASC" }, relations: ["prices"] } + ) + + let processedCount = 0 + while (processedCount < totalCount) { + const [variants] = await variantService.listAndCount( + {}, + { + skip: processedCount, + take: BATCH_SIZE, + order: { id: "ASC" }, + relations: ["prices"], + } + ) + + const links: any[] = [] + + await promiseAll( + variants.map(async (variant) => { + const priceSet = await pricingService.create({ + rules: [{ rule_attribute: "region_id" }], + prices: + variant?.prices + ?.filter(({ price_list_id }) => !price_list_id) + .map((price) => ({ + rules: { + ...(price.region_id ? { region_id: price.region_id } : {}), + }, + currency_code: price.currency_code, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + amount: price.amount, + })) ?? [], + }) + + links.push({ + productService: { + variant_id: variant.id, + }, + pricingService: { + price_set_id: priceSet.id, + }, + }) + }) + ) + await link.create(links) + + processedCount += variants.length + console.log(`Processed ${processedCount} of ${totalCount}`) + } +} diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index de1265db5f0dc..79c0752fc78b2 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -4,15 +4,18 @@ import { PriceSetMoneyAmountDTO, RemoteQueryFunction, } from "@medusajs/types" - +import { + CustomerService, + ProductVariantService, + RegionService, + TaxProviderService, +} from "." import { FlagRouter, MedusaV2Flag, promiseAll, removeNullish, } from "@medusajs/utils" -import { ProductVariantService, RegionService, TaxProviderService } from "." - import { IPriceSelectionStrategy, PriceSelectionContext, @@ -33,11 +36,11 @@ import { TaxedPricing, } from "../types/pricing" -import { MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" -import { TransactionBaseService } from "../interfaces" +import { MedusaError } from "medusa-core-utils" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { TaxServiceRate } from "../types/tax-service" +import { TransactionBaseService } from "../interfaces" import { calculatePriceTaxAmount } from "../utils" type InjectedDependencies = { @@ -45,6 +48,7 @@ type InjectedDependencies = { productVariantService: ProductVariantService taxProviderService: TaxProviderService regionService: RegionService + customerService: CustomerService priceSelectionStrategy: IPriceSelectionStrategy featureFlagRouter: FlagRouter remoteQuery: RemoteQueryFunction @@ -57,6 +61,7 @@ type InjectedDependencies = { class PricingService extends TransactionBaseService { protected readonly regionService: RegionService protected readonly taxProviderService: TaxProviderService + protected readonly customerService_: CustomerService protected readonly priceSelectionStrategy: IPriceSelectionStrategy protected readonly productVariantService: ProductVariantService protected readonly featureFlagRouter: FlagRouter @@ -64,6 +69,7 @@ class PricingService extends TransactionBaseService { protected get pricingModuleService(): IPricingModuleService { return this.__container__.pricingModuleService } + protected get remoteQuery(): RemoteQueryFunction { return this.__container__.remoteQuery } @@ -74,6 +80,7 @@ class PricingService extends TransactionBaseService { regionService, priceSelectionStrategy, featureFlagRouter, + customerService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -82,6 +89,7 @@ class PricingService extends TransactionBaseService { this.taxProviderService = taxProviderService this.priceSelectionStrategy = priceSelectionStrategy this.productVariantService = productVariantService + this.customerService_ = customerService this.featureFlagRouter = featureFlagRouter } @@ -220,9 +228,20 @@ class PricingService extends TransactionBaseService { (variantPriceSet) => variantPriceSet.price_set_id ) - const queryContext: PriceSelectionContext = removeNullish( - context.price_selection - ) + const queryContext: PriceSelectionContext & { + customer_group_id?: string[] + } = removeNullish(context.price_selection) + + if (queryContext.customer_id) { + const { groups } = await this.customerService_.retrieve( + queryContext.customer_id, + { relations: ["groups"] } + ) + + if (groups?.length) { + queryContext.customer_group_id = groups.map((group) => group.id) + } + } let calculatedPrices: CalculatedPriceSet[] = [] @@ -259,25 +278,37 @@ class PricingService extends TransactionBaseService { } if (priceSetId) { - const calculatedPrice: CalculatedPriceSet | undefined = + const calculatedPrices: CalculatedPriceSet | undefined = calculatedPriceMap.get(priceSetId) - if (calculatedPrice) { - pricingResult.prices = [ - { - id: calculatedPrice.calculated_price?.money_amount_id, - currency_code: calculatedPrice.currency_code, - amount: calculatedPrice.calculated_amount, - min_quantity: calculatedPrice.calculated_price?.min_quantity, - max_quantity: calculatedPrice.calculated_price?.max_quantity, - price_list_id: calculatedPrice.calculated_price?.price_list_id, - }, - ] as MoneyAmount[] - - pricingResult.original_price = calculatedPrice?.original_amount - pricingResult.calculated_price = calculatedPrice?.calculated_amount + if (calculatedPrices) { + pricingResult.prices.push({ + id: calculatedPrices?.original_price?.money_amount_id, + currency_code: calculatedPrices.currency_code, + amount: calculatedPrices.original_amount, + min_quantity: calculatedPrices.original_price?.min_quantity, + max_quantity: calculatedPrices.original_price?.max_quantity, + price_list_id: calculatedPrices.original_price?.price_list_id, + } as MoneyAmount) + + if ( + calculatedPrices.calculated_price?.money_amount_id !== + calculatedPrices.original_price?.money_amount_id + ) { + pricingResult.prices.push({ + id: calculatedPrices.calculated_price?.money_amount_id, + currency_code: calculatedPrices.currency_code, + amount: calculatedPrices.calculated_amount, + min_quantity: calculatedPrices.calculated_price?.min_quantity, + max_quantity: calculatedPrices.calculated_price?.max_quantity, + price_list_id: calculatedPrices.calculated_price?.price_list_id, + } as MoneyAmount) + } + + pricingResult.original_price = calculatedPrices?.original_amount + pricingResult.calculated_price = calculatedPrices?.calculated_amount pricingResult.calculated_price_type = - calculatedPrice?.calculated_price?.price_list_type + calculatedPrices?.calculated_price?.price_list_type } } diff --git a/packages/medusa/src/types/price-list.ts b/packages/medusa/src/types/price-list.ts index 24ea5e1225790..208001af42ea3 100644 --- a/packages/medusa/src/types/price-list.ts +++ b/packages/medusa/src/types/price-list.ts @@ -9,7 +9,7 @@ import { ValidateNested, } from "class-validator" import { PriceList } from "../models/price-list" -import { DateComparisonOperator, FindConfig } from "./common" +import { DateComparisonOperator } from "./common" import { XorConstraint } from "./validators/xor" /** diff --git a/packages/orchestration/src/workflow/workflow-manager.ts b/packages/orchestration/src/workflow/workflow-manager.ts index e46f300f5bf5f..634c63b0fe138 100644 --- a/packages/orchestration/src/workflow/workflow-manager.ts +++ b/packages/orchestration/src/workflow/workflow-manager.ts @@ -70,7 +70,7 @@ export class WorkflowManager { static register( workflowId: string, - flow: TransactionStepsDefinition | OrchestratorBuilder, + flow: TransactionStepsDefinition | OrchestratorBuilder | undefined, handlers: WorkflowHandler, requiredModules?: Set, optionalModules?: Set @@ -78,19 +78,22 @@ export class WorkflowManager { const finalFlow = flow instanceof OrchestratorBuilder ? flow.build() : flow if (WorkflowManager.workflows.has(workflowId)) { - const areStepsEqual = - JSON.stringify(finalFlow) === - JSON.stringify(WorkflowManager.workflows.get(workflowId)!.flow_) + const areStepsEqual = finalFlow + ? JSON.stringify(finalFlow) === + JSON.stringify(WorkflowManager.workflows.get(workflowId)!.flow_) + : true if (!areStepsEqual) { - throw new Error(`Workflow with id "${workflowId}" and step definition already exists.`) + throw new Error( + `Workflow with id "${workflowId}" and step definition already exists.` + ) } } WorkflowManager.workflows.set(workflowId, { id: workflowId, - flow_: finalFlow, - orchestrator: new TransactionOrchestrator(workflowId, finalFlow), + flow_: finalFlow!, + orchestrator: new TransactionOrchestrator(workflowId, finalFlow ?? {}), handler: WorkflowManager.buildHandlers(handlers), handlers_: handlers, requiredModules, diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts index 659ac3e350360..734c96da15a24 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/calculate-price.spec.ts @@ -1185,6 +1185,77 @@ describe("PricingModule Service - Calculate Price", () => { ]) }) + it("should return best price list price first when price list conditions match", async () => { + await createPriceLists(service) + await createPriceLists( + service, + {}, + {}, + defaultPriceListPrices.map((price) => { + return { ...price, amount: price.amount / 2 } + }) + ) + + const priceSetsResult = await service.calculatePrices( + { id: ["price-set-EUR", "price-set-PLN"] }, + { + context: { + currency_code: "PLN", + region_id: "DE", + customer_group_id: "vip-customer-group-id", + company_id: "medusa-company-id", + }, + } + ) + + expect(priceSetsResult).toEqual([ + { + id: "price-set-EUR", + is_calculated_price_price_list: false, + calculated_amount: null, + is_original_price_price_list: false, + original_amount: null, + currency_code: null, + calculated_price: { + money_amount_id: null, + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + original_price: { + money_amount_id: null, + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + }, + { + id: "price-set-PLN", + is_calculated_price_price_list: true, + calculated_amount: 116, + is_original_price_price_list: false, + original_amount: 400, + currency_code: "PLN", + calculated_price: { + money_amount_id: expect.any(String), + price_list_id: expect.any(String), + price_list_type: "sale", + min_quantity: null, + max_quantity: null, + }, + original_price: { + money_amount_id: expect.any(String), + price_list_id: null, + price_list_type: null, + min_quantity: 1, + max_quantity: 5, + }, + }, + ]) + }) + it("should return price list prices when price list dont have rules, but context is loaded", async () => { await createPriceLists(service, {}, {}) diff --git a/packages/pricing/src/joiner-config.ts b/packages/pricing/src/joiner-config.ts index ee9c415f72904..7be13f7c8b88a 100644 --- a/packages/pricing/src/joiner-config.ts +++ b/packages/pricing/src/joiner-config.ts @@ -1,12 +1,20 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Currency, MoneyAmount, PriceSet } from "@models" +import { + Currency, + MoneyAmount, + PriceList, + PriceSet, + PriceSetMoneyAmount, +} from "@models" export const LinkableKeys = { money_amount_id: MoneyAmount.name, currency_code: Currency.name, price_set_id: PriceSet.name, + price_list_id: PriceList.name, + price_set_money_amount_id: PriceSetMoneyAmount.name, } const entityLinkableKeysMap: MapToConfig = {} Object.entries(LinkableKeys).forEach(([key, value]) => { @@ -16,11 +24,12 @@ Object.entries(LinkableKeys).forEach(([key, value]) => { valueFrom: key.split("_").pop()!, }) }) + export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap export const joinerConfig: ModuleJoinerConfig = { serviceName: Modules.PRICING, - primaryKeys: ["id", "currency_code"], + primaryKeys: ["id"], linkableKeys: LinkableKeys, alias: [ { @@ -53,5 +62,17 @@ export const joinerConfig: ModuleJoinerConfig = { methodSuffix: "Currencies", }, }, + { + name: "price_list", + args: { + methodSuffix: "PriceLists", + }, + }, + { + name: "price_lists", + args: { + methodSuffix: "PriceLists", + }, + }, ], } diff --git a/packages/pricing/src/migrations/Migration20231101232834.ts b/packages/pricing/src/migrations/Migration20231101232834.ts index 8056911665cea..a50aad2cd9987 100644 --- a/packages/pricing/src/migrations/Migration20231101232834.ts +++ b/packages/pricing/src/migrations/Migration20231101232834.ts @@ -14,13 +14,10 @@ export class Migration20231101232834 extends Migration { ) this.addSql( - `ALTER TABLE price_list ADD COLUMN IF NOT EXISTS number_rules integer not null default 0` + `ALTER TABLE price_list + ADD COLUMN IF NOT EXISTS number_rules integer not null default 0` ) - - this.addSql( - 'alter table "price_list" add column if not exists "title" text not null, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\';' - ) - + this.addSql( 'alter table "price_set_money_amount" drop constraint "price_set_money_amount_price_list_id_foreign";' ) @@ -30,9 +27,19 @@ export class Migration20231101232834 extends Migration { ) this.addSql( - 'alter table "price_list" add column if not exists "title" text not null, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\', add column if not exists "created_at" timestamptz not null default now(), add column if not exists "updated_at" timestamptz not null default now(), add column if not exists "deleted_at" timestamptz null;' + 'alter table "price_list" add column if not exists "title" text, add column if not exists "name" text, add column if not exists "description" text not null, add column if not exists "type" text check ("type" in (\'sale\', \'override\')) not null default \'sale\', add column if not exists "created_at" timestamptz not null default now(), add column if not exists "updated_at" timestamptz not null default now(), add column if not exists "deleted_at" timestamptz null;' ) + this.addSql(` + UPDATE "price_list" + SET title = name + `) + + this.addSql(`alter table "price_list" + alter column "title" set not null `) + + this.addSql('alter table "price_list" drop column if exists "name";') + this.addSql( 'create index if not exists "IDX_price_list_deleted_at" on "price_list" ("deleted_at");' ) diff --git a/packages/pricing/src/models/price-rule.ts b/packages/pricing/src/models/price-rule.ts index aab8908a656fe..a1d786759bda1 100644 --- a/packages/pricing/src/models/price-rule.ts +++ b/packages/pricing/src/models/price-rule.ts @@ -39,9 +39,6 @@ export default class PriceRule { }) rule_type: RuleType - @Property({ columnType: "boolean", default: false }) - is_dynamic: boolean - @Property({ columnType: "text" }) value: string diff --git a/packages/pricing/src/repositories/pricing.ts b/packages/pricing/src/repositories/pricing.ts index 6aadce388621b..9e12f4a491b50 100644 --- a/packages/pricing/src/repositories/pricing.ts +++ b/packages/pricing/src/repositories/pricing.ts @@ -71,6 +71,9 @@ export class PricingRepository price_list_id: "psma1.price_list_id", pl_number_rules: "pl.number_rules", pl_type: "pl.type", + has_price_list: knex.raw( + "case when psma1.price_list_id IS NULL then False else True end" + ), }) .leftJoin("price_set_money_amount as psma1", "psma1.id", "psma1.id") .leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma1.id") @@ -154,15 +157,23 @@ export class PricingRepository .leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id") .whereIn("ps.id", pricingFilters.id) .andWhere("ma.currency_code", "=", currencyCode) + .orderBy([ - { column: "price_list_id", order: "asc" }, + { column: "psma.has_price_list", order: "asc" }, { column: "number_rules", order: "desc" }, { column: "default_priority", order: "desc" }, + { column: "amount", order: "asc" }, ]) if (quantity) { priceSetQueryKnex.where("ma.min_quantity", "<=", quantity) priceSetQueryKnex.andWhere("ma.max_quantity", ">=", quantity) + } else { + priceSetQueryKnex.andWhere(function () { + this.andWhere("ma.min_quantity", "<=", "1").orWhereNull( + "ma.min_quantity" + ) + }) } return await priceSetQueryKnex diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index 101ad70055c79..ce623d79ac222 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -13,7 +13,15 @@ import { PricingTypes, RuleTypeDTO, } from "@medusajs/types" -import { PriceListType } from "@medusajs/utils" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, + MedusaError, + PriceListType, + groupBy, + removeNullish, +} from "@medusajs/utils" import { Currency, @@ -42,15 +50,6 @@ import { PriceSetService, RuleTypeService, } from "@services" - -import { - groupBy, - InjectManager, - InjectTransactionManager, - MedusaContext, - MedusaError, - removeNullish, -} from "@medusajs/utils" import { joinerConfig } from "../joiner-config" import { CreatePriceListRuleValueDTO, PricingRepositoryService } from "../types" @@ -147,6 +146,7 @@ export default class PricingModuleService< pricingContext, sharedContext ) + const pricesSetPricesMap = groupBy(results, "price_set_id") const calculatedPrices = pricingFilters.id.map( @@ -155,8 +155,9 @@ export default class PricingModuleService< // which is prioritized by number_rules first for exact match and then deafult_priority of the rule_type // inject custom price selection here const prices = pricesSetPricesMap.get(priceSetId) || [] - const priceListPrice = prices?.find((p) => !!p.price_list_id) - const defaultPrice = prices?.find((p) => !!!p.price_list_id) + const priceListPrice = prices.find((p) => p.price_list_id) + + const defaultPrice = prices?.find((p) => !p.price_list_id) let calculatedPrice: PricingTypes.CalculatedPriceSetDTO = defaultPrice let originalPrice: PricingTypes.CalculatedPriceSetDTO = defaultPrice diff --git a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts index abfbb7c579ff0..f2e4f8754b76b 100644 --- a/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts +++ b/packages/product/integration-tests/__tests__/services/product-module-service/products.spec.ts @@ -201,6 +201,7 @@ describe("ProductModuleService products", function () { expect(createdVariant?.options).toHaveLength(1) expect(product.tags).toHaveLength(1) expect(product.variants).toHaveLength(2) + expect(product).toEqual( expect.objectContaining({ id: expect.any(String), diff --git a/packages/product/src/loaders/container.ts b/packages/product/src/loaders/container.ts index 085f8e21fa9f6..7375d3a9ef0c5 100644 --- a/packages/product/src/loaders/container.ts +++ b/packages/product/src/loaders/container.ts @@ -5,6 +5,7 @@ import { ProductCollectionRepository, ProductImageRepository, ProductOptionRepository, + ProductOptionValueRepository, ProductRepository, ProductTagRepository, ProductTypeRepository, @@ -17,6 +18,7 @@ import { ProductImageService, ProductModuleService, ProductOptionService, + ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, @@ -48,6 +50,7 @@ export default async ({ productImageService: asClass(ProductImageService).singleton(), productTypeService: asClass(ProductTypeService).singleton(), productOptionService: asClass(ProductOptionService).singleton(), + productOptionValueService: asClass(ProductOptionValueService).singleton(), }) if (customRepositories) { @@ -69,6 +72,9 @@ function loadDefaultRepositories({ container }) { productTagRepository: asClass(ProductTagRepository).singleton(), productTypeRepository: asClass(ProductTypeRepository).singleton(), productOptionRepository: asClass(ProductOptionRepository).singleton(), + productOptionValueRepository: asClass( + ProductOptionValueRepository + ).singleton(), productVariantRepository: asClass(ProductVariantRepository).singleton(), }) } diff --git a/packages/product/src/migrations/Migration20230719100648.ts b/packages/product/src/migrations/Migration20230719100648.ts index a99311bc5bfd8..aa66d30890166 100644 --- a/packages/product/src/migrations/Migration20230719100648.ts +++ b/packages/product/src/migrations/Migration20230719100648.ts @@ -8,8 +8,11 @@ export class Migration20230719100648 extends Migration { this.addSql( 'create index IF NOT EXISTS "IDX_product_category_path" on "product_category" ("mpath");' ) + + this.addSql('DROP INDEX IF EXISTS "IDX_product_category_handle";') + this.addSql( - 'alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");' + 'alter table "product_category" ADD CONSTRAINT "IDX_product_category_handle" unique ("handle");' ) this.addSql( diff --git a/packages/product/src/models/index.ts b/packages/product/src/models/index.ts index 6017c491d7036..f3f6a304fabe7 100644 --- a/packages/product/src/models/index.ts +++ b/packages/product/src/models/index.ts @@ -5,4 +5,5 @@ export { default as ProductTag } from "./product-tag" export { default as ProductType } from "./product-type" export { default as ProductVariant } from "./product-variant" export { default as ProductOption } from "./product-option" +export { default as ProductOptionValue } from "./product-option-value" export { default as Image } from "./product-image" diff --git a/packages/product/src/repositories/index.ts b/packages/product/src/repositories/index.ts index 77e7f8a7a1079..b1f0ab1e37f1f 100644 --- a/packages/product/src/repositories/index.ts +++ b/packages/product/src/repositories/index.ts @@ -7,3 +7,4 @@ export { ProductCategoryRepository } from "./product-category" export { ProductImageRepository } from "./product-image" export { ProductTypeRepository } from "./product-type" export { ProductOptionRepository } from "./product-option" +export { ProductOptionValueRepository } from "./product-option-value" diff --git a/packages/product/src/repositories/product-option-value.ts b/packages/product/src/repositories/product-option-value.ts new file mode 100644 index 0000000000000..f2f3e10bd8416 --- /dev/null +++ b/packages/product/src/repositories/product-option-value.ts @@ -0,0 +1,114 @@ +import { Context, DAL } from "@medusajs/types" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" + +import { DALUtils } from "@medusajs/utils" +import { FilterQuery as MikroFilterQuery } from "@mikro-orm/core/typings" +import { FindOptions as MikroOptions } from "@mikro-orm/core/drivers/IDatabaseDriver" +import { ProductOptionValue } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +export class ProductOptionValueRepository extends DALUtils.MikroOrmBaseRepository { + protected readonly manager_: SqlEntityManager + + constructor({ manager }: { manager: SqlEntityManager }) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.manager_ = manager + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + + return await manager.find( + ProductOptionValue, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async upsert( + optionValues: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[], + context: Context = {} + ): Promise { + const manager = this.getActiveManager(context) + + const optionValueIds: string[] = [] + + for (const optionValue of optionValues) { + if (optionValue.id) { + optionValueIds.push(optionValue.id) + } + } + + const existingOptionValues = await this.find( + { + where: { + id: { + $in: optionValueIds, + }, + }, + }, + context + ) + + const existingOptionValuesMap = new Map( + existingOptionValues.map<[string, ProductOptionValue]>((optionValue) => [ + optionValue.id, + optionValue, + ]) + ) + + const upsertedOptionValues: ProductOptionValue[] = [] + const optionValuesToCreate: ProductOptionValue[] = [] + const optionValuesToUpdate: ProductOptionValue[] = [] + + optionValues.forEach(({ option_id, ...optionValue }) => { + const existingOptionValue = optionValue.id + ? existingOptionValuesMap.get(optionValue.id) + : undefined + + if (optionValue.id && existingOptionValue) { + const updatedOptionValue = manager.assign(existingOptionValue, { + option: option_id, + ...optionValue, + }) + optionValuesToUpdate.push(updatedOptionValue) + return + } + + const newOptionValue = manager.create(ProductOptionValue, { + option: option_id, + variant: (optionValue as CreateProductOptionValueDTO).variant_id, + ...optionValue, + }) + optionValuesToCreate.push(newOptionValue) + }) + + if (optionValuesToCreate.length) { + manager.persist(optionValuesToCreate) + upsertedOptionValues.push(...optionValuesToCreate) + } + + if (optionValuesToUpdate.length) { + manager.persist(optionValuesToUpdate) + upsertedOptionValues.push(...optionValuesToUpdate) + } + + return upsertedOptionValues + } + + async delete(ids: string[], context: Context = {}): Promise { + const manager = this.getActiveManager(context) + await manager.nativeDelete(ProductOptionValue, { id: { $in: ids } }, {}) + } +} diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts index 0d7ba770b164b..dc485567aae5c 100644 --- a/packages/product/src/services/index.ts +++ b/packages/product/src/services/index.ts @@ -7,3 +7,4 @@ export { default as ProductVariantService } from "./product-variant" export { default as ProductTypeService } from "./product-type" export { default as ProductOptionService } from "./product-option" export { default as ProductImageService } from "./product-image" +export { default as ProductOptionValueService } from "./product-option-value" diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 547052b87a1a3..a74109e1ed9ab 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -16,6 +16,7 @@ import { ProductCategory, ProductCollection, ProductOption, + ProductOptionValue, ProductTag, ProductType, ProductVariant, @@ -24,6 +25,7 @@ import { ProductCategoryService, ProductCollectionService, ProductOptionService, + ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, @@ -53,6 +55,8 @@ import { } from "../types/services/product" import { + arrayDifference, + groupBy, InjectManager, InjectTransactionManager, isDefined, @@ -68,6 +72,10 @@ import { joinerConfig, LinkableKeys, } from "./../joiner-config" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -79,6 +87,7 @@ type InjectedDependencies = { productImageService: ProductImageService productTypeService: ProductTypeService productOptionService: ProductOptionService + productOptionValueService: ProductOptionValueService eventBusModuleService?: IEventBusModuleService } @@ -90,7 +99,8 @@ export default class ProductModuleService< TProductCategory extends ProductCategory = ProductCategory, TProductImage extends Image = Image, TProductType extends ProductType = ProductType, - TProductOption extends ProductOption = ProductOption + TProductOption extends ProductOption = ProductOption, + TProductOptionValue extends ProductOptionValue = ProductOptionValue > implements ProductTypes.IProductModuleService { protected baseRepository_: DAL.RepositoryService @@ -108,6 +118,8 @@ export default class ProductModuleService< protected readonly productImageService_: ProductImageService protected readonly productTypeService_: ProductTypeService protected readonly productOptionService_: ProductOptionService + // eslint-disable-next-line max-len + protected readonly productOptionValueService_: ProductOptionValueService protected readonly eventBusModuleService_?: IEventBusModuleService constructor( @@ -121,6 +133,7 @@ export default class ProductModuleService< productImageService, productTypeService, productOptionService, + productOptionValueService, eventBusModuleService, }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration @@ -134,6 +147,7 @@ export default class ProductModuleService< this.productImageService_ = productImageService this.productTypeService_ = productTypeService this.productOptionService_ = productOptionService + this.productOptionValueService_ = productOptionValueService this.eventBusModuleService_ = eventBusModuleService } @@ -231,6 +245,207 @@ export default class ProductModuleService< return [JSON.parse(JSON.stringify(variants)), count] } + async createVariants( + data: ProductTypes.CreateProductVariantDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productOptionIds = data + .map((pv) => (pv.options || []).map((opt) => opt.option_id!)) + .flat() + + const productOptions = await this.listOptions( + { id: productOptionIds }, + {}, + sharedContext + ) + + const productOptionsMap = new Map( + productOptions.map((po) => [po.id, po]) + ) + + const productVariantsMap = new Map< + string, + ProductTypes.CreateProductVariantDTO[] + >() + + for (const productVariantData of data) { + productVariantData.options = productVariantData.options?.map((option) => { + const productOption = productOptionsMap.get(option.option_id!) + + return { + option: productOption?.id, + value: option.value, + } + }) + + const productVariants = productVariantsMap.get( + productVariantData.product_id! + ) + + if (productVariants) { + productVariants.push(productVariantData) + } else { + productVariantsMap.set(productVariantData.product_id!, [ + productVariantData, + ]) + } + } + + const productVariants = ( + await promiseAll( + [...productVariantsMap].map(async ([productId, variants]) => { + return await this.productVariantService_.create( + productId, + variants as unknown as ProductTypes.CreateProductVariantOnlyDTO[], + sharedContext + ) + }) + ) + ).flat() + + return productVariants as unknown as ProductTypes.ProductVariantDTO[] + } + + @InjectTransactionManager("baseRepository_") + async deleteVariants( + productVariantIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + await this.productVariantService_.delete(productVariantIds, sharedContext) + + await this.eventBusModuleService_?.emit( + productVariantIds.map((id) => ({ + eventName: ProductEvents.PRODUCT_DELETED, + data: { id }, + })) + ) + } + + @InjectManager("baseRepository_") + async updateVariants( + data: ProductTypes.UpdateProductVariantOnlyDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const productVariants = await this.updateVariants_(data, sharedContext) + + const updatedVariants = await this.baseRepository_.serialize< + ProductTypes.ProductVariantDTO[] + >(productVariants, { + populate: true, + }) + + return updatedVariants + } + + @InjectTransactionManager("baseRepository_") + protected async updateVariants_( + data: ProductTypes.UpdateProductVariantOnlyDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const variantIdsToUpdate = data.map(({ id }) => id) + const variants = await this.listVariants( + { id: variantIdsToUpdate }, + { relations: ["options", "options.option"] }, + sharedContext + ) + const variantsMap = new Map( + variants.map((variant) => [variant.id, variant]) + ) + + if (variants.length !== data.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot update non-existing variants with ids: ${arrayDifference( + variantIdsToUpdate, + [...variantsMap.keys()] + ).join(", ")}` + ) + } + + const optionValuesToUpsert: ( + | CreateProductOptionValueDTO + | UpdateProductOptionValueDTO + )[] = [] + const optionsValuesToDelete: string[] = [] + + const toUpdate = data.map(({ id, options, ...rest }) => { + const variant = variantsMap.get(id)! + + const toUpdate: UpdateProductVariantDTO = { + id, + product_id: variant.product_id, + } + + if (options?.length) { + const optionIdToUpdateValueMap = new Map( + options.map(({ option, option_id, value }) => { + const computedOptionId = option_id ?? option.id ?? option + return [computedOptionId, value] + }) + ) + + for (const existingOptionValue of variant.options) { + if (!optionIdToUpdateValueMap.has(existingOptionValue.option.id)) { + optionsValuesToDelete.push(existingOptionValue.id) + + continue + } + + optionValuesToUpsert.push({ + id: existingOptionValue.id, + option_id: existingOptionValue.option.id, + value: optionIdToUpdateValueMap.get(existingOptionValue.option.id)!, + }) + optionIdToUpdateValueMap.delete(existingOptionValue.option.id) + } + + for (const [option_id, value] of optionIdToUpdateValueMap.entries()) { + optionValuesToUpsert.push({ + option_id, + value, + variant_id: id, + }) + } + } + + for (const [key, value] of Object.entries(rest)) { + if (variant[key] !== value) { + toUpdate[key] = value + } + } + + return toUpdate + }) + + const groups = groupBy(toUpdate, "product_id") + + const [, , productVariants]: [ + void, + TProductOptionValue[], + TProductVariant[][] + ] = await promiseAll([ + await this.productOptionValueService_.delete( + optionsValuesToDelete, + sharedContext + ), + await this.productOptionValueService_.upsert( + optionValuesToUpsert, + sharedContext + ), + await promiseAll( + [...groups.entries()].map(async ([product_id, update]) => { + return await this.productVariantService_.update( + product_id, + update.map(({ product_id, ...update }) => update), + sharedContext + ) + }) + ), + ]) + + return productVariants.flat() + } + @InjectManager("baseRepository_") async retrieveTag( tagId: string, @@ -1022,7 +1237,11 @@ export default class ProductModuleService< if (!productData.thumbnail && productData.images?.length) { productData.thumbnail = isString(productData.images[0]) ? (productData.images[0] as string) - : (productData.images[0] as { url: string }).url + : ( + productData.images[0] as { + url: string + } + ).url } if (productData.images?.length) { diff --git a/packages/product/src/services/product-option-value.ts b/packages/product/src/services/product-option-value.ts new file mode 100644 index 0000000000000..f72a9fb533ea1 --- /dev/null +++ b/packages/product/src/services/product-option-value.ts @@ -0,0 +1,44 @@ +import { ProductOptionValue } from "@models" +import { Context, DAL } from "@medusajs/types" +import { + ProductOptionRepository, + ProductOptionValueRepository, +} from "@repositories" +import { InjectTransactionManager, MedusaContext } from "@medusajs/utils" +import { + CreateProductOptionValueDTO, + UpdateProductOptionValueDTO, +} from "../types/services/product-option-value" + +type InjectedDependencies = { + productOptionValueRepository: DAL.RepositoryService +} + +export default class ProductOptionValueService< + TEntity extends ProductOptionValue = ProductOptionValue +> { + protected readonly productOptionValueRepository_: DAL.RepositoryService + + constructor({ productOptionValueRepository }: InjectedDependencies) { + this.productOptionValueRepository_ = + productOptionValueRepository as ProductOptionRepository + } + + @InjectTransactionManager("productOptionValueRepository_") + async delete( + ids: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return await this.productOptionValueRepository_.delete(ids, sharedContext) + } + + @InjectTransactionManager("productOptionValueRepository_") + async upsert( + data: (UpdateProductOptionValueDTO | CreateProductOptionValueDTO)[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + return (await ( + this.productOptionValueRepository_ as ProductOptionValueRepository + ).upsert!(data, sharedContext)) as TEntity[] + } +} diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts index de7a776f67f80..06d30a8f021f0 100644 --- a/packages/product/src/services/product-variant.ts +++ b/packages/product/src/services/product-variant.ts @@ -1,14 +1,14 @@ -import { Product, ProductVariant } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" -import { ProductVariantRepository } from "@repositories" import { InjectManager, InjectTransactionManager, - isString, MedusaContext, ModulesSdkUtils, + isString, retrieveEntity, } from "@medusajs/utils" +import { Product, ProductVariant } from "@models" +import { ProductVariantRepository } from "@repositories" import { ProductVariantServiceTypes } from "../types/services" import ProductService from "./product" @@ -96,7 +96,7 @@ export default class ProductVariantService< if (isString(productOrId)) { product = await this.productService_.retrieve( productOrId, - {}, + { relations: ["variants"] }, sharedContext ) } @@ -105,6 +105,8 @@ export default class ProductVariantService< const data_ = [...data] data_.forEach((variant) => { + delete variant?.product_id + Object.assign(variant, { variant_rank: computedRank++, product, diff --git a/packages/product/src/types/services/product-option-value.ts b/packages/product/src/types/services/product-option-value.ts new file mode 100644 index 0000000000000..5d375dabc45ae --- /dev/null +++ b/packages/product/src/types/services/product-option-value.ts @@ -0,0 +1,14 @@ +export interface UpdateProductOptionValueDTO { + id: string + value: string + option_id: string + metadata?: Record | null +} + +export interface CreateProductOptionValueDTO { + id?: string + value: string + option_id: string + variant_id: string + metadata?: Record | null +} diff --git a/packages/product/src/types/services/product-variant.ts b/packages/product/src/types/services/product-variant.ts index 1c7a90f95f105..0f4dedd361f63 100644 --- a/packages/product/src/types/services/product-variant.ts +++ b/packages/product/src/types/services/product-variant.ts @@ -2,6 +2,7 @@ import { CreateProductVariantOptionDTO } from "@medusajs/types" export interface UpdateProductVariantDTO { id: string + product_id: string title?: string sku?: string barcode?: string @@ -18,6 +19,6 @@ export interface UpdateProductVariantDTO { length?: number height?: number width?: number - options?: CreateProductVariantOptionDTO[] + options?: (CreateProductVariantOptionDTO & { id?: string })[] metadata?: Record } diff --git a/packages/types/src/pricing/common/money-amount.ts b/packages/types/src/pricing/common/money-amount.ts index 72e58d2e9a74d..160abead7bcb5 100644 --- a/packages/types/src/pricing/common/money-amount.ts +++ b/packages/types/src/pricing/common/money-amount.ts @@ -36,8 +36,6 @@ export interface MoneyAmountDTO { max_quantity?: number /** * The details of the relation between the money amount and its associated price set. - * - * @expandable */ price_set_money_amount?: PriceSetMoneyAmountDTO } diff --git a/packages/types/src/pricing/common/price-rule.ts b/packages/types/src/pricing/common/price-rule.ts index 5b22cbe9e96a8..738d4b3d885d7 100644 --- a/packages/types/src/pricing/common/price-rule.ts +++ b/packages/types/src/pricing/common/price-rule.ts @@ -4,7 +4,7 @@ import { RuleTypeDTO } from "./rule-type" /** * @interface - * + * * A price rule's data. */ export interface PriceRuleDTO { @@ -18,7 +18,6 @@ export interface PriceRuleDTO { price_set_id: string /** * The associated price set. - * * @expandable */ price_set: PriceSetDTO @@ -28,17 +27,10 @@ export interface PriceRuleDTO { rule_type_id: string /** * The associated rule type. - * + * * @expandable */ rule_type: RuleTypeDTO - /** - * @ignore - * @privateRemarks - * - * Behavior behind this property is not implemented yet. - */ - is_dynamic: boolean /** * The value of the price rule. */ @@ -58,9 +50,9 @@ export interface PriceRuleDTO { } /** - * + * * @interface - * + * * A price rule to create. */ export interface CreatePriceRuleDTO { @@ -76,13 +68,6 @@ export interface CreatePriceRuleDTO { * The ID of the associated rule type. */ rule_type_id: string - /** - * @ignore - * @privateRemarks - * - * Behavior behind this property is not implemented yet. - */ - is_dynamic?: boolean /** * The value of the price rule. */ @@ -92,22 +77,15 @@ export interface CreatePriceRuleDTO { } /** - * + * * @interface - * + * * The data to update in a price rule. The `id` is used to identify which money amount to update. */ export interface UpdatePriceRuleDTO { id: string price_set_id?: string rule_type_id?: string - /** - * @ignore - * @privateRemark - * - * Behavior behind this property is not implemented yet. - */ - is_dynamic?: boolean /** * The value of the price rule. */ @@ -128,7 +106,6 @@ export interface UpdatePriceRuleDTO { /** * @interface - * * Filters to apply on price rules. */ export interface FilterablePriceRuleProps diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 27184f1d79d95..7f44de05f9445 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -13,7 +13,7 @@ export enum ProductStatus { /** * @interface - * + * * A product's data. */ export interface ProductDTO { @@ -83,43 +83,43 @@ export interface ProductDTO { material?: string | null /** * The associated product collection. - * + * * @expandable */ collection: ProductCollectionDTO /** * The associated product categories. - * + * * @expandable */ categories?: ProductCategoryDTO[] | null /** * The associated product type. - * + * * @expandable */ type: ProductTypeDTO[] /** * The associated product tags. - * + * * @expandable */ tags: ProductTagDTO[] /** * The associated product variants. - * + * * @expandable */ variants: ProductVariantDTO[] /** * The associated product options. - * + * * @expandable */ options: ProductOptionDTO[] /** * The associated product images. - * + * * @expandable */ images: ProductImageDTO[] @@ -152,7 +152,7 @@ export interface ProductDTO { /** * @interface - * + * * A product variant's data. */ export interface ProductVariantDTO { @@ -226,17 +226,17 @@ export interface ProductVariantDTO { width?: number | null /** * The associated product options. - * + * * @expandable */ - options: ProductOptionValueDTO + options: ProductOptionValueDTO[] /** * Holds custom data in key-value pairs. */ metadata?: Record | null /** * The associated product. - * + * * @expandable */ product: ProductDTO @@ -264,7 +264,7 @@ export interface ProductVariantDTO { /** * @interface - * + * * A product category's data. */ export interface ProductCategoryDTO { @@ -298,13 +298,13 @@ export interface ProductCategoryDTO { rank?: number /** * The associated parent category. - * + * * @expandable */ parent_category?: ProductCategoryDTO /** * The associated child categories. - * + * * @expandable */ category_children: ProductCategoryDTO[] @@ -320,7 +320,7 @@ export interface ProductCategoryDTO { /** * @interface - * + * * A product category to create. */ export interface CreateProductCategoryDTO { @@ -356,7 +356,7 @@ export interface CreateProductCategoryDTO { /** * @interface - * + * * The data to update in a product category. */ export interface UpdateProductCategoryDTO { @@ -392,7 +392,7 @@ export interface UpdateProductCategoryDTO { /** * @interface - * + * * A product tag's data. */ export interface ProductTagDTO { @@ -410,7 +410,7 @@ export interface ProductTagDTO { metadata?: Record | null /** * The associated products. - * + * * @expandable */ products?: ProductDTO[] @@ -418,7 +418,7 @@ export interface ProductTagDTO { /** * @interface - * + * * A product collection's data. */ export interface ProductCollectionDTO { @@ -444,7 +444,7 @@ export interface ProductCollectionDTO { deleted_at?: string | Date /** * The associated products. - * + * * @expandable */ products?: ProductDTO[] @@ -452,7 +452,7 @@ export interface ProductCollectionDTO { /** * @interface - * + * * A product type's data. */ export interface ProductTypeDTO { @@ -476,9 +476,9 @@ export interface ProductTypeDTO { /** * @interface - * + * * A product option's data. - * + * */ export interface ProductOptionDTO { /** @@ -491,13 +491,13 @@ export interface ProductOptionDTO { title: string /** * The associated product. - * + * * @expandable */ product: ProductDTO /** * The associated product option values. - * + * * @expandable */ values: ProductOptionValueDTO[] @@ -513,8 +513,13 @@ export interface ProductOptionDTO { /** * @interface - * + * * The product image's data. + * + * @prop id - The ID of the product image. + * @prop url - The URL of the product image. + * @prop metadata - Holds custom data in key-value pairs. + * @prop deleted_at - When the product image was deleted. */ export interface ProductImageDTO { /** @@ -537,8 +542,15 @@ export interface ProductImageDTO { /** * @interface - * + * * The product option value's data. + * + * @prop id - The ID of the product option value. + * @prop value - The value of the product option value. + * @prop option - The associated product option. It may only be available if the `option` relation is expanded. + * @prop variant - The associated product variant. It may only be available if the `variant` relation is expanded. + * @prop metadata - Holds custom data in key-value pairs. + * @prop deleted_at - When the product option value was deleted. */ export interface ProductOptionValueDTO { /** @@ -551,13 +563,13 @@ export interface ProductOptionValueDTO { value: string /** * The associated product option. - * + * * @expandable */ option: ProductOptionDTO /** * The associated product variant. - * + * * @expandable */ variant: ProductVariantDTO @@ -573,8 +585,15 @@ export interface ProductOptionValueDTO { /** * @interface - * + * * The filters to apply on retrieved products. + * + * @prop q - Search through the products' attributes, such as titles and descriptions, using this search term. + * @prop handle - The handles to filter products by. + * @prop id - The IDs to filter products by. + * @prop tags - Filters on a product's tags. + * @prop categories - Filters on a product's categories. + * @prop collection_id - Filters a product by its associated collections. */ export interface FilterableProductProps extends BaseFilterable { @@ -593,7 +612,7 @@ export interface FilterableProductProps /** * Filters on a product's tags. */ - tags?: { + tags?: { /** * Values to filter product tags by. */ @@ -628,8 +647,11 @@ export interface FilterableProductProps /** * @interface - * + * * The filters to apply on retrieved product tags. + * + * @prop id - The IDs to filter product tags by. + * @prop value - The value to filter product tags by. */ export interface FilterableProductTagProps extends BaseFilterable { @@ -645,8 +667,11 @@ export interface FilterableProductTagProps /** * @interface - * + * * The filters to apply on retrieved product types. + * + * @prop id - The IDs to filter product types by. + * @prop value - The value to filter product types by. */ export interface FilterableProductTypeProps extends BaseFilterable { @@ -662,8 +687,12 @@ export interface FilterableProductTypeProps /** * @interface - * + * * The filters to apply on retrieved product options. + * + * @prop id - The IDs to filter product options by. + * @prop title - The titles to filter product options by. + * @prop product_id - Filter the product options by their associated products' IDs. */ export interface FilterableProductOptionProps extends BaseFilterable { @@ -683,8 +712,11 @@ export interface FilterableProductOptionProps /** * @interface - * + * * The filters to apply on retrieved product collections. + * + * @prop id - The IDs to filter product collections by. + * @prop title - The title to filter product collections by. */ export interface FilterableProductCollectionProps extends BaseFilterable { @@ -704,8 +736,13 @@ export interface FilterableProductCollectionProps /** * @interface - * + * * The filters to apply on retrieved product variants. + * + * @prop id - The IDs to filter product variants by. + * @prop sku - The SKUs to filter product variants by. + * @prop product_id - Filter the product variants by their associated products' IDs. + * @prop options - Filter product variants by their associated options. */ export interface FilterableProductVariantProps extends BaseFilterable { @@ -724,7 +761,7 @@ export interface FilterableProductVariantProps /** * Filter product variants by their associated options. */ - options?: { + options?: { /** * IDs to filter options by. */ @@ -734,8 +771,16 @@ export interface FilterableProductVariantProps /** * @interface - * + * * The filters to apply on retrieved product categories. + * + * @prop id - The IDs to filter product categories by. + * @prop name - The names to filter product categories by. + * @prop parent_category_id - Filter product categories by their parent category's ID. + * @prop handle - The handles to filter product categories by. + * @prop is_active - Filter product categories by whether they're active. + * @prop is_internal - Filter product categories by whether they're internal. + * @prop include_descendants_tree - Whether to include children of retrieved product categories. */ export interface FilterableProductCategoryProps extends BaseFilterable { @@ -771,8 +816,13 @@ export interface FilterableProductCategoryProps /** * @interface - * + * * A product collection to create. + * + * @prop title - The product collection's title. + * @prop handle - The product collection's handle. If not provided, the value of this attribute is set to the slug version of the title. + * @prop products - The products to associate with the collection. + * @prop metadata - Holds custom data in key-value pairs. */ export interface CreateProductCollectionDTO { /** @@ -795,7 +845,7 @@ export interface CreateProductCollectionDTO { /** * @interface - * + * * The data to update in a product collection. The `id` is used to identify which product collection to update. */ export interface UpdateProductCollectionDTO { @@ -827,7 +877,7 @@ export interface UpdateProductCollectionDTO { /** * @interface - * + * * A product type to create. */ export interface CreateProductTypeDTO { @@ -852,7 +902,7 @@ export interface UpsertProductTypeDTO { /** * @interface - * + * * The data to update in a product type. The `id` is used to identify which product type to update. */ export interface UpdateProductTypeDTO { @@ -872,7 +922,7 @@ export interface UpdateProductTypeDTO { /** * @interface - * + * * A product tag to create. */ export interface CreateProductTagDTO { @@ -888,9 +938,9 @@ export interface UpsertProductTagDTO { } /** - * + * * @interface - * + * * The data to update in a product tag. The `id` is used to identify which product tag to update. */ export interface UpdateProductTagDTO { @@ -906,7 +956,7 @@ export interface UpdateProductTagDTO { /** * @interface - * + * * A product option to create. */ export interface CreateProductOptionDTO { @@ -928,7 +978,7 @@ export interface UpdateProductOptionDTO { /** * @interface - * + * * A product variant option to create. */ export interface CreateProductVariantOptionDTO { @@ -936,14 +986,20 @@ export interface CreateProductVariantOptionDTO { * The value of a product variant option. */ value: string + + option_id?: string } /** * @interface - * + * * A product variant to create. */ export interface CreateProductVariantDTO { + /** + * The id of the product + */ + product_id?: string /** * The tile of the product variant. */ @@ -1020,7 +1076,7 @@ export interface CreateProductVariantDTO { /** * @interface - * + * * The data to update in a product variant. The `id` is used to identify which product variant to update. */ export interface UpdateProductVariantDTO { @@ -1104,7 +1160,7 @@ export interface UpdateProductVariantDTO { /** * @interface - * + * * A product to create. */ export interface CreateProductDTO { @@ -1214,7 +1270,7 @@ export interface CreateProductDTO { /** * @interface - * + * * The data to update in a product. The `id` is used to identify which product to update. */ export interface UpdateProductDTO { @@ -1352,6 +1408,7 @@ export interface CreateProductOnlyDTO { } export interface CreateProductVariantOnlyDTO { + product_id?: string title: string sku?: string barcode?: string diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 14a67c4260abb..00b3320fe187a 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -5,6 +5,7 @@ import { CreateProductOptionDTO, CreateProductTagDTO, CreateProductTypeDTO, + CreateProductVariantDTO, FilterableProductCategoryProps, FilterableProductCollectionProps, FilterableProductOptionProps, @@ -25,12 +26,13 @@ import { UpdateProductOptionDTO, UpdateProductTagDTO, UpdateProductTypeDTO, + UpdateProductVariantDTO, } from "./common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" +import { Context } from "../shared-context" import { FindConfig } from "../common" -import { RestoreReturn, SoftDeleteReturn } from "../dal" import { ModuleJoinerConfig } from "../modules-sdk" -import { Context } from "../shared-context" export interface IProductModuleService { /** @@ -40,45 +42,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product by its ID - * - * @param {string} productId - The ID of the product to retrieve. - * @param {FindConfig} config - + * + * @param {string} productId - The ID of the product to retrieve. + * @param {FindConfig} config - * The configurations determining how the product is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product. - * + * * @example * A simple example that retrieves a product by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProduct (id: string) { * const productModule = await initializeProductModule() - * + * * const product = await productModule.retrieve(id) - * + * * // do something with the product or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProduct (id: string) { * const productModule = await initializeProductModule() - * + * * const product = await productModule.retrieve(id, { * relations: ["categories"] * }) - * + * * // do something with the product or return it * } * ``` @@ -91,85 +93,85 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of price sets based on optional filters and configuration. - * + * * @param {FilterableProductProps} filters - The filters to apply on the retrieved products. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the products are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of products. - * + * * @example * To retrieve a list of products using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }) - * + * * // do something with the products or return them * } * ``` - * + * * To specify relations that should be retrieved within the products: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }, { * relations: ["categories"] * }) - * + * * // do something with the products or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * id: ids * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.list({ * $and: [ * { @@ -181,10 +183,10 @@ export interface IProductModuleService { * ] * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` @@ -197,85 +199,85 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of products along with the total count of available products satisfying the provided filters. - * + * * @param {FilterableProductProps} filters - The filters to apply on the retrieved products. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the products are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of products along with the total count. - * + * * @example * To retrieve a list of products using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }) - * + * * // do something with the products or return them * } * ``` - * + * * To specify relations that should be retrieved within the products: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }, { * relations: ["categories"] * }) - * + * * // do something with the products or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * id: ids * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProducts (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [products, count] = await productModule.listAndCount({ * $and: [ * { @@ -287,10 +289,10 @@ export interface IProductModuleService { * ] * }, { * relations: ["categories"], - * skip, + * skip, * take * }) - * + * * // do something with the products or return them * } * ``` @@ -303,45 +305,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a tag by its ID. - * - * @param {string} tagId - The ID of the tag to retrieve. - * @param {FindConfig} config - + * + * @param {string} tagId - The ID of the tag to retrieve. + * @param {FindConfig} config - * The configurations determining how the product tag is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product tag. - * + * * @example * A simple example that retrieves a product tag by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagId: string) { * const productModule = await initializeProductModule() - * + * * const productTag = await productModule.retrieveTag(tagId) - * + * * // do something with the product tag or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagId: string) { * const productModule = await initializeProductModule() - * + * * const productTag = await productModule.retrieveTag(tagId, { * relations: ["products"] * }) - * + * * // do something with the product tag or return it * } * ``` @@ -354,63 +356,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of tags based on optional filters and configuration. - * + * * @param {FilterableProductTagProps} filters - The filters applied on the retrieved product tags. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product tags are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product tags. - * + * * @example * To retrieve a list of product tags using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * To specify relations that should be retrieved within the product tags: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }, { * relations: ["products"] * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * id: tagIds * }, { @@ -418,21 +420,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.listTags({ * $and: [ * { @@ -447,7 +449,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` @@ -460,63 +462,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product tags along with the total count of available product tags satisfying the provided filters. - * + * * @param {FilterableProductTagProps} filters - The filters applied on the retrieved product tags. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product tags are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductTagDTO[], number]>} The list of product tags along with the total count. - * + * * @example * To retrieve a list of product tags using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * To specify relations that should be retrieved within the product tags: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }, { * relations: ["products"] * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * id: tagIds * }, { @@ -524,21 +526,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTag (tagIds: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTags, count] = await productModule.listAndCountTags({ * $and: [ * { @@ -553,7 +555,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product tags or return them * } * ``` @@ -566,25 +568,25 @@ export interface IProductModuleService { /** * This method is used to create product tags. - * + * * @param {CreateProductTagDTO[]} data - The product tags to create. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product tags. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductTags (values: string[]) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.createTags( * values.map((value) => ({ * value * })) * ) - * + * * // do something with the product tags or return them * } */ @@ -595,26 +597,26 @@ export interface IProductModuleService { /** * This method is used to update existing product tags. - * + * * @param {UpdateProductTagDTO[]} data - The product tags to be updated, each having the attributes that should be updated in a product tag. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product tags. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductTag (id: string, value: string) { * const productModule = await initializeProductModule() - * + * * const productTags = await productModule.updateTags([ * { * id, * value * } * ]) - * + * * // do something with the product tags or return them * } */ @@ -625,66 +627,66 @@ export interface IProductModuleService { /** * This method is used to delete product tags by their ID. - * + * * @param {string[]} productTagIds - The IDs of the product tags to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product tags are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductTags (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteTags(ids) - * + * * } */ deleteTags(productTagIds: string[], sharedContext?: Context): Promise /** * This method is used to retrieve a product type by its ID. - * + * * @param {string} typeId - The ID of the product type to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product type is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product type. - * + * * @example * A simple example that retrieves a product type by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductType (id: string) { * const productModule = await initializeProductModule() - * + * * const productType = await productModule.retrieveType(id) - * + * * // do something with the product type or return it * } * ``` - * + * * To specify attributes that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductType (id: string) { * const productModule = await initializeProductModule() - * + * * const productType = await productModule.retrieveType(id, { * select: ["value"] * }) - * + * * // do something with the product type or return it * } * ``` @@ -697,63 +699,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product types based on optional filters and configuration. - * + * * @param {FilterableProductTypeProps} filters - The filters to apply on the retrieved product types. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product types are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product types. - * + * * @example * To retrieve a list of product types using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }) - * + * * // do something with the product types or return them * } * ``` - * + * * To specify attributes that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }, { * select: ["value"] * }) - * + * * // do something with the product types or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * id: ids * }, { @@ -761,21 +763,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.listTypes({ * $and: [ * { @@ -790,7 +792,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` @@ -803,63 +805,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product types along with the total count of available product types satisfying the provided filters. - * + * * @param {FilterableProductTypeProps} filters - The filters to be applied on the retrieved product type. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product types are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductTypeDTO[], number]>} The list of product types along with their total count. - * + * * @example * To retrieve a list of product types using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }) - * + * * // do something with the product types or return them * } * ``` - * + * * To specify attributes that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }, { * select: ["value"] * }) - * + * * // do something with the product types or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * id: ids * }, { @@ -867,21 +869,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductTypes (ids: string[], value: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productTypes, count] = await productModule.listAndCountTypes({ * $and: [ * { @@ -896,7 +898,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product types or return them * } * ``` @@ -909,25 +911,25 @@ export interface IProductModuleService { /** * This method is used to create a product type. - * + * * @param {CreateProductTypeDTO[]} data - The product types to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @return {Promise} The list of created product types. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductType (value: string) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.createTypes([ * { * value * } * ]) - * + * * // do something with the product types or return them * } */ @@ -938,26 +940,26 @@ export interface IProductModuleService { /** * This method is used to update a product type - * + * * @param {UpdateProductTypeDTO[]} data - The product types to be updated, each having the attributes that should be updated in the product type. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product types. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductType (id: string, value: string) { * const productModule = await initializeProductModule() - * + * * const productTypes = await productModule.updateTypes([ * { * id, * value * } * ]) - * + * * // do something with the product types or return them * } */ @@ -968,19 +970,19 @@ export interface IProductModuleService { /** * This method is used to delete a product type. - * + * * @param {string[]} productTypeIds - The IDs of the product types to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product types are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductTypes (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteTypes(ids) * } */ @@ -988,45 +990,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product option by its ID. - * + * * @param {string} optionId - The ID of the product option to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product option is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product option. - * + * * @example * A simple example that retrieves a product option by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOption (id: string) { * const productModule = await initializeProductModule() - * + * * const productOption = await productModule.retrieveOption(id) - * + * * // do something with the product option or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOption (id: string) { * const productModule = await initializeProductModule() - * + * * const productOption = await productModule.retrieveOption(id, { * relations: ["product"] * }) - * + * * // do something with the product option or return it * } * ``` @@ -1039,63 +1041,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product options based on optional filters and configuration. - * + * * @param {FilterableProductOptionProps} filters - The filters applied on the retrieved product options. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product options are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product options. - * + * * @example * To retrieve a list of product options using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }) - * + * * // do something with the product options or return them * } * ``` - * + * * To specify relations that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }, { * relations: ["product"] * }) - * + * * // do something with the product options or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * id: ids * }, { @@ -1103,21 +1105,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.listOptions({ * $and: [ * { @@ -1132,7 +1134,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` @@ -1145,63 +1147,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product options along with the total count of available product options satisfying the provided filters. - * + * * @param {FilterableProductOptionProps} filters - The filters applied on the retrieved product options. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product options are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductOptionDTO[], number]>} The list of product options along with the total count. - * + * * @example * To retrieve a list of product options using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }) - * + * * // do something with the product options or return them * } * ``` - * + * * To specify relations that should be retrieved within the product types: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }, { * relations: ["product"] * }) - * + * * // do something with the product options or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * id: ids * }, { @@ -1209,21 +1211,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductOptions (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [productOptions, count] = await productModule.listAndCountOptions({ * $and: [ * { @@ -1238,7 +1240,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product options or return them * } * ``` @@ -1251,26 +1253,26 @@ export interface IProductModuleService { /** * This method is used to create product options. - * + * * @param {CreateProductOptionDTO[]} data - The product options to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {ProductOptionDTO[]} The list of created product options. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductOption (title: string, productId: string) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.createOptions([ * { * title, * product_id: productId * } * ]) - * + * * // do something with the product options or return them * } */ @@ -1281,26 +1283,26 @@ export interface IProductModuleService { /** * This method is used to update existing product options. - * + * * @param {UpdateProductOptionDTO[]} data - The product options to be updated, each holding the attributes that should be updated in the product option. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {ProductOptionDTO[]} The list of updated product options. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProductOption (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const productOptions = await productModule.updateOptions([ * { * id, * title * } * ]) - * + * * // do something with the product options or return them * } */ @@ -1311,19 +1313,19 @@ export interface IProductModuleService { /** * This method is used to delete a product option. - * + * * @param {string[]} productOptionIds - The IDs of the product options to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product options are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProductOptions (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteOptions(ids) * } */ @@ -1334,45 +1336,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product variant by its ID. - * + * * @param {string} productVariantId - The ID of the product variant to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variant is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product variant. - * + * * @example * A simple example that retrieves a product variant by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariant (id: string) { * const productModule = await initializeProductModule() - * + * * const variant = await productModule.retrieveVariant(id) - * + * * // do something with the product variant or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariant (id: string) { * const productModule = await initializeProductModule() - * + * * const variant = await productModule.retrieveVariant(id, { * relations: ["options"] * }) - * + * * // do something with the product variant or return it * } * ``` @@ -1385,63 +1387,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product variants based on optional filters and configuration. - * + * * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product variants. - * + * * @example * To retrieve a list of product variants using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * To specify relations that should be retrieved within the product variants: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }, { * relations: ["options"] * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * id: ids * }, { @@ -1449,21 +1451,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const variants = await productModule.listVariants({ * $and: [ * { @@ -1478,7 +1480,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` @@ -1489,65 +1491,98 @@ export interface IProductModuleService { sharedContext?: Context ): Promise + updateVariants( + data: UpdateProductVariantDTO[], + sharedContext?: Context + ): Promise + + createVariants( + data: CreateProductVariantDTO[], + sharedContext?: Context + ): Promise + + /** + * This method is used to delete ProductVariant. This method will completely remove the ProductVariant and they can no longer be accessed or retrieved. + * + * @param {string[]} productVariantIds - The IDs of the ProductVariant to be deleted. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the ProductVariant are successfully deleted. + * + * @example + * import { + * initialize as initializeProductModule, + * } from "@medusajs/product" + * + * async function deleteProducts (ids: string[]) { + * const productModule = await initializeProductModule() + * + * await productModule.deleteVariants(ids) + * } + */ + deleteVariants( + productVariantIds: string[], + sharedContext?: Context + ): Promise + /** * This method is used to retrieve a paginated list of product variants along with the total count of available product variants satisfying the provided filters. - * + * * @param {FilterableProductVariantProps} filters - The filters applied on the retrieved product variants. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product variants are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product variant. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductVariantDTO[], number]>} The list of product variants along with their total count. - * + * * @example * To retrieve a list of product variants using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * To specify relations that should be retrieved within the product variants: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }, { * relations: ["options"] * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * id: ids * }, { @@ -1555,21 +1590,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveProductVariants (ids: string[], sku: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [variants, count] = await productModule.listAndCountVariants({ * $and: [ * { @@ -1584,7 +1619,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product variants or return them * } * ``` @@ -1597,45 +1632,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product collection by its ID. - * + * * @param {string} productCollectionId - The ID of the product collection to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collection is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product collection. - * + * * @example * A simple example that retrieves a product collection by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollection (id: string) { * const productModule = await initializeProductModule() - * + * * const collection = await productModule.retrieveCollection(id) - * + * * // do something with the product collection or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollection (id: string) { * const productModule = await initializeProductModule() - * + * * const collection = await productModule.retrieveCollection(id, { * relations: ["products"] * }) - * + * * // do something with the product collection or return it * } * ``` @@ -1648,63 +1683,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product collections based on optional filters and configuration. - * + * * @param {FilterableProductCollectionProps} filters - The filters applied on the retrieved product collections. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collections are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product collections. - * + * * @example * To retrieve a list of product collections using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * To specify relations that should be retrieved within the product collections: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }, { * relations: ["products"] * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * id: ids * }, { @@ -1712,21 +1747,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.listCollections({ * $and: [ * { @@ -1741,7 +1776,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` @@ -1754,63 +1789,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product collections along with the total count of available product collections satisfying the provided filters. - * + * * @param {FilterableProductCollectionProps} filters - The filters applied on the retrieved product collections. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product collections are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductCollectionDTO[], number]>} The list of product collections along with the total count. - * + * * @example * To retrieve a list of product collections using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * To specify relations that should be retrieved within the product collections: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }, { * relations: ["products"] * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * id: ids * }, { @@ -1818,21 +1853,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCollections (ids: string[], title: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [collections, count] = await productModule.listAndCountCollections({ * $and: [ * { @@ -1847,7 +1882,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product collections or return them * } * ``` @@ -1860,28 +1895,28 @@ export interface IProductModuleService { /** * This method is used to create product collections. - * + * * @param {CreateProductCollectionDTO[]} data - The product collections to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of created product collections. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createCollection (title: string) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.createCollections([ * { * title * } * ]) - * + * * // do something with the product collections or return them * } - * + * */ createCollections( data: CreateProductCollectionDTO[], @@ -1890,29 +1925,29 @@ export interface IProductModuleService { /** * This method is used to update existing product collections. - * + * * @param {UpdateProductCollectionDTO[]} data - The product collections to be updated, each holding the attributes that should be updated in the product collection. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated product collections. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateCollection (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const collections = await productModule.updateCollections([ * { * id, * title * } * ]) - * + * * // do something with the product collections or return them * } - * + * */ updateCollections( data: UpdateProductCollectionDTO[], @@ -1921,22 +1956,22 @@ export interface IProductModuleService { /** * This method is used to delete collections by their ID. - * + * * @param {string[]} productCollectionIds - The IDs of the product collections to be updated. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product options are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteCollection (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteCollections(ids) * } - * + * */ deleteCollections( productCollectionIds: string[], @@ -1945,45 +1980,45 @@ export interface IProductModuleService { /** * This method is used to retrieve a product category by its ID. - * + * * @param {string} productCategoryId - The ID of the product category to retrieve. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product category is retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The retrieved product category. - * + * * @example * A simple example that retrieves a product category by its ID: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategory (id: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.retrieveCategory(id) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategory (id: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.retrieveCategory(id, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` @@ -1996,63 +2031,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product categories based on optional filters and configuration. - * + * * @param {FilterableProductCategoryProps} filters - The filters to be applied on the retrieved product categories. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product categories are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of product categories. - * + * * @example * To retrieve a list of product categories using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved within the product categories: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * id: ids * }, { @@ -2060,21 +2095,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], name: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const categories = await productModule.listCategories({ * $or: [ * { @@ -2089,7 +2124,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` @@ -2102,63 +2137,63 @@ export interface IProductModuleService { /** * This method is used to retrieve a paginated list of product categories along with the total count of available product categories satisfying the provided filters. - * + * * @param {FilterableProductCategoryProps} filters - The filters to apply on the retrieved product categories. - * @param {FindConfig} config - + * @param {FindConfig} config - * The configurations determining how the product categories are retrieved. Its properties, such as `select` or `relations`, accept the * attributes or relations associated with a product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise<[ProductCategoryDTO[], number]>} The list of product categories along with their total count. - * + * * @example * To retrieve a list of product categories using their IDs: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }) - * + * * // do something with the product category or return it * } * ``` - * + * * To specify relations that should be retrieved within the product categories: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }, { * relations: ["parent_category"] * }) - * + * * // do something with the product category or return it * } * ``` - * + * * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * id: ids * }, { @@ -2166,21 +2201,21 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` - * + * * You can also use the `$and` or `$or` properties of the `filter` parameter to use and/or conditions in your filters. For example: - * + * * ```ts - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function retrieveCategories (ids: string[], name: string, skip: number, take: number) { * const productModule = await initializeProductModule() - * + * * const [categories, count] = await productModule.listAndCountCategories({ * $or: [ * { @@ -2195,7 +2230,7 @@ export interface IProductModuleService { * skip, * take * }) - * + * * // do something with the product category or return it * } * ``` @@ -2208,27 +2243,27 @@ export interface IProductModuleService { /** * This method is used to create a product category. - * + * * @param {CreateProductCategoryDTO} data - The product category to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created product category. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createCategory (name: string, parent_category_id: string | null) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.createCategory({ * name, * parent_category_id * }) - * + * * // do something with the product category or return it * } - * + * */ createCategory( data: CreateProductCategoryDTO, @@ -2237,24 +2272,24 @@ export interface IProductModuleService { /** * This method is used to update a product category by its ID. - * + * * @param {string} categoryId - The ID of the product category to update. * @param {UpdateProductCategoryDTO} data - The attributes to update in th product category. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated product category. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateCategory (id: string, name: string) { * const productModule = await initializeProductModule() - * + * * const category = await productModule.updateCategory(id, { * name, * }) - * + * * // do something with the product category or return it * } */ @@ -2266,19 +2301,19 @@ export interface IProductModuleService { /** * This method is used to delete a product category by its ID. - * + * * @param {string} categoryId - The ID of the product category to delete. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the product category is successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteCategory (id: string) { * const productModule = await initializeProductModule() - * + * * await productModule.deleteCategory(id) * } */ @@ -2286,25 +2321,25 @@ export interface IProductModuleService { /** * This method is used to create a product. - * + * * @param {CreateProductDTO[]} data - The products to be created. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of created products. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProduct (title: string) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.create([ * { * title * } * ]) - * + * * // do something with the products or return them * } */ @@ -2315,26 +2350,26 @@ export interface IProductModuleService { /** * This method is used to update a product. - * + * * @param {UpdateProductDTO[]} data - The products to be updated, each holding the attributes that should be updated in the product. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The list of updated products. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function updateProduct (id: string, title: string) { * const productModule = await initializeProductModule() - * + * * const products = await productModule.update([ * { * id, * title * } * ]) - * + * * // do something with the products or return them * } */ @@ -2345,19 +2380,19 @@ export interface IProductModuleService { /** * This method is used to delete products. Unlike the {@link softDelete} method, this method will completely remove the products and they can no longer be accessed or retrieved. - * + * * @param {string[]} productIds - The IDs of the products to be deleted. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the products are successfully deleted. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * await productModule.delete(ids) * } */ @@ -2365,29 +2400,29 @@ export interface IProductModuleService { /** * This method is used to delete products. Unlike the {@link delete} method, this method won't completely remove the product. It can still be accessed or retrieved using methods like {@link retrieve} if you pass the `withDeleted` property to the `config` object parameter. - * + * * The soft-deleted products can be restored using the {@link restore} method. - * + * * @param {string[]} productIds - The IDs of the products to soft-delete. * @param {SoftDeleteReturn} config - * Configurations determining which relations to soft delete along with the each of the products. You can pass to its `returnLinkableKeys` * property any of the product's relation attribute names, such as `variant_id`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} + * @returns {Promise | void>} * An object that includes the IDs of related records that were also soft deleted, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of a record associated with the product through this relation, such as the IDs of associated product variants. - * + * * If there are no related records, the promise resolved to `void`. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function deleteProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const cascadedEntities = await productModule.softDelete(ids) - * + * * // do something with the returned cascaded entity IDs or return them * } */ @@ -2399,29 +2434,29 @@ export interface IProductModuleService { /** * This method is used to restore products which were deleted using the {@link softDelete} method. - * + * * @param {string[]} productIds - The IDs of the products to restore. * @param {RestoreReturn} config - * Configurations determining which relations to restore along with the each of the products. You can pass to its `returnLinkableKeys` * property any of the product's relation attribute names, such as `variant_id`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} + * @returns {Promise | void>} * An object that includes the IDs of related records that were restored, such as the ID of associated product variants. The object's keys are the ID attribute names of the product entity's relations, such as `variant_id`, and its value is an array of strings, each being the ID of the record associated with the product through this relation, such as the IDs of associated product variants. - * + * * If there are no related records that were restored, the promise resolved to `void`. - * + * * @example - * import { + * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function restoreProducts (ids: string[]) { * const productModule = await initializeProductModule() - * + * * const cascadedEntities = await productModule.restore(ids, { * returnLinkableKeys: ["variant_id"] * }) - * + * * // do something with the returned cascaded entity IDs or return them * } */ diff --git a/packages/types/src/workflow/index.ts b/packages/types/src/workflow/index.ts index ecce74d8af45a..57ba877ed972a 100644 --- a/packages/types/src/workflow/index.ts +++ b/packages/types/src/workflow/index.ts @@ -2,3 +2,4 @@ export * as CartWorkflow from "./cart" export * as CommonWorkflow from "./common" export * as ProductWorkflow from "./product" export * as InventoryWorkflow from "./inventory" +export * as PriceListWorkflow from "./price-list" diff --git a/packages/types/src/workflow/price-list/create-price-list.ts b/packages/types/src/workflow/price-list/create-price-list.ts new file mode 100644 index 0000000000000..024a58ddb1c58 --- /dev/null +++ b/packages/types/src/workflow/price-list/create-price-list.ts @@ -0,0 +1,78 @@ +import { + CreatePriceListRules, + PriceListRuleDTO, + PriceListStatus, +} from "../../pricing" + +export interface CreatePriceListDTO { + starts_at?: string + ends_at?: string + status?: PriceListStatus + number_rules?: number + rules?: PriceListRuleDTO[] + prices?: { + amount: number + currency_code: string + region_id?: string + max_quantity?: number + min_quantity?: number + }[] + customer_groups?: { + id: string + }[] +} + +export interface CreatePriceListRuleDTO { + rule_attribute: string + value: string[] +} + +export interface CreatePriceListPriceDTO { + amount: number + currency_code: string + price_set_id: string | null + region_id?: string + max_quantity?: number + min_quantity?: number +} + +export interface CreatePriceListWorkflowInputDTO { + price_lists: CreatePriceListWorkflowDTO[] +} + +export interface RemovePriceListProductsWorkflowInputDTO { + product_ids: string[] + price_list_id: string +} + +export interface RemovePriceListVariantsWorkflowInputDTO { + variant_ids: string[] + price_list_id: string +} + +export interface RemovePriceListPricesWorkflowInputDTO { + money_amount_ids: string[] + price_list_id: string +} + +export interface CreatePriceListWorkflowDTO { + title?: string + name: string + description: string + type?: string + starts_at?: string + ends_at?: string + status?: PriceListStatus + number_rules?: number + prices: InputPrice[] + rules?: CreatePriceListRules +} + +interface InputPrice { + region_id?: string + currency_code: string + amount: number + variant_id: string + min_quantity?: number + max_quantity?: number +} diff --git a/packages/types/src/workflow/price-list/index.ts b/packages/types/src/workflow/price-list/index.ts new file mode 100644 index 0000000000000..3649befbea739 --- /dev/null +++ b/packages/types/src/workflow/price-list/index.ts @@ -0,0 +1,3 @@ +export * from "./create-price-list" +export * from "./update-price-list" +export * from "./remove-price-list" diff --git a/packages/types/src/workflow/price-list/remove-price-list.ts b/packages/types/src/workflow/price-list/remove-price-list.ts new file mode 100644 index 0000000000000..d6789e631e783 --- /dev/null +++ b/packages/types/src/workflow/price-list/remove-price-list.ts @@ -0,0 +1,3 @@ +export interface RemovePriceListWorkflowInputDTO { + price_lists: string[] +} diff --git a/packages/types/src/workflow/price-list/update-price-list.ts b/packages/types/src/workflow/price-list/update-price-list.ts new file mode 100644 index 0000000000000..a0f035267fee6 --- /dev/null +++ b/packages/types/src/workflow/price-list/update-price-list.ts @@ -0,0 +1,25 @@ +import { CreatePriceListRules, PriceListStatus } from "../../pricing" + +import { UpdateProductVariantPricesInputDTO } from "../product" + +export type PriceListVariantPriceDTO = UpdateProductVariantPricesInputDTO & { + variant_id?: string + price_set_id?: string +} + +export interface UpdatePriceListWorkflowDTO { + id: string + name?: string + starts_at?: string + ends_at?: string + status?: PriceListStatus + rules?: CreatePriceListRules + prices?: PriceListVariantPriceDTO[] + customer_groups?: { + id: string + }[] +} + +export interface UpdatePriceListWorkflowInputDTO { + price_lists: UpdatePriceListWorkflowDTO[] +} diff --git a/packages/types/src/workflow/product/create-product-variants.ts b/packages/types/src/workflow/product/create-product-variants.ts new file mode 100644 index 0000000000000..1d7fb0eebad0c --- /dev/null +++ b/packages/types/src/workflow/product/create-product-variants.ts @@ -0,0 +1,32 @@ +import { + UpsertProductVariantOptionInputDTO, + UpsertProductVariantPricesInputDTO, +} from "./update-product-variants" + +export interface CreateProductVariantsInputDTO { + product_id?: string + title?: string + sku?: string + ean?: string + upc?: string + barcode?: string + hs_code?: string + inventory_quantity?: number + allow_backorder?: boolean + manage_inventory?: boolean + weight?: number + length?: number + height?: number + width?: number + origin_country?: string + mid_code?: string + material?: string + metadata?: Record + + prices?: UpsertProductVariantPricesInputDTO[] + options?: UpsertProductVariantOptionInputDTO[] +} + +export interface CreateProductVariantsWorkflowInputDTO { + productVariants: CreateProductVariantsInputDTO[] +} diff --git a/packages/types/src/workflow/product/index.ts b/packages/types/src/workflow/product/index.ts index f960c39643df7..6ff12931de41c 100644 --- a/packages/types/src/workflow/product/index.ts +++ b/packages/types/src/workflow/product/index.ts @@ -1,3 +1,4 @@ +export * from "./create-product-variants" export * from "./create-products" export * from "./update-product-variants" export * from "./update-products" diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 11596a9825ed9..0bcc495237a80 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -137,6 +137,8 @@ export abstract class MikroOrmAbstractBaseRepository retrieveConstraintsToApply: (q: string) => any[] ): void { if (!("q" in findOptions.where) || !findOptions.where.q) { + delete findOptions.where.q + return } diff --git a/packages/workflows/src/definition/index.ts b/packages/workflows/src/definition/index.ts index 50b5440ede1f4..805f14718f34c 100644 --- a/packages/workflows/src/definition/index.ts +++ b/packages/workflows/src/definition/index.ts @@ -1,3 +1,4 @@ export * from "./cart" export * from "./product" export * from "./inventory" +export * from './price-list' \ No newline at end of file diff --git a/packages/workflows/src/definition/price-list/create-price-lists.ts b/packages/workflows/src/definition/price-list/create-price-lists.ts new file mode 100644 index 0000000000000..0a54a3dbb5f25 --- /dev/null +++ b/packages/workflows/src/definition/price-list/create-price-lists.ts @@ -0,0 +1,74 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { PricingTypes, WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum CreatePriceListActions { + prepare = "prepare", + createPriceList = "createPriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: CreatePriceListActions.prepare, + noCompensation: true, + next: { + action: CreatePriceListActions.createPriceList, + }, + }, +} + +const handlers = new Map([ + [ + CreatePriceListActions.prepare, + { + invoke: pipe( + { + inputAlias: CreatePriceListActions.prepare, + merge: true, + invoke: { + from: CreatePriceListActions.prepare, + }, + }, + PriceListHandlers.prepareCreatePriceLists + ), + }, + ], + [ + CreatePriceListActions.createPriceList, + { + invoke: pipe( + { + invoke: { + from: CreatePriceListActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.createPriceLists + ), + compensate: pipe( + { + invoke: { + from: CreatePriceListActions.createPriceList, + alias: PriceListHandlers.removePriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.CreatePriceList, workflowSteps, handlers) + +export const createPriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.CreatePriceListWorkflowInputDTO, + { + priceList: PricingTypes.CreatePriceListDTO + }[] +>(Workflows.CreatePriceList, CreatePriceListActions.createPriceList) diff --git a/packages/workflows/src/definition/price-list/index.ts b/packages/workflows/src/definition/price-list/index.ts new file mode 100644 index 0000000000000..28dac74acd6b9 --- /dev/null +++ b/packages/workflows/src/definition/price-list/index.ts @@ -0,0 +1,5 @@ +export * from "./create-price-lists" +export * from "./remove-price-lists" +export * from "./remove-product-prices" +export * from "./remove-variant-prices" +export * from "./update-price-lists" diff --git a/packages/workflows/src/definition/price-list/remove-price-list-prices.ts b/packages/workflows/src/definition/price-list/remove-price-list-prices.ts new file mode 100644 index 0000000000000..086d79cf8ff72 --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-price-list-prices.ts @@ -0,0 +1,71 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemovePriceListPricesActions { + prepare = "prepare", + removePriceListPrices = "removePriceListPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemovePriceListPricesActions.prepare, + noCompensation: true, + next: { + action: RemovePriceListPricesActions.removePriceListPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemovePriceListPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemovePriceListPricesActions.prepare, + merge: true, + invoke: { + from: RemovePriceListPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemovePriceListPrices + ), + }, + ], + [ + RemovePriceListPricesActions.removePriceListPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemovePriceListPricesActions.prepare, + }, + }, + PriceListHandlers.removePrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListPrices, + workflowSteps, + handlers +) + +export const removePriceListProductPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListPricesWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListPrices, + RemovePriceListPricesActions.removePriceListPrices +) diff --git a/packages/workflows/src/definition/price-list/remove-price-lists.ts b/packages/workflows/src/definition/price-list/remove-price-lists.ts new file mode 100644 index 0000000000000..6a4b3f39dc2ae --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-price-lists.ts @@ -0,0 +1,57 @@ +import { WorkflowTypes } from "@medusajs/types" +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { exportWorkflow, pipe } from "../../helper" + +import { PriceListHandlers } from "../../handlers" +import { Workflows } from "../../definitions" + +export enum RemovePriceListActions { + removePriceList = "removePriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemovePriceListActions.removePriceList, + noCompensation: true, + }, +} + +const handlers = new Map([ + [ + RemovePriceListActions.removePriceList, + { + invoke: pipe( + { + inputAlias: RemovePriceListActions.removePriceList, + merge: true, + invoke: { + from: RemovePriceListActions.removePriceList, + }, + }, + PriceListHandlers.removePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.DeletePriceLists, workflowSteps, handlers) + +export const removePriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListWorkflowInputDTO, + { + price_list_ids: string[] + } +>( + Workflows.DeletePriceLists, + RemovePriceListActions.removePriceList, + async (data) => { + return { + price_lists: data.price_lists.map((priceListId) => ({ + price_list: { id: priceListId }, + })), + } + } +) diff --git a/packages/workflows/src/definition/price-list/remove-product-prices.ts b/packages/workflows/src/definition/price-list/remove-product-prices.ts new file mode 100644 index 0000000000000..248129c00d842 --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-product-prices.ts @@ -0,0 +1,72 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemoveProductPricesActions { + prepare = "prepare", + removePriceListPriceSetPrices = "removePriceListPriceSetPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemoveProductPricesActions.prepare, + noCompensation: true, + next: { + action: RemoveProductPricesActions.removePriceListPriceSetPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemoveProductPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemoveProductPricesActions.prepare, + merge: true, + invoke: { + from: RemoveProductPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemoveProductPrices + ), + }, + ], + [ + RemoveProductPricesActions.removePriceListPriceSetPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemoveProductPricesActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceListPriceSetPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListProductPrices, + workflowSteps, + handlers +) + +export const removePriceListProductPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListProductsWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListProductPrices, + RemoveProductPricesActions.removePriceListPriceSetPrices +) diff --git a/packages/workflows/src/definition/price-list/remove-variant-prices.ts b/packages/workflows/src/definition/price-list/remove-variant-prices.ts new file mode 100644 index 0000000000000..b68ef60ed75c1 --- /dev/null +++ b/packages/workflows/src/definition/price-list/remove-variant-prices.ts @@ -0,0 +1,72 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum RemoveVariantPricesActions { + prepare = "prepare", + removePriceListPriceSetPrices = "removePriceListPriceSetPrices", +} + +const workflowSteps: TransactionStepsDefinition = { + next: { + action: RemoveVariantPricesActions.prepare, + noCompensation: true, + next: { + action: RemoveVariantPricesActions.removePriceListPriceSetPrices, + noCompensation: true, + }, + }, +} + +const handlers = new Map([ + [ + RemoveVariantPricesActions.prepare, + { + invoke: pipe( + { + inputAlias: RemoveVariantPricesActions.prepare, + merge: true, + invoke: { + from: RemoveVariantPricesActions.prepare, + }, + }, + PriceListHandlers.prepareRemoveVariantPrices + ), + }, + ], + [ + RemoveVariantPricesActions.removePriceListPriceSetPrices, + { + invoke: pipe( + { + merge: true, + invoke: { + from: RemoveVariantPricesActions.prepare, + alias: PriceListHandlers.createPriceLists.aliases.priceLists, + }, + }, + PriceListHandlers.removePriceListPriceSetPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.RemovePriceListVariantPrices, + workflowSteps, + handlers +) + +export const removePriceListVariantPrices = exportWorkflow< + WorkflowTypes.PriceListWorkflow.RemovePriceListVariantsWorkflowInputDTO, + string[] +>( + Workflows.RemovePriceListVariantPrices, + RemoveVariantPricesActions.removePriceListPriceSetPrices +) diff --git a/packages/workflows/src/definition/price-list/update-price-lists.ts b/packages/workflows/src/definition/price-list/update-price-lists.ts new file mode 100644 index 0000000000000..a445714199b55 --- /dev/null +++ b/packages/workflows/src/definition/price-list/update-price-lists.ts @@ -0,0 +1,65 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { WorkflowTypes } from "@medusajs/types" +import { exportWorkflow, pipe } from "../../helper" + +import { Workflows } from "../../definitions" +import { PriceListHandlers } from "../../handlers" + +export enum UpdatePriceListActions { + prepare = "prepare", + updatePriceList = "updatePriceList", +} + +const workflowSteps: TransactionStepsDefinition = { + action: UpdatePriceListActions.prepare, + noCompensation: true, + next: { + next: { + noCompensation: true, + action: UpdatePriceListActions.updatePriceList, + }, + }, +} + +const handlers = new Map([ + [ + UpdatePriceListActions.prepare, + { + invoke: pipe( + { + inputAlias: UpdatePriceListActions.prepare, + merge: true, + invoke: { + from: UpdatePriceListActions.prepare, + }, + }, + PriceListHandlers.prepareUpdatePriceLists + ), + }, + ], + [ + UpdatePriceListActions.updatePriceList, + { + invoke: pipe( + { + inputAlias: UpdatePriceListActions.updatePriceList, + merge: true, + invoke: { + from: UpdatePriceListActions.prepare, + }, + }, + PriceListHandlers.updatePriceLists + ), + }, + ], +]) + +WorkflowManager.register(Workflows.UpdatePriceLists, workflowSteps, handlers) + +export const updatePriceLists = exportWorkflow< + WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowInputDTO, + { priceList: WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowDTO }[] +>(Workflows.UpdatePriceLists, UpdatePriceListActions.updatePriceList) diff --git a/packages/workflows/src/definition/product/create-product-variants.ts b/packages/workflows/src/definition/product/create-product-variants.ts new file mode 100644 index 0000000000000..0322af48a2ccc --- /dev/null +++ b/packages/workflows/src/definition/product/create-product-variants.ts @@ -0,0 +1,121 @@ +import { + TransactionStepsDefinition, + WorkflowManager, +} from "@medusajs/orchestration" +import { InputAlias, Workflows } from "../../definitions" +import { exportWorkflow, pipe } from "../../helper" + +import { ProductTypes, WorkflowTypes } from "@medusajs/types" +import { ProductHandlers } from "../../handlers" + +export enum CreateProductVariantsActions { + prepare = "prepare", + createProductVariants = "createProductVariants", + revertProductVariantsCreate = "revertProductVariantsCreate", + upsertPrices = "upsertPrices", +} + +export const workflowSteps: TransactionStepsDefinition = { + next: { + action: CreateProductVariantsActions.prepare, + noCompensation: true, + next: { + action: CreateProductVariantsActions.createProductVariants, + next: [ + { + action: CreateProductVariantsActions.upsertPrices, + }, + ], + }, + }, +} + +const handlers = new Map([ + [ + CreateProductVariantsActions.prepare, + { + invoke: pipe( + { + merge: true, + inputAlias: InputAlias.ProductVariantsCreateInputData, + invoke: { + from: InputAlias.ProductVariantsCreateInputData, + }, + }, + ProductHandlers.createProductVariantsPrepareData + ), + }, + ], + [ + CreateProductVariantsActions.createProductVariants, + { + invoke: pipe( + { + merge: true, + invoke: { + from: CreateProductVariantsActions.prepare, + }, + }, + ProductHandlers.createProductVariants + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.prepare, + }, + { + from: CreateProductVariantsActions.createProductVariants, + }, + ], + }, + ProductHandlers.removeProductVariants + ), + }, + ], + [ + CreateProductVariantsActions.upsertPrices, + { + invoke: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.createProductVariants, + }, + ], + }, + ProductHandlers.upsertVariantPrices + ), + compensate: pipe( + { + merge: true, + invoke: [ + { + from: CreateProductVariantsActions.prepare, + }, + { + from: CreateProductVariantsActions.upsertPrices, + }, + ], + }, + ProductHandlers.revertVariantPrices + ), + }, + ], +]) + +WorkflowManager.register( + Workflows.CreateProductVariants, + workflowSteps, + handlers +) + +export const createProductVariants = exportWorkflow< + WorkflowTypes.ProductWorkflow.CreateProductVariantsWorkflowInputDTO, + ProductTypes.ProductVariantDTO[] +>( + Workflows.CreateProductVariants, + CreateProductVariantsActions.createProductVariants +) diff --git a/packages/workflows/src/definition/product/index.ts b/packages/workflows/src/definition/product/index.ts index ab2a40334d17b..c138e52cdd69f 100644 --- a/packages/workflows/src/definition/product/index.ts +++ b/packages/workflows/src/definition/product/index.ts @@ -1,3 +1,4 @@ +export * as CreateProductVariants from "./create-product-variants" export * from "./create-products" export * as UpdateProductVariants from "./update-product-variants" export * from "./update-products" diff --git a/packages/workflows/src/definitions.ts b/packages/workflows/src/definitions.ts index 5f4fa84616e20..645b88ce4e4a2 100644 --- a/packages/workflows/src/definitions.ts +++ b/packages/workflows/src/definitions.ts @@ -4,12 +4,21 @@ export enum Workflows { UpdateProducts = "update-products", // Product Variant workflows + CreateProductVariants = "create-product-variants", UpdateProductVariants = "update-product-variants", // Cart workflows CreateCart = "create-cart", CreateInventoryItems = "create-inventory-items", + + // Price list workflows + CreatePriceList = "create-price-list", + UpdatePriceLists = "update-price-lists", + DeletePriceLists = "delete-price-lists", + RemovePriceListProductPrices = "remove-price-list-products", + RemovePriceListVariantPrices = "remove-price-list-variants", + RemovePriceListPrices = "remove-price-list-prices", } export enum InputAlias { @@ -19,6 +28,7 @@ export enum InputAlias { ProductVariants = "productVariants", ProductVariantsUpdateInputData = "productVariantsUpdateInputData", + ProductVariantsCreateInputData = "productVariantsCreateInputData", InventoryItems = "inventoryItems", RemovedInventoryItems = "removedInventoryItems", diff --git a/packages/workflows/src/handlers/index.ts b/packages/workflows/src/handlers/index.ts index 5c0282342f7d9..2c3607b6670be 100644 --- a/packages/workflows/src/handlers/index.ts +++ b/packages/workflows/src/handlers/index.ts @@ -4,6 +4,7 @@ export * as CommonHandlers from "./common" export * as CustomerHandlers from "./customer" export * as InventoryHandlers from "./inventory" export * as MiddlewaresHandlers from "./middlewares" +export * as PriceListHandlers from "./price-list" export * as ProductHandlers from "./product" export * as RegionHandlers from "./region" export * as SalesChannelHandlers from "./sales-channel" diff --git a/packages/workflows/src/handlers/price-list/create-price-list.ts b/packages/workflows/src/handlers/price-list/create-price-list.ts new file mode 100644 index 0000000000000..1e69066082853 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/create-price-list.ts @@ -0,0 +1,42 @@ +import { + CreatePriceListDTO, + IPricingModuleService, + PriceListDTO, +} from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceList: PriceListDTO +}[] + +type Input = { + tag?: string + priceList: CreatePriceListDTO +}[] + +export async function createPriceLists({ + container, + data, +}: WorkflowArguments<{ + priceLists: Input +}>): Promise { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + return await Promise.all( + data.priceLists.map(async (item) => { + const [priceList] = await pricingService!.createPriceLists([ + item.priceList, + ]) + + return { tag: item.tag ?? priceList.id, priceList } + }) + ) +} + +createPriceLists.aliases = { + priceLists: "priceLists", +} diff --git a/packages/workflows/src/handlers/price-list/index.ts b/packages/workflows/src/handlers/price-list/index.ts new file mode 100644 index 0000000000000..9f95fd8c6b026 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/index.ts @@ -0,0 +1,10 @@ +export * from "./create-price-list" +export * from "./prepare-create-price-list" +export * from "./prepare-update-price-lists" +export * from "./remove-price-list" +export * from "./update-price-lists" +export * from "./prepare-remove-product-prices" +export * from "./remove-price-set-price-list-prices" +export * from "./prepare-remove-variant-prices" +export * from "./prepare-remove-price-list-prices" +export * from "./remove-prices" diff --git a/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts b/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts new file mode 100644 index 0000000000000..327badb612edd --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-create-price-list.ts @@ -0,0 +1,87 @@ +import { CreatePriceListDTO, PriceListWorkflow } from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import { WorkflowArguments } from "../../helper" + +type Result = { + tag?: string + priceList: CreatePriceListDTO +}[] + +export async function prepareCreatePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: (PriceListWorkflow.CreatePriceListWorkflowDTO & { + _associationTag?: string + })[] +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_lists } = data + + const variantIds = price_lists + .map((priceList) => priceList.prices.map((price) => price.variant_id)) + .flat() + + const variables = { + variant_id: variantIds, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + const variantIdPriceSetIdMap: Map = new Map( + variantPriceSets.map((variantPriceSet) => [ + variantPriceSet.variant_id, + variantPriceSet.price_set_id, + ]) + ) + + const variantsWithoutPriceSets: string[] = [] + + for (const variantId of variantIds) { + if (!variantIdPriceSetIdMap.has(variantId)) { + variantsWithoutPriceSets.push(variantId) + } + } + + if (variantsWithoutPriceSets.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No priceSet exist for variants: ${variantsWithoutPriceSets.join(", ")}` + ) + } + + return price_lists.map((priceListDTO) => { + priceListDTO.title ??= priceListDTO.name + const { _associationTag, name, prices, ...rest } = priceListDTO + + const priceList = rest as CreatePriceListDTO + + priceList.rules ??= {} + priceList.prices = + prices?.map((price) => { + const price_set_id = variantIdPriceSetIdMap.get(price.variant_id)! + + return { + currency_code: price.currency_code, + amount: price.amount, + min_quantity: price.min_quantity, + max_quantity: price.max_quantity, + price_set_id, + } + }) ?? [] + + return { priceList, tag: _associationTag } + }) +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts new file mode 100644 index 0000000000000..e31a63cb185c5 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-price-list-prices.ts @@ -0,0 +1,48 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + moneyAmountIds: string[] + priceListId: string +} + +export async function prepareRemovePriceListPrices({ + container, + data, +}: WorkflowArguments<{ + money_amount_ids: string[] + price_list_id: string +}>): Promise { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const { + price_list_id: priceListId, + money_amount_ids: moneyAmountIdsToDelete, + } = data + + const moneyAmounts = await pricingService.listMoneyAmounts( + { id: moneyAmountIdsToDelete }, + { + relations: [ + "price_set_money_amount", + "price_set_money_amount.price_list", + ], + } + ) + + const moneyAmountIds = moneyAmounts + .filter( + (moneyAmount) => + moneyAmount?.price_set_money_amount?.price_list?.id === priceListId + ) + .map((ma) => ma.id) + + return { moneyAmountIds, priceListId } +} + +prepareRemovePriceListPrices.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts new file mode 100644 index 0000000000000..34b22b9000f32 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-product-prices.ts @@ -0,0 +1,62 @@ +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +type Result = { + priceSetIds: string[] + priceListId: string +} + +export async function prepareRemoveProductPrices({ + container, + data, +}: WorkflowArguments<{ + product_ids: string[] + price_list_id: string +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_list_id, product_ids } = data + + const variables = { + id: product_ids, + } + + const query = { + product: { + __args: variables, + ...defaultAdminProductRemoteQueryObject, + }, + } + + const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query) + + const priceSetIds = productsWithVariantPriceSets + .map(({ variants }) => variants.map(({ price }) => price.price_set_id)) + .flat() + + return { priceSetIds, priceListId: price_list_id } +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} + +type QueryResult = { + id: string + variants: { + id: string + price: { + price_set_id: string + variant_id: string + } + }[] +} + +const defaultAdminProductRemoteQueryObject = { + fields: ["id"], + variants: { + price: { + fields: ["variant_id", "price_set_id"], + }, + }, +} diff --git a/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts b/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts new file mode 100644 index 0000000000000..d903fd459ffeb --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-remove-variant-prices.ts @@ -0,0 +1,56 @@ +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +type Result = { + priceSetIds: string[] + priceListId: string +} + +export async function prepareRemoveVariantPrices({ + container, + data, +}: WorkflowArguments<{ + variant_ids: string[] + price_list_id: string +}>): Promise { + const remoteQuery = container.resolve("remoteQuery") + + const { price_list_id, variant_ids } = data + + const variables = { + variant_id: variant_ids, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const productsWithVariantPriceSets: QueryResult[] = await remoteQuery(query) + + const priceSetIds = productsWithVariantPriceSets.map( + (variantPriceSet) => variantPriceSet.price_set_id + ) + + return { priceSetIds, priceListId: price_list_id } +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} + +type QueryResult = { + price_set_id: string + variant_id: string +} + +const defaultAdminProductRemoteQueryObject = { + fields: ["id"], + variants: { + price: { + fields: ["variant_id", "price_set_id"], + }, + }, +} diff --git a/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts b/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts new file mode 100644 index 0000000000000..490974b395560 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/prepare-update-price-lists.ts @@ -0,0 +1,87 @@ +import { + PriceListPriceDTO, + UpdatePriceListDTO, + WorkflowTypes, +} from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceLists: UpdatePriceListDTO[] + priceListPricesMap: Map +} + +export async function prepareUpdatePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: WorkflowTypes.PriceListWorkflow.UpdatePriceListWorkflowDTO[] +}>): Promise { + const { price_lists: priceListsData } = data + const remoteQuery = container.resolve("remoteQuery") + + const variantPriceSetMap = new Map() + const priceListPricesMap = new Map() + + const variantIds = priceListsData + .map((priceListData) => priceListData.prices?.map((p) => p.variant_id)) + .flat() + + const variables = { + variant_id: variantIds, + } + + const query = { + product_variant_price_set: { + __args: variables, + fields: ["variant_id", "price_set_id"], + }, + } + + const variantPriceSets = await remoteQuery(query) + + for (const { variant_id, price_set_id } of variantPriceSets) { + variantPriceSetMap.set(variant_id, price_set_id) + } + + const priceLists = priceListsData.map((priceListData) => { + const priceListPrices: PriceListPriceDTO[] = [] + + priceListData.prices?.forEach((price) => { + const { variant_id, ...priceData } = price + if (!variant_id) { + return + } + + priceListPrices.push({ + id: priceData.id, + price_set_id: variantPriceSetMap.get(variant_id) as string, + currency_code: priceData.currency_code as string, + amount: priceData.amount, + min_quantity: priceData.min_quantity, + max_quantity: priceData.max_quantity, + }) + + return + }) + + priceListPricesMap.set(priceListData.id, priceListPrices) + + delete priceListData?.prices + + const priceListDataClone: UpdatePriceListDTO = { + ...priceListData, + } + + if (priceListData.name) { + priceListDataClone.title = priceListData.name + } + + return priceListDataClone + }) + + return { priceLists, priceListPricesMap } +} + +prepareUpdatePriceLists.aliases = { + payload: "prepare", +} diff --git a/packages/workflows/src/handlers/price-list/remove-price-list.ts b/packages/workflows/src/handlers/price-list/remove-price-list.ts new file mode 100644 index 0000000000000..b38bd92ccaeb9 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-price-list.ts @@ -0,0 +1,31 @@ +import { IPricingModuleService, PriceListDTO } from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +export async function removePriceLists({ + container, + data, +}: WorkflowArguments<{ + price_lists: { + price_list: PriceListDTO + }[] +}>): Promise< + { + price_list: PriceListDTO + }[] +> { + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + await pricingService!.deletePriceLists( + data.price_lists.map(({ price_list }) => price_list.id) + ) + + return data.price_lists +} + +removePriceLists.aliases = { + priceLists: "priceLists", +} diff --git a/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts b/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts new file mode 100644 index 0000000000000..09ce0a382d1c2 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-price-set-price-list-prices.ts @@ -0,0 +1,39 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" +import { prepareCreatePriceLists } from "./prepare-create-price-list" + +export async function removePriceListPriceSetPrices({ + container, + data, +}: WorkflowArguments<{ + priceSetIds: string[] + priceListId: string +}>): Promise { + const { priceSetIds, priceListId } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const priceSetMoneyAmounts = await pricingService.listPriceSetMoneyAmounts( + { + price_set_id: priceSetIds, + price_list_id: [priceListId], + }, + { + relations: ["money_amount"], + } + ) + + const moneyAmountIDs = priceSetMoneyAmounts + .map((priceSetMoneyAmount) => priceSetMoneyAmount.money_amount?.id) + .filter((moneyAmountId): moneyAmountId is string => !!moneyAmountId) + + await pricingService.deleteMoneyAmounts(moneyAmountIDs) + + return moneyAmountIDs +} + +prepareCreatePriceLists.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/remove-prices.ts b/packages/workflows/src/handlers/price-list/remove-prices.ts new file mode 100644 index 0000000000000..159f74050ef36 --- /dev/null +++ b/packages/workflows/src/handlers/price-list/remove-prices.ts @@ -0,0 +1,29 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IPricingModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type Result = { + deletedPriceIds: string[] +} + +export async function removePrices({ + container, + data, +}: WorkflowArguments<{ + moneyAmountIds: string[] +}>): Promise { + const { moneyAmountIds } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + await pricingService.deleteMoneyAmounts(moneyAmountIds) + + return { + deletedPriceIds: moneyAmountIds, + } +} + +removePrices.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/price-list/update-price-lists.ts b/packages/workflows/src/handlers/price-list/update-price-lists.ts new file mode 100644 index 0000000000000..4e62603e47f3e --- /dev/null +++ b/packages/workflows/src/handlers/price-list/update-price-lists.ts @@ -0,0 +1,63 @@ +import { + AddPriceListPricesDTO, + IPricingModuleService, + PriceListDTO, + PriceListPriceDTO, + UpdateMoneyAmountDTO, + UpdatePriceListDTO, +} from "@medusajs/types" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { WorkflowArguments } from "../../helper" + +type Result = { + priceLists: PriceListDTO[] +} + +export async function updatePriceLists({ + container, + data, +}: WorkflowArguments<{ + priceLists: UpdatePriceListDTO[] + priceListPricesMap: Map +}>): Promise { + const { priceLists: priceListsData, priceListPricesMap } = data + const pricingService: IPricingModuleService = container.resolve( + ModuleRegistrationName.PRICING + ) + + const priceLists = await pricingService.updatePriceLists(priceListsData) + const addPriceListPricesData: AddPriceListPricesDTO[] = [] + const moneyAmountsToUpdate: UpdateMoneyAmountDTO[] = [] + + for (const [priceListId, prices] of priceListPricesMap.entries()) { + const moneyAmountsToCreate: PriceListPriceDTO[] = [] + + for (const price of prices) { + if (price.id) { + moneyAmountsToUpdate.push(price as UpdateMoneyAmountDTO) + } else { + moneyAmountsToCreate.push(price) + } + } + + addPriceListPricesData.push({ + priceListId, + prices: moneyAmountsToCreate, + }) + } + + if (addPriceListPricesData.length) { + await pricingService.addPriceListPrices(addPriceListPricesData) + } + + if (moneyAmountsToUpdate.length) { + await pricingService.updateMoneyAmounts(moneyAmountsToUpdate) + } + + return { priceLists } +} + +updatePriceLists.aliases = { + payload: "updatePriceLists", +} diff --git a/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts b/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts new file mode 100644 index 0000000000000..daadc4d82734d --- /dev/null +++ b/packages/workflows/src/handlers/product/create-product-variants-prepare-data.ts @@ -0,0 +1,67 @@ +import { ProductWorkflow, WorkflowTypes } from "@medusajs/types" + +import { WorkflowArguments } from "../../helper" + +type VariantPrice = { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +export type CreateProductVariantsPreparedData = { + productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] + variantIndexPricesMap: Map + productVariantsMap: Map< + string, + ProductWorkflow.CreateProductVariantsInputDTO[] + > +} + +export async function createProductVariantsPrepareData({ + container, + data, +}: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const productVariants: ProductWorkflow.CreateProductVariantsInputDTO[] = + data.productVariants || [] + + const variantIndexPricesMap = new Map() + const productVariantsMap = new Map< + string, + ProductWorkflow.CreateProductVariantsInputDTO[] + >() + + for (const [index, productVariantData] of productVariants.entries()) { + if (!productVariantData.product_id) { + continue + } + + variantIndexPricesMap.set(index, productVariantData.prices || []) + + delete productVariantData.prices + + const productVariants = productVariantsMap.get( + productVariantData.product_id + ) + + if (productVariants) { + productVariants.push(productVariantData) + } else { + productVariantsMap.set(productVariantData.product_id, [ + productVariantData, + ]) + } + } + + return { + productVariants, + variantIndexPricesMap, + productVariantsMap, + } +} + +createProductVariantsPrepareData.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/create-product-variants.ts b/packages/workflows/src/handlers/product/create-product-variants.ts new file mode 100644 index 0000000000000..054877d5a59ce --- /dev/null +++ b/packages/workflows/src/handlers/product/create-product-variants.ts @@ -0,0 +1,46 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { ProductTypes } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type VariantPrice = { + region_id?: string + currency_code?: string + amount: number + min_quantity?: number + max_quantity?: number +} + +type HandlerInput = { + productVariantsMap: Map + variantIndexPricesMap: Map +} + +export async function createProductVariants({ + container, + data, +}: WorkflowArguments): Promise<{ + productVariants: ProductTypes.ProductVariantDTO[] + variantPricesMap: Map +}> { + const { productVariantsMap, variantIndexPricesMap } = data + const variantPricesMap = new Map() + const productModuleService: ProductTypes.IProductModuleService = + container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) + + const productVariants = await productModuleService.createVariants( + [...productVariantsMap.values()].flat() + ) + + productVariants.forEach((variant, index) => { + variantPricesMap.set(variant.id, variantIndexPricesMap.get(index) || []) + }) + + return { + productVariants, + variantPricesMap, + } +} + +createProductVariants.aliases = { + payload: "payload", +} diff --git a/packages/workflows/src/handlers/product/create-products-prepare-data.ts b/packages/workflows/src/handlers/product/create-products-prepare-data.ts index b6b2f1c0c6c18..ebce044b592d3 100644 --- a/packages/workflows/src/handlers/product/create-products-prepare-data.ts +++ b/packages/workflows/src/handlers/product/create-products-prepare-data.ts @@ -1,8 +1,8 @@ import { ProductTypes, SalesChannelTypes, WorkflowTypes } from "@medusajs/types" import { FeatureFlagUtils, - ShippingProfileUtils, kebabCase, + ShippingProfileUtils, } from "@medusajs/utils" import { WorkflowArguments } from "../../helper" @@ -114,23 +114,17 @@ export async function createProductsPrepareData({ } if (product.variants) { - const hasPrices = product.variants.some((variant) => { - return (variant.prices?.length ?? 0) > 0 - }) + const items = + productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] - if (hasPrices) { - const items = - productsHandleVariantsIndexPricesMap.get(product.handle!) ?? [] - - product.variants.forEach((variant, index) => { - items.push({ - index, - prices: variant.prices!, - }) + product.variants.forEach((variant, index) => { + items.push({ + index, + prices: variant.prices!, }) + }) - productsHandleVariantsIndexPricesMap.set(product.handle!, items) - } + productsHandleVariantsIndexPricesMap.set(product.handle!, items) } } diff --git a/packages/workflows/src/handlers/product/index.ts b/packages/workflows/src/handlers/product/index.ts index 697c0e0b83ac7..bfadce740dfa4 100644 --- a/packages/workflows/src/handlers/product/index.ts +++ b/packages/workflows/src/handlers/product/index.ts @@ -1,10 +1,13 @@ export * from "./attach-sales-channel-to-products" export * from "./attach-shipping-profile-to-products" +export * from "./create-product-variants" +export * from "./create-product-variants-prepare-data" export * from "./create-products" export * from "./create-products-prepare-data" export * from "./detach-sales-channel-from-products" export * from "./detach-shipping-profile-from-products" export * from "./list-products" +export * from "./remove-product-variants" export * from "./remove-products" export * from "./revert-update-products" export * from "./revert-variant-prices" diff --git a/packages/workflows/src/handlers/product/remove-product-variants.ts b/packages/workflows/src/handlers/product/remove-product-variants.ts new file mode 100644 index 0000000000000..d40edf7a4b732 --- /dev/null +++ b/packages/workflows/src/handlers/product/remove-product-variants.ts @@ -0,0 +1,26 @@ +import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" +import { IProductModuleService } from "@medusajs/types" +import { WorkflowArguments } from "../../helper" + +type HandlerInput = { productVariants: { id: string }[] } + +export async function removeProductVariants({ + container, + data, +}: WorkflowArguments): Promise { + if (!data.productVariants.length) { + return + } + + const productModuleService: IProductModuleService = container.resolve( + ModulesDefinition[Modules.PRODUCT].registrationName + ) + + await productModuleService.deleteVariants( + data.productVariants.map((p) => p.id) + ) +} + +removeProductVariants.aliases = { + productVariants: "productVariants", +} diff --git a/packages/workflows/src/handlers/product/update-product-variants.ts b/packages/workflows/src/handlers/product/update-product-variants.ts index 3f057c91f1b78..e91c6775a45ab 100644 --- a/packages/workflows/src/handlers/product/update-product-variants.ts +++ b/packages/workflows/src/handlers/product/update-product-variants.ts @@ -1,5 +1,5 @@ import { Modules, ModulesDefinition } from "@medusajs/modules-sdk" -import { ProductTypes } from "@medusajs/types" +import { ProductTypes, UpdateProductVariantOnlyDTO } from "@medusajs/types" import { WorkflowArguments } from "../../helper" type HandlerInput = { @@ -14,21 +14,22 @@ export async function updateProductVariants({ > { const { productVariantsMap } = data const productsVariants: ProductTypes.UpdateProductVariantDTO[] = [] - const updateProductsData: ProductTypes.UpdateProductDTO[] = [] + const updateVariantsData: ProductTypes.UpdateProductVariantOnlyDTO[] = [] const productModuleService: ProductTypes.IProductModuleService = container.resolve(ModulesDefinition[Modules.PRODUCT].registrationName) - for (const [productId, variantsData = []] of productVariantsMap) { - updateProductsData.push({ - id: productId, - variants: variantsData, - }) + for (const [product_id, variantsUpdateData = []] of productVariantsMap) { + updateVariantsData.push( + ...(variantsUpdateData as unknown as UpdateProductVariantOnlyDTO[]).map( + (update) => ({ ...update, product_id }) + ) + ) - productsVariants.push(...variantsData) + productsVariants.push(...variantsUpdateData) } - if (updateProductsData.length) { - await productModuleService.update(updateProductsData) + if (updateVariantsData.length) { + await productModuleService.updateVariants(updateVariantsData) } return productsVariants diff --git a/packages/workflows/src/handlers/product/upsert-variant-prices.ts b/packages/workflows/src/handlers/product/upsert-variant-prices.ts index abe7de74d502d..441ed9d3e1509 100644 --- a/packages/workflows/src/handlers/product/upsert-variant-prices.ts +++ b/packages/workflows/src/handlers/product/upsert-variant-prices.ts @@ -23,11 +23,9 @@ type HandlerInput = { export async function upsertVariantPrices({ container, - context, data, }: WorkflowArguments) { const { variantPricesMap } = data - const featureFlagRouter = container.resolve("featureFlagRouter") if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { @@ -80,29 +78,34 @@ export async function upsertVariantPrices({ ) for (const price of prices) { + const region = price.region_id && regionsMap.get(price.region_id) + let region_currency_code: string | undefined + let region_rules: Record | undefined + + if (region) { + region_currency_code = region.currency_code + region_rules = { + region_id: region.id, + } + } + if (price.id) { - moneyAmountsToUpdate.push({ + const priceToUpdate = { id: price.id, min_quantity: price.min_quantity, max_quantity: price.max_quantity, amount: price.amount, - currency_code: price.currency_code, - }) + currency_code: region_currency_code ?? price.currency_code, + } + + moneyAmountsToUpdate.push(priceToUpdate) } else { - const region = price.region_id && regionsMap.get(price.region_id) const variantPrice: PricingTypes.CreatePricesDTO = { min_quantity: price.min_quantity, max_quantity: price.max_quantity, amount: price.amount, - currency_code: price.currency_code, - rules: {}, - } - - if (region) { - variantPrice.currency_code = region.currency_code - variantPrice.rules = { - region_id: region.id, - } + currency_code: region_currency_code ?? price.currency_code, + rules: region_rules ?? {}, } delete price.region_id @@ -129,16 +132,16 @@ export async function upsertVariantPrices({ priceSetId = createdPriceSet?.id createdPriceSets.push(createdPriceSet) - } - linksToCreate.push({ - productService: { - variant_id: variantId, - }, - pricingService: { - price_set_id: priceSetId, - }, - }) + linksToCreate.push({ + productService: { + variant_id: variantId, + }, + pricingService: { + price_set_id: priceSetId, + }, + }) + } } const createdLinks = await remoteLink.create(linksToCreate) diff --git a/packages/workflows/src/helper/workflow-export.ts b/packages/workflows/src/helper/workflow-export.ts index 3b2fc8dcec70b..ed75b21d30eae 100644 --- a/packages/workflows/src/helper/workflow-export.ts +++ b/packages/workflows/src/helper/workflow-export.ts @@ -11,6 +11,7 @@ import { MedusaModule } from "@medusajs/modules-sdk" import { EOL } from "os" import { ulid } from "ulid" import { Workflows } from "../definitions" +import { SymbolWorkflowWorkflowData } from "../utils/composer" export type FlowRunOptions = { input?: TData @@ -26,7 +27,7 @@ export type WorkflowResult = { } export const exportWorkflow = ( - workflowId: Workflows, + workflowId: Workflows | string, defaultResult?: string, dataPreparation?: (data: TData) => Promise ) => { @@ -63,7 +64,7 @@ export const exportWorkflow = ( if (typeof dataPreparation === "function") { try { - const copyInput = JSON.parse(JSON.stringify(input)) + const copyInput = input ? JSON.parse(JSON.stringify(input)) : input input = await dataPreparation(copyInput as TData) } catch (err) { if (throwOnError) { @@ -97,11 +98,13 @@ export const exportWorkflow = ( if (resultFrom) { if (Array.isArray(resultFrom)) { - result = resultFrom.map( - (from) => transaction.getContext().invoke?.[from] - ) + result = resultFrom.map((from) => { + const res = transaction.getContext().invoke?.[from] + return res?.__type === SymbolWorkflowWorkflowData ? res.output : res + }) } else { - result = transaction.getContext().invoke?.[resultFrom] + const res = transaction.getContext().invoke?.[resultFrom] + result = res?.__type === SymbolWorkflowWorkflowData ? res.output : res } } diff --git a/packages/workflows/src/index.ts b/packages/workflows/src/index.ts index 2816d42f49232..e60ea511b25cb 100644 --- a/packages/workflows/src/index.ts +++ b/packages/workflows/src/index.ts @@ -2,3 +2,5 @@ export * from "./definition" export * from "./definitions" export * as Handlers from "./handlers" export * from "./helper" +export * from "./utils/composer" +export * as Composer from "./utils/composer" diff --git a/packages/workflows/src/utils/composer/create-step.ts b/packages/workflows/src/utils/composer/create-step.ts new file mode 100644 index 0000000000000..e647ab60d4eb8 --- /dev/null +++ b/packages/workflows/src/utils/composer/create-step.ts @@ -0,0 +1,295 @@ +import { + resolveValue, + StepResponse, + SymbolMedusaWorkflowComposerContext, + SymbolWorkflowStep, + SymbolWorkflowStepBind, + SymbolWorkflowStepResponse, + SymbolWorkflowWorkflowData, +} from "./helpers" +import { + CreateWorkflowComposerContext, + StepExecutionContext, + StepFunction, + StepFunctionResult, + WorkflowData, +} from "./type" +import { proxify } from "./helpers/proxy" + +/** + * The type of invocation function passed to a step. + * + * @typeParam TInput - The type of the input that the function expects. + * @typeParam TOutput - The type of the output that the function returns. + * @typeParam TCompensateInput - The type of the input that the compensation function expects. + * + * @returns The expected output based on the type parameter `TOutput`. + */ +type InvokeFn = ( + /** + * The input of the step. + */ + input: { + [Key in keyof TInput]: TInput[Key] + }, + /** + * The step's context. + */ + context: StepExecutionContext +) => + | void + | StepResponse< + TOutput, + TCompensateInput extends undefined ? TOutput : TCompensateInput + > + | Promise> + +/** + * The type of compensation function passed to a step. + * + * @typeParam T - + * The type of the argument passed to the compensation function. If not specified, then it will be the same type as the invocation function's output. + * + * @returns There's no expected type to be returned by the compensation function. + */ +type CompensateFn = ( + /** + * The argument passed to the compensation function. + */ + input: T | undefined, + /** + * The step's context. + */ + context: StepExecutionContext +) => unknown | Promise + +interface ApplyStepOptions< + TStepInputs extends { + [K in keyof TInvokeInput]: WorkflowData + }, + TInvokeInput extends object, + TInvokeResultOutput, + TInvokeResultCompensateInput +> { + stepName: string + input: TStepInputs + invokeFn: InvokeFn< + TInvokeInput, + TInvokeResultOutput, + TInvokeResultCompensateInput + > + compensateFn?: CompensateFn +} + +/** + * @internal + * + * Internal function to create the invoke and compensate handler for a step. + * This is where the inputs and context are passed to the underlying invoke and compensate function. + * + * @param stepName + * @param input + * @param invokeFn + * @param compensateFn + */ +function applyStep< + TInvokeInput extends object, + TStepInput extends { + [K in keyof TInvokeInput]: WorkflowData + }, + TInvokeResultOutput, + TInvokeResultCompensateInput +>({ + stepName, + input, + invokeFn, + compensateFn, +}: ApplyStepOptions< + TStepInput, + TInvokeInput, + TInvokeResultOutput, + TInvokeResultCompensateInput +>): StepFunctionResult { + return function (this: CreateWorkflowComposerContext) { + if (!this.workflowId) { + throw new Error( + "createStep must be used inside a createWorkflow definition" + ) + } + + const handler = { + invoke: async (transactionContext) => { + const executionContext: StepExecutionContext = { + container: transactionContext.container, + metadata: transactionContext.metadata, + context: transactionContext.context, + } + + const argInput = await resolveValue(input, transactionContext) + const stepResponse: StepResponse = await invokeFn.apply( + this, + [argInput, executionContext] + ) + + const stepResponseJSON = + stepResponse?.__type === SymbolWorkflowStepResponse + ? stepResponse.toJSON() + : stepResponse + + return { + __type: SymbolWorkflowWorkflowData, + output: stepResponseJSON, + } + }, + compensate: compensateFn + ? async (transactionContext) => { + const executionContext: StepExecutionContext = { + container: transactionContext.container, + metadata: transactionContext.metadata, + context: transactionContext.context, + } + + const stepOutput = transactionContext.invoke[stepName]?.output + const invokeResult = + stepOutput?.__type === SymbolWorkflowStepResponse + ? stepOutput.compensateInput && + JSON.parse(JSON.stringify(stepOutput.compensateInput)) + : stepOutput && JSON.parse(JSON.stringify(stepOutput)) + + const args = [invokeResult, executionContext] + const output = await compensateFn.apply(this, args) + return { + output, + } + } + : undefined, + } + + this.flow.addAction(stepName, { + noCompensation: !compensateFn, + }) + this.handlers.set(stepName, handler) + + const ret = { + __type: SymbolWorkflowStep, + __step__: stepName, + } + + return proxify(ret) + } +} + +/** + * This function creates a {@link StepFunction} that can be used as a step in a workflow constructed by the {@link createWorkflow} function. + * + * @typeParam TInvokeInput - The type of the expected input parameter to the invocation function. + * @typeParam TInvokeResultOutput - The type of the expected output parameter of the invocation function. + * @typeParam TInvokeResultCompensateInput - The type of the expected input parameter to the compensation function. + * + * @returns A step function to be used in a workflow. + * + * @example + * import { + * createStep, + * StepResponse, + * StepExecutionContext, + * WorkflowData + * } from "@medusajs/workflows" + * + * interface CreateProductInput { + * title: string + * } + * + * export const createProductStep = createStep( + * "createProductStep", + * async function ( + * input: CreateProductInput, + * context + * ) { + * const productService = context.container.resolve( + * "productService" + * ) + * const product = await productService.create(input) + * return new StepResponse({ + * product + * }, { + * product_id: product.id + * }) + * }, + * async function ( + * input, + * context + * ) { + * const productService = context.container.resolve( + * "productService" + * ) + * await productService.delete(input.product_id) + * } + * ) + */ +export function createStep< + TInvokeInput extends object, + TInvokeResultOutput, + TInvokeResultCompensateInput +>( + /** + * The name of the step. + */ + name: string, + /** + * An invocation function that will be executed when the workflow is executed. The function must return an instance of {@link StepResponse}. The constructor of {@link StepResponse} + * accepts the output of the step as a first argument, and optionally as a second argument the data to be passed to the compensation function as a parameter. + */ + invokeFn: InvokeFn< + TInvokeInput, + TInvokeResultOutput, + TInvokeResultCompensateInput + >, + /** + * A compensation function that's executed if an error occurs in the workflow. It's used to roll-back actions when errors occur. + * It accepts as a parameter the second argument passed to the constructor of the {@link StepResponse} instance returned by the invocation function. If the + * invocation function doesn't pass the second argument to `StepResponse` constructor, the compensation function receives the first argument + * passed to the `StepResponse` constructor instead. + */ + compensateFn?: CompensateFn +): StepFunction { + const stepName = name ?? invokeFn.name + + const returnFn = function (input: { + [K in keyof TInvokeInput]: WorkflowData + }): WorkflowData { + if (!global[SymbolMedusaWorkflowComposerContext]) { + throw new Error( + "createStep must be used inside a createWorkflow definition" + ) + } + + const stepBinder = ( + global[ + SymbolMedusaWorkflowComposerContext + ] as CreateWorkflowComposerContext + ).stepBinder + + return stepBinder( + applyStep< + TInvokeInput, + { [K in keyof TInvokeInput]: WorkflowData }, + TInvokeResultOutput, + TInvokeResultCompensateInput + >({ + stepName, + input, + invokeFn, + compensateFn, + }) + ) + } + + returnFn.__type = SymbolWorkflowStepBind + returnFn.__step__ = stepName + + return returnFn as unknown as StepFunction +} diff --git a/packages/workflows/src/utils/composer/create-workflow.ts b/packages/workflows/src/utils/composer/create-workflow.ts new file mode 100644 index 0000000000000..a2c01e3cdc027 --- /dev/null +++ b/packages/workflows/src/utils/composer/create-workflow.ts @@ -0,0 +1,263 @@ +import { + LocalWorkflow, + WorkflowHandler, + WorkflowManager, +} from "@medusajs/orchestration" +import { LoadedModule, MedusaContainer } from "@medusajs/types" +import { exportWorkflow, FlowRunOptions, WorkflowResult } from "../../helper" +import { + CreateWorkflowComposerContext, + WorkflowData, + WorkflowDataProperties, +} from "./type" +import { + resolveValue, + SymbolInputReference, + SymbolMedusaWorkflowComposerContext, + SymbolWorkflowStep, +} from "./helpers" +import { proxify } from "./helpers/proxy" + +global[SymbolMedusaWorkflowComposerContext] = null + +/** + * An exported workflow, which is the type of a workflow constructed by the {@link createWorkflow} function. The exported workflow can be invoked to create + * an executable workflow, optionally within a specified container. So, to execute the workflow, you must invoke the exported workflow, then run the + * `run` method of the exported workflow. + * + * @example + * To execute a workflow: + * + * ```ts + * myWorkflow() + * .run({ + * input: { + * name: "John" + * } + * }) + * .then(({ result }) => { + * console.log(result) + * }) + * ``` + * + * To specify the container of the workflow, you can pass it as an argument to the call of the exported workflow. This is necessary when executing the workflow + * within a Medusa resource such as an API Route or a Subscriber. + * + * For example: + * + * ```ts + * import type { + * MedusaRequest, + * MedusaResponse + * } from "@medusajs/medusa"; + * import myWorkflow from "../../../workflows/hello-world"; + * + * export async function GET( + * req: MedusaRequest, + * res: MedusaResponse + * ) { + * const { result } = await myWorkflow(req.scope) + * .run({ + * input: { + * name: req.query.name as string + * } + * }) + * + * res.send(result) + * } + * ``` + */ +type ReturnWorkflow> = { + ( + container?: LoadedModule[] | MedusaContainer + ): Omit & { + run: ( + args?: FlowRunOptions< + TDataOverride extends undefined ? TData : TDataOverride + > + ) => Promise< + WorkflowResult< + TResultOverride extends undefined ? TResult : TResultOverride + > + > + } +} & THooks + +/** + * This function creates a workflow with the provided name and a constructor function. + * The constructor function builds the workflow from steps created by the {@link createStep} function. + * The returned workflow is an exported workflow of type {@link ReturnWorkflow}, meaning it's not executed right away. To execute it, + * invoke the exported workflow, then run its `run` method. + * + * @typeParam TData - The type of the input passed to the composer function. + * @typeParam TResult - The type of the output returned by the composer function. + * @typeParam THooks - The type of hooks defined in the workflow. + * + * @returns The created workflow. You can later execute the workflow by invoking it, then using its `run` method. + * + * @example + * import { createWorkflow } from "@medusajs/workflows" + * import { MedusaRequest, MedusaResponse, Product } from "@medusajs/medusa" + * import { + * createProductStep, + * getProductStep, + * createPricesStep + * } from "./steps" + * + * interface WorkflowInput { + * title: string + * } + * + * const myWorkflow = createWorkflow< + * WorkflowInput, + * Product + * >("my-workflow", (input) => { + * // Everything here will be executed and resolved later + * // during the execution. Including the data access. + * + * const product = createProductStep(input) + * const prices = createPricesStep(product) + * return getProductStep(product.id) + * } + * ) + * + * export async function GET( + * req: MedusaRequest, + * res: MedusaResponse + * ) { + * const { result: product } = await myWorkflow(req.scope) + * .run({ + * input: { + * title: "Shirt" + * } + * }) + * + * res.json({ + * product + * }) + * } + */ + +export function createWorkflow< + TData, + TResult, + THooks extends Record = Record +>( + /** + * The name of the workflow. + */ + name: string, + /** + * The constructor function that is executed when the `run` method in {@link ReturnWorkflow} is used. + * The function can't be an arrow function or an asynchronus function. It also can't directly manipulate data. + * You'll have to use the {@link transform} function if you need to directly manipulate data. + */ + composer: (input: WorkflowData) => + | void + | WorkflowData + | { + [K in keyof TResult]: + | WorkflowData + | WorkflowDataProperties + } +): ReturnWorkflow { + const handlers: WorkflowHandler = new Map() + + if (WorkflowManager.getWorkflow(name)) { + WorkflowManager.unregister(name) + } + + WorkflowManager.register(name, undefined, handlers) + + const context: CreateWorkflowComposerContext = { + workflowId: name, + flow: WorkflowManager.getTransactionDefinition(name), + handlers, + hooks_: [], + hooksCallback_: {}, + hookBinder: (name, fn) => { + context.hooks_.push(name) + return fn(context) + }, + stepBinder: (fn) => { + return fn.bind(context)() + }, + parallelizeBinder: (fn) => { + return fn.bind(context)() + }, + } + + global[SymbolMedusaWorkflowComposerContext] = context + + const inputPlaceHolder = proxify({ + __type: SymbolInputReference, + __step__: "", + }) + + const returnedStep = composer.apply(context, [inputPlaceHolder]) + + delete global[SymbolMedusaWorkflowComposerContext] + + WorkflowManager.update(name, context.flow, handlers) + + const workflow = exportWorkflow(name) + + const mainFlow = ( + container?: LoadedModule[] | MedusaContainer + ) => { + const workflow_ = workflow(container) + const originalRun = workflow_.run + + workflow_.run = (async ( + args?: FlowRunOptions< + TDataOverride extends undefined ? TData : TDataOverride + > + ): Promise< + WorkflowResult< + TResultOverride extends undefined ? TResult : TResultOverride + > + > => { + args ??= {} + args.resultFrom ??= + returnedStep?.__type === SymbolWorkflowStep + ? returnedStep.__step__ + : undefined + + // Forwards the input to the ref object on composer.apply + const workflowResult = (await originalRun( + args + )) as unknown as WorkflowResult< + TResultOverride extends undefined ? TResult : TResultOverride + > + + workflowResult.result = await resolveValue( + workflowResult.result || returnedStep, + workflowResult.transaction.getContext() + ) + + return workflowResult + }) as any + + return workflow_ + } + + let shouldRegisterHookHandler = true + + for (const hook of context.hooks_) { + mainFlow[hook] = (fn) => { + context.hooksCallback_[hook] ??= [] + + if (!shouldRegisterHookHandler) { + console.warn( + `A hook handler has already been registered for the ${hook} hook. The current handler registration will be skipped.` + ) + return + } + + context.hooksCallback_[hook].push(fn) + shouldRegisterHookHandler = false + } + } + + return mainFlow as ReturnWorkflow +} diff --git a/packages/workflows/src/utils/composer/helpers/index.ts b/packages/workflows/src/utils/composer/helpers/index.ts new file mode 100644 index 0000000000000..2636ee016ffcc --- /dev/null +++ b/packages/workflows/src/utils/composer/helpers/index.ts @@ -0,0 +1,3 @@ +export * from "./step-response" +export * from "./symbol" +export * from "./resolve-value" \ No newline at end of file diff --git a/packages/workflows/src/utils/composer/helpers/proxy.ts b/packages/workflows/src/utils/composer/helpers/proxy.ts new file mode 100644 index 0000000000000..68ca47b2f3359 --- /dev/null +++ b/packages/workflows/src/utils/composer/helpers/proxy.ts @@ -0,0 +1,28 @@ +import { transform } from "../transform" +import { WorkflowData, WorkflowTransactionContext } from "../type" +import { SymbolInputReference, SymbolWorkflowStepTransformer } from "./symbol" +import { resolveValue } from "./resolve-value" + +export function proxify(obj: WorkflowData): T { + return new Proxy(obj, { + get(target: any, prop: string | symbol): any { + if (prop in target) { + return target[prop] + } + + return transform(target[prop], async function (input, context) { + const { invoke } = context as WorkflowTransactionContext + let output = + target.__type === SymbolInputReference || + target.__type === SymbolWorkflowStepTransformer + ? target + : invoke?.[obj.__step__]?.output + + output = await resolveValue(output, context) + output = output?.[prop] + + return output && JSON.parse(JSON.stringify(output)) + }) + }, + }) as unknown as T +} diff --git a/packages/workflows/src/utils/composer/helpers/resolve-value.ts b/packages/workflows/src/utils/composer/helpers/resolve-value.ts new file mode 100644 index 0000000000000..c5fb131771b81 --- /dev/null +++ b/packages/workflows/src/utils/composer/helpers/resolve-value.ts @@ -0,0 +1,74 @@ +import { promiseAll } from "@medusajs/utils" +import { + SymbolInputReference, + SymbolWorkflowHook, + SymbolWorkflowStep, + SymbolWorkflowStepResponse, + SymbolWorkflowStepTransformer, +} from "./symbol" + +async function resolveProperty(property, transactionContext) { + const { invoke: invokeRes } = transactionContext + + if (property?.__type === SymbolInputReference) { + return transactionContext.payload + } else if (property?.__type === SymbolWorkflowStepTransformer) { + return await property.__resolver(transactionContext) + } else if (property?.__type === SymbolWorkflowHook) { + return await property.__value(transactionContext) + } else if (property?.__type === SymbolWorkflowStep) { + const output = invokeRes[property.__step__]?.output + if (output?.__type === SymbolWorkflowStepResponse) { + return output.output + } + + return output + } else if (property?.__type === SymbolWorkflowStepResponse) { + return property.output + } else { + return property + } +} + +/** + * @internal + */ +export async function resolveValue(input, transactionContext) { + const unwrapInput = async ( + inputTOUnwrap: Record, + parentRef: any + ) => { + if (inputTOUnwrap == null) { + return inputTOUnwrap + } + + if (Array.isArray(inputTOUnwrap)) { + return await promiseAll( + inputTOUnwrap.map((i) => unwrapInput(i, transactionContext)) + ) + } + + if (typeof inputTOUnwrap !== "object") { + return inputTOUnwrap + } + + for (const key of Object.keys(inputTOUnwrap)) { + parentRef[key] = await resolveProperty( + inputTOUnwrap[key], + transactionContext + ) + + if (typeof parentRef[key] === "object") { + await unwrapInput(parentRef[key], parentRef[key]) + } + } + + return parentRef + } + + const result = input?.__type + ? await resolveProperty(input, transactionContext) + : await unwrapInput(input, {}) + + return result && JSON.parse(JSON.stringify(result)) +} diff --git a/packages/workflows/src/utils/composer/helpers/step-response.ts b/packages/workflows/src/utils/composer/helpers/step-response.ts new file mode 100644 index 0000000000000..a0ccaff81e337 --- /dev/null +++ b/packages/workflows/src/utils/composer/helpers/step-response.ts @@ -0,0 +1,69 @@ +import { SymbolWorkflowStepResponse } from "./symbol" + +/** + * This class is used to create the response returned by a step. A step return its data by returning an instance of `StepResponse`. + * + * @typeParam TOutput - The type of the output of the step. + * @typeParam TCompensateInput - + * The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same + * as that of `TOutput`. + */ +export class StepResponse { + readonly #__type = SymbolWorkflowStepResponse + readonly #output: TOutput + readonly #compensateInput?: TCompensateInput + + /** + * The constructor of the StepResponse + * + * @typeParam TOutput - The type of the output of the step. + * @typeParam TCompensateInput - + * The type of the compensation input. If the step doesn't specify any compensation input, then the type of `TCompensateInput` is the same + * as that of `TOutput`. + */ + constructor( + /** + * The output of the step. + */ + output: TOutput, + /** + * The input to be passed as a parameter to the step's compensation function. If not provided, the `output` will be provided instead. + */ + compensateInput?: TCompensateInput + ) { + this.#output = output + this.#compensateInput = (compensateInput ?? output) as TCompensateInput + } + + /** + * @internal + */ + get __type() { + return this.#__type + } + + /** + * @internal + */ + get output(): TOutput { + return this.#output + } + + /** + * @internal + */ + get compensateInput(): TCompensateInput { + return this.#compensateInput as TCompensateInput + } + + /** + * @internal + */ + toJSON() { + return { + __type: this.#__type, + output: this.#output, + compensateInput: this.#compensateInput, + } + } +} diff --git a/packages/workflows/src/utils/composer/helpers/symbol.ts b/packages/workflows/src/utils/composer/helpers/symbol.ts new file mode 100644 index 0000000000000..8ec8177d7f28d --- /dev/null +++ b/packages/workflows/src/utils/composer/helpers/symbol.ts @@ -0,0 +1,12 @@ +export const SymbolMedusaWorkflowComposerContext = Symbol.for( + "MedusaWorkflowComposerContext" +) +export const SymbolInputReference = Symbol.for("WorkflowInputReference") +export const SymbolWorkflowStep = Symbol.for("WorkflowStep") +export const SymbolWorkflowHook = Symbol.for("WorkflowHook") +export const SymbolWorkflowWorkflowData = Symbol.for("WorkflowWorkflowData") +export const SymbolWorkflowStepResponse = Symbol.for("WorkflowStepResponse") +export const SymbolWorkflowStepBind = Symbol.for("WorkflowStepBind") +export const SymbolWorkflowStepTransformer = Symbol.for( + "WorkflowStepTransformer" +) diff --git a/packages/workflows/src/utils/composer/hook.ts b/packages/workflows/src/utils/composer/hook.ts new file mode 100644 index 0000000000000..8fa91c8028a8c --- /dev/null +++ b/packages/workflows/src/utils/composer/hook.ts @@ -0,0 +1,136 @@ +import { + resolveValue, + SymbolMedusaWorkflowComposerContext, + SymbolWorkflowHook, +} from "./helpers" +import { + CreateWorkflowComposerContext, + StepExecutionContext, + WorkflowData, +} from "./type" + +/** + * + * @ignore + * + * This function allows you to add hooks in your workflow that provide access to some data. Then, consumers of that workflow can add a handler function that performs + * an action with the provided data or modify it. + * + * For example, in a "create product" workflow, you may add a hook after the product is created, providing access to the created product. + * Then, developers using that workflow can hook into that point to access the product, modify its attributes, then return the updated product. + * + * @typeParam TOutput - The expected output of the hook's handler function. + * @returns The output of handler functions of this hook. If there are no handler functions, the output is `undefined`. + * + * @example + * import { + * createWorkflow, + * StepExecutionContext, + * hook, + * transform + * } from "@medusajs/workflows" + * import { + * createProductStep, + * getProductStep, + * createPricesStep + * } from "./steps" + * import { + * MedusaRequest, + * MedusaResponse, + * Product, ProductService + * } from "@medusajs/medusa" + * + * interface WorkflowInput { + * title: string + * } + * + * const myWorkflow = createWorkflow< + * WorkflowInput, + * Product + * >("my-workflow", + * function (input) { + * const product = createProductStep(input) + * + * const hookProduct = hook("createdProductHook", product) + * + * const newProduct = transform({ + * product, + * hookProduct + * }, (input) => { + * return input.hookProduct || input.product + * }) + * + * const prices = createPricesStep(newProduct) + * + * return getProductStep(product.id) + * } + * ) + * + * myWorkflow.createdProductHook( + * async (product, context: StepExecutionContext) => { + * const productService: ProductService = context.container.resolve("productService") + * + * const updatedProduct = await productService.update(product.id, { + * description: "a cool shirt" + * }) + * + * return updatedProduct + * }) + * + * export async function POST( + * req: MedusaRequest, + * res: MedusaResponse + * ) { + * const { result: product } = await myWorkflow(req.scope) + * .run({ + * input: { + * title: req.body.title + * } + * }) + * + * res.json({ + * product + * }) + * } + */ +export function hook( + /** + * The name of the hook. This will be used by the consumer to add a handler method for the hook. + */ + name: string, + /** + * The data that a handler function receives as a parameter. + */ + value: any +): WorkflowData { + const hookBinder = ( + global[SymbolMedusaWorkflowComposerContext] as CreateWorkflowComposerContext + ).hookBinder + + return hookBinder(name, function (context) { + return { + __value: async function (transactionContext) { + const executionContext: StepExecutionContext = { + container: transactionContext.container, + metadata: transactionContext.metadata, + context: transactionContext.context, + } + + const allValues = await resolveValue(value, transactionContext) + const stepValue = allValues + ? JSON.parse(JSON.stringify(allValues)) + : allValues + + let finalResult + const functions = context.hooksCallback_[name] + for (let i = 0; i < functions.length; i++) { + const fn = functions[i] + const arg = i === 0 ? stepValue : finalResult + finalResult = await fn.apply(fn, [arg, executionContext]) + } + return finalResult + }, + __type: SymbolWorkflowHook, + } + }) +} diff --git a/packages/workflows/src/utils/composer/index.ts b/packages/workflows/src/utils/composer/index.ts new file mode 100644 index 0000000000000..485d8451cc6f4 --- /dev/null +++ b/packages/workflows/src/utils/composer/index.ts @@ -0,0 +1,9 @@ +export * from "./create-step" +export * from "./create-workflow" +export * from "./hook" +export * from "./parallelize" +export * from "./helpers/resolve-value" +export * from "./helpers/symbol" +export * from "./helpers/step-response" +export * from "./transform" +export * from "./type" diff --git a/packages/workflows/src/utils/composer/parallelize.ts b/packages/workflows/src/utils/composer/parallelize.ts new file mode 100644 index 0000000000000..fdc8ed50b399a --- /dev/null +++ b/packages/workflows/src/utils/composer/parallelize.ts @@ -0,0 +1,70 @@ +import { CreateWorkflowComposerContext, WorkflowData } from "./type" +import { SymbolMedusaWorkflowComposerContext } from "./helpers" + +/** + * This function is used to run multiple steps in parallel. The result of each step will be returned as part of the result array. + * + * @typeParam TResult - The type of the expected result. + * + * @returns The step results. The results are ordered in the array by the order they're passed in the function's parameter. + * + * @example + * ```ts + * import { + * createWorkflow, + * parallelize + * } from "@medusajs/workflows" + * import { + * createProductStep, + * getProductStep, + * createPricesStep, + * attachProductToSalesChannelStep + * } from "./steps" + * + * interface WorkflowInput { + * title: string + * } + * + * const myWorkflow = createWorkflow< + * WorkflowInput, + * Product + * >("my-workflow", (input) => { + * const product = createProductStep(input) + * + * const [prices, productSalesChannel] = parallelize( + * createPricesStep(product), + * attachProductToSalesChannelStep(product) + * ) + * + * const id = product.id + * return getProductStep(product.id) + * } + * ) + */ +export function parallelize( + ...steps: TResult +): TResult { + if (!global[SymbolMedusaWorkflowComposerContext]) { + throw new Error( + "parallelize must be used inside a createWorkflow definition" + ) + } + + const parallelizeBinder = ( + global[SymbolMedusaWorkflowComposerContext] as CreateWorkflowComposerContext + ).parallelizeBinder + + const resultSteps = steps.map((step) => step) + + return parallelizeBinder(function ( + this: CreateWorkflowComposerContext + ) { + const stepOntoMerge = steps.shift()! + this.flow.mergeActions( + stepOntoMerge.__step__, + ...steps.map((step) => step.__step__) + ) + + return resultSteps as unknown as TResult + }) +} diff --git a/packages/workflows/src/utils/composer/transform.ts b/packages/workflows/src/utils/composer/transform.ts new file mode 100644 index 0000000000000..03a97d3c6ab68 --- /dev/null +++ b/packages/workflows/src/utils/composer/transform.ts @@ -0,0 +1,193 @@ +import { resolveValue, SymbolWorkflowStepTransformer } from "./helpers" +import { StepExecutionContext, WorkflowData } from "./type" +import { proxify } from "./helpers/proxy" + +type Func1 = ( + input: T extends WorkflowData + ? U + : T extends object + ? { [K in keyof T]: T[K] extends WorkflowData ? U : T[K] } + : {}, + context: StepExecutionContext +) => U | Promise + +type Func = (input: T, context: StepExecutionContext) => U | Promise + +/** + * + * This function transforms the output of other utility functions. + * + * For example, if you're using the value(s) of some step(s) as an input to a later step. As you can't directly manipulate data in the workflow constructor function passed to {@link createWorkflow}, + * the `transform` function provides access to the runtime value of the step(s) output so that you can manipulate them. + * + * Another example is if you're using the runtime value of some step(s) as the output of a workflow. + * + * If you're also retrieving the output of a hook and want to check if its value is set, you must use a workflow to get the runtime value of that hook. + * + * @returns There's no expected value to be returned by the `transform` function. + * + * @example + * import { + * createWorkflow, + * transform + * } from "@medusajs/workflows" + * import { step1, step2 } from "./steps" + * + * type WorkflowInput = { + * name: string + * } + * + * type WorkflowOutput = { + * message: string + * } + * + * const myWorkflow = createWorkflow< + * WorkflowInput, + * WorkflowOutput + * > + * ("hello-world", (input) => { + * const str1 = step1(input) + * const str2 = step2(input) + * + * return transform({ + * str1, + * str2 + * }, (input) => ({ + * message: `${input.str1}${input.str2}` + * })) + * }) + */ +// prettier-ignore +// eslint-disable-next-line max-len +export function transform( + /** + * The output(s) of other step functions. + */ + values: T, + /** + * The transform function used to perform action on the runtime values of the provided `values`. + */ + ...func: + | [Func1] +): WorkflowData + +/** + * @internal + */ +// prettier-ignore +// eslint-disable-next-line max-len +export function transform( + values: T, + ...func: + | [Func1] + | [Func1, Func] +): WorkflowData + +/** + * @internal + */ +// prettier-ignore +// eslint-disable-next-line max-len +export function transform( + values: T, + ...func: + | [Func1] + | [Func1, Func] + | [Func1, Func, Func] +): WorkflowData + +/** + * @internal + */ +// prettier-ignore +// eslint-disable-next-line max-len +export function transform( + values: T, + ...func: + | [Func1] + | [Func1, Func] + | [Func1, Func, Func] + | [Func1, Func, Func, Func] +): WorkflowData + +/** + * @internal + */ +// prettier-ignore +// eslint-disable-next-line max-len +export function transform( + values: T, + ...func: + | [Func1] + | [Func1, Func] + | [Func1, Func, Func
- Configure the pricing for this variant. -