diff --git a/.changeset/healthy-ligers-learn.md b/.changeset/healthy-ligers-learn.md new file mode 100644 index 0000000000000..6f0a2a908194b --- /dev/null +++ b/.changeset/healthy-ligers-learn.md @@ -0,0 +1,6 @@ +--- +"@medusajs/ui-preset": patch +"@medusajs/ui": patch +--- + +feat(ui,ui-preset): Update to latest version of TailwindCSS. Increase spacing between columns in component. diff --git a/.eslintignore b/.eslintignore index f7f026ec94781..a2f3463632feb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,8 @@ packages/* !packages/medusa !packages/admin-ui !packages/admin +!packages/admin-next +!packages/admin-next/dashboard !packages/medusa-payment-stripe !packages/medusa-payment-paypal !packages/event-bus-redis diff --git a/.eslintrc.js b/.eslintrc.js index c34c47f13a79a..56edede0b3e47 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -72,7 +72,6 @@ module.exports = { node: true, jest: true, }, - ignorePatterns: ["packages/admin-next/dashboard/**/dist"], overrides: [ { files: ["*.ts"], @@ -86,6 +85,7 @@ module.exports = { "./packages/medusa-payment-paypal/tsconfig.spec.json", "./packages/admin-ui/tsconfig.json", "./packages/admin-ui/tsconfig.spec.json", + "./packages/admin-next/dashboard/tsconfig.json", "./packages/event-bus-local/tsconfig.spec.json", "./packages/event-bus-redis/tsconfig.spec.json", "./packages/medusa-plugin-meilisearch/tsconfig.spec.json", @@ -228,23 +228,52 @@ module.exports = { }, }, { - files: ["packages/admin-next/dashboard/src/**/*.{ts,tsx}"], - env: { browser: true, es2020: true, node: true }, + files: [ + "packages/admin-next/dashboard/**/*.ts", + "packages/admin-next/dashboard/**/*.tsx", + ], + plugins: ["unused-imports", "react-refresh"], extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "plugin:react/jsx-runtime", "plugin:react-hooks/recommended", ], parser: "@typescript-eslint/parser", parserOptions: { - project: "tsconfig.json", + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + project: "./packages/admin-next/dashboard/tsconfig.json", + }, + globals: { + __BASE__: "readonly", + }, + env: { + browser: true, }, - plugins: ["react-refresh"], rules: { + "prettier/prettier": "error", + "react/prop-types": "off", + "new-cap": "off", + "require-jsdoc": "off", + "valid-jsdoc": "off", "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], + "no-unused-expressions": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { + vars: "all", + varsIgnorePattern: "^_", + args: "after-used", + argsIgnorePattern: "^_", + }, + ], }, }, { diff --git a/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts index 6b986b0bc04b6..bae4ee4918c8d 100644 --- a/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts +++ b/integration-tests/plugins/__tests__/customer/store/create-customer.spec.ts @@ -1,11 +1,12 @@ +import { IAuthModuleService, ICustomerModuleService } from "@medusajs/types" +import { initDb, useDb } from "../../../../environment-helpers/use-db" + import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { ICustomerModuleService, IAuthModuleService } from "@medusajs/types" +import adminSeeder from "../../../../helpers/admin-seeder" +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" -import { getContainer } from "../../../../environment-helpers/use-container" -import { initDb, useDb } from "../../../../environment-helpers/use-db" -import adminSeeder from "../../../../helpers/admin-seeder" jest.setTimeout(50000) @@ -49,6 +50,7 @@ describe("POST /store/customers", () => { const authUser = await authService.createAuthUser({ entity_id: "store_user", provider_id: "test", + scope: "store", }) const jwt = await authService.generateJwtToken(authUser.id, "store") diff --git a/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts b/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts index abaf5abe24ca1..ca4753eac7b43 100644 --- a/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts +++ b/integration-tests/plugins/__tests__/customer/store/list-customer-addresses.ts @@ -9,6 +9,8 @@ import { createAuthenticatedCustomer } from "../../../helpers/create-authenticat const env = { MEDUSA_FF_MEDUSA_V2: true } +jest.setTimeout(100000) + describe("GET /store/customers/me/addresses", () => { let dbConnection let appContainer @@ -16,13 +18,17 @@ describe("GET /store/customers/me/addresses", () => { let customerModuleService: ICustomerModuleService beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) - dbConnection = await initDb({ cwd, env } as any) - shutdownServer = await startBootstrapApp({ cwd, env }) - appContainer = getContainer() - customerModuleService = appContainer.resolve( - ModuleRegistrationName.CUSTOMER - ) + try { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + customerModuleService = appContainer.resolve( + ModuleRegistrationName.CUSTOMER + ) + } catch (error) { + console.error(error) + } }) afterAll(async () => { 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 index 51e99ad6ad58d..3fd7165c48d5e 100644 --- a/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts +++ b/integration-tests/plugins/__tests__/product/admin/create-product-variant.spec.ts @@ -5,14 +5,13 @@ 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" +import { ProductModuleService } from "@medusajs/product" +import { PricingModuleService } from "@medusajs/pricing" jest.setTimeout(50000) diff --git a/integration-tests/plugins/__tests__/workflows/product/create-product.ts b/integration-tests/plugins/__tests__/workflows/product/create-product.ts index 5d218d108e7a8..11de759e379cc 100644 --- a/integration-tests/plugins/__tests__/workflows/product/create-product.ts +++ b/integration-tests/plugins/__tests__/workflows/product/create-product.ts @@ -11,7 +11,7 @@ import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app import { getContainer } from "../../../../environment-helpers/use-container" import { initDb, useDb } from "../../../../environment-helpers/use-db" -jest.setTimeout(30000) +jest.setTimeout(50000) describe("CreateProduct workflow", function () { let medusaContainer @@ -129,7 +129,7 @@ describe("CreateProduct workflow", function () { expect(product).toEqual( expect.objectContaining({ - deleted_at: expect.any(String), + deleted_at: expect.any(Date), }) ) }) diff --git a/integration-tests/plugins/helpers/create-authenticated-customer.ts b/integration-tests/plugins/helpers/create-authenticated-customer.ts index 54f19cf0973c6..3bb092d950d2a 100644 --- a/integration-tests/plugins/helpers/create-authenticated-customer.ts +++ b/integration-tests/plugins/helpers/create-authenticated-customer.ts @@ -13,6 +13,7 @@ export const createAuthenticatedCustomer = async ( const authUser = await authService.createAuthUser({ entity_id: "store_user", provider_id: "test", + scope: "store", app_metadata: { customer_id: customer.id }, }) diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index 537059c726d30..db37624a6a5f2 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -22,6 +22,7 @@ "@medusajs/icons": "workspace:^", "@medusajs/ui": "workspace:^", "@radix-ui/react-collapsible": "1.0.3", + "@radix-ui/react-hover-card": "^1.0.7", "@tanstack/react-query": "4.22.0", "@tanstack/react-table": "8.10.7", "@uiw/react-json-view": "2.0.0-alpha.10", @@ -43,13 +44,14 @@ "@medusajs/types": "workspace:^", "@medusajs/ui-preset": "workspace:^", "@medusajs/vite-plugin-extension": "workspace:^", + "@types/node": "^20.11.15", "@types/react": "18.2.43", "@types/react-dom": "18.2.17", "@vitejs/plugin-react": "4.2.1", "autoprefixer": "10.4.16", "postcss": "8.4.32", "prettier": "^3.1.1", - "tailwindcss": "3.3.6", + "tailwindcss": "^3.4.1", "typescript": "5.2.2", "vite": "5.0.10" }, diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 7e8bee990e87d..713a2c3355155 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -23,6 +23,7 @@ "pages": "pages", "next": "Next", "prev": "Prev", + "is": "is", "extensions": "Extensions", "settings": "Settings", "general": "General", @@ -35,6 +36,8 @@ "remove": "Remove", "admin": "Admin", "store": "Store", + "items_one": "{{count}} item", + "items_other": "{{count}} items", "countSelected": "{{count}} selected", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", @@ -107,7 +110,29 @@ "deleteCustomerGroupWarning": "You are about to delete the customer group {{name}}. This action cannot be undone." }, "orders": { - "domain": "Orders" + "domain": "Orders", + "paymentStatusLabel": "Payment Status", + "paymentStatus": { + "notPaid": "Not Paid", + "awaiting": "Awaiting", + "captured": "Captured", + "partiallyRefunded": "Partially Refunded", + "refunded": "Refunded", + "canceled": "Canceled", + "requresAction": "Requires Action" + }, + "fulfillmentStatusLabel": "Fulfillment Status", + "fulfillmentStatus": { + "notFulfilled": "Not Fulfilled", + "partiallyFulfilled": "Partially Fulfilled", + "fulfilled": "Fulfilled", + "partiallyShipped": "Partially Shipped", + "shipped": "Shipped", + "partiallyReturned": "Partially Returned", + "returned": "Returned", + "canceled": "Canceled", + "requresAction": "Requires Action" + } }, "draftOrders": { "domain": "Draft Orders" @@ -263,6 +288,14 @@ "total": "Total", "created": "Created", "key": "Key", + "customer": "Customer", + "date": "Date", + "order": "Order", + "fulfillment": "Fulfillment", + "payment": "Payment", + "items": "Items", + "salesChannel": "Sales Channel", + "region": "Region", "role": "Role", "sent": "Sent" } diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts deleted file mode 100644 index 20b29f6c8514a..0000000000000 --- a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./order-table-cells" diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx deleted file mode 100644 index 9026e455903a1..0000000000000 --- a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import type { Order } from "@medusajs/medusa" -import { StatusBadge } from "@medusajs/ui" -import { format } from "date-fns" -import { getPresentationalAmount } from "../../../lib/money-amount-helpers" - -export const OrderDisplayIdCell = ({ id }: { id: Order["display_id"] }) => { - return #{id} -} - -export const OrderDateCell = ({ - date, -}: { - date: Order["created_at"] | string -}) => { - const value = new Date(date) - - return {format(value, "dd MMM, yyyy")} -} - -export const OrderFulfillmentStatusCell = ({ - status, -}: { - status: Order["fulfillment_status"] -}) => { - switch (status) { - case "not_fulfilled": - return Not fulfilled - case "partially_fulfilled": - return Partially fulfilled - case "fulfilled": - return Fulfilled - case "partially_shipped": - return Partially shipped - case "shipped": - return Shipped - case "partially_returned": - return Partially returned - case "returned": - return Returned - case "canceled": - return Canceled - case "requires_action": - return Requires action - } -} - -export const OrderPaymentStatusCell = ({ - status, -}: { - status: Order["payment_status"] -}) => { - switch (status) { - case "not_paid": - return Not paid - case "awaiting": - return Awaiting - case "captured": - return Captured - case "partially_refunded": - return Partially refunded - case "refunded": - return Refunded - case "canceled": - return Canceled - case "requires_action": - return Requires action - } -} - -// TODO: Fix formatting amount with correct division eg. EUR 1000 -> EUR 10.00 -// Source currency info from `@medusajs/medusa` definition -export const OrderTotalCell = ({ - total, - currencyCode, -}: { - total: Order["total"] - currencyCode: Order["currency_code"] -}) => { - const formatted = new Intl.NumberFormat(undefined, { - style: "currency", - currency: currencyCode, - currencyDisplay: "narrowSymbol", - }).format(0) - - const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() - - const presentationAmount = getPresentationalAmount(total, currencyCode) - const formattedTotal = new Intl.NumberFormat(undefined, { - style: "decimal", - }).format(presentationAmount) - - return ( - - {symbol} {formattedTotal} {currencyCode.toUpperCase()} - - ) -} diff --git a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx index 31044999a7196..7150224afb904 100644 --- a/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx +++ b/packages/admin-next/dashboard/src/components/layout/shell/shell.tsx @@ -35,13 +35,13 @@ export const Shell = ({ children }: PropsWithChildren) => { {children}{children} -
+
-
+
-
+
) @@ -76,6 +76,7 @@ const Breadcrumbs = () => {
    {crumbs.map((crumb, index) => { const isLast = index === crumbs.length - 1 + const isSingle = crumbs.length === 1 return (
  1. { ) : (
    - ... - + {!isSingle && ...} + {crumb.label}
    diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx new file mode 100644 index 0000000000000..daacb414408fe --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/context.tsx @@ -0,0 +1,19 @@ +import { createContext, useContext } from "react" + +type DataTableFilterContextValue = { + removeFilter: (key: string) => void + removeAllFilters: () => void +} + +export const DataTableFilterContext = + createContext(null) + +export const useDataTableFilterContext = () => { + const ctx = useContext(DataTableFilterContext) + if (!ctx) { + throw new Error( + "useDataTableFacetedFilterContext must be used within a DataTableFacetedFilter" + ) + } + return ctx +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx new file mode 100644 index 0000000000000..c5ac5f6be9ae9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/data-table-filter.tsx @@ -0,0 +1,255 @@ +import { Button, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { useSearchParams } from "react-router-dom" + +import { DataTableFilterContext, useDataTableFilterContext } from "./context" +import { DateFilter } from "./date-filter" +import { SelectFilter } from "./select-filter" + +type Option = { + label: string + value: unknown +} + +export type Filter = { + key: string + label: string +} & ( + | { + type: "select" + options: Option[] + multiple?: boolean + searchable?: boolean + } + | { + type: "date" + options?: never + } +) + +type DataTableFilterProps = { + filters: Filter[] + prefix?: string +} + +export const DataTableFilter = ({ filters, prefix }: DataTableFilterProps) => { + const [searchParams] = useSearchParams() + const [open, setOpen] = useState(false) + + const [activeFilters, setActiveFilters] = useState( + getInitialFilters({ searchParams, filters, prefix }) + ) + + const availableFilters = filters.filter( + (f) => !activeFilters.find((af) => af.key === f.key) + ) + + /** + * If there are any filters in the URL that are not in the active filters, + * add them to the active filters. This ensures that we display the filters + * if a user navigates to a page with filters in the URL. + */ + const initialMount = useRef(true) + + useEffect(() => { + if (initialMount.current) { + const params = new URLSearchParams(searchParams) + + filters.forEach((filter) => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key + const value = params.get(key) + if (value && !activeFilters.find((af) => af.key === filter.key)) { + console.log("adding filter", filter.key, "to active filters") + if (filter.type === "select") { + setActiveFilters((prev) => [ + ...prev, + { + ...filter, + multiple: filter.multiple, + options: filter.options, + openOnMount: false, + }, + ]) + } else { + setActiveFilters((prev) => [ + ...prev, + { ...filter, openOnMount: false }, + ]) + } + } + }) + } + + initialMount.current = false + }, [activeFilters, filters, prefix, searchParams]) + + const addFilter = (filter: Filter) => { + setOpen(false) + setActiveFilters((prev) => [...prev, { ...filter, openOnMount: true }]) + } + + const removeFilter = useCallback((key: string) => { + setActiveFilters((prev) => prev.filter((f) => f.key !== key)) + }, []) + + const removeAllFilters = useCallback(() => { + setActiveFilters([]) + }, []) + + return ( + ({ + removeFilter, + removeAllFilters, + }), + [removeAllFilters, removeFilter] + )} + > +
    + {activeFilters.map((filter) => { + if (filter.type === "select") { + return ( + + ) + } + + return ( + + ) + })} + {availableFilters.length > 0 && ( + + + + + + { + const hasOpenFilter = activeFilters.find( + (filter) => filter.openOnMount + ) + + if (hasOpenFilter) { + e.preventDefault() + } + }} + > + {availableFilters.map((filter) => { + return ( +
    { + addFilter(filter) + }} + > + {filter.label} +
    + ) + })} +
    +
    +
    + )} + {activeFilters.length > 0 && ( + + )} +
    +
    + ) +} + +type ClearAllFiltersProps = { + filters: Filter[] + prefix?: string +} + +const ClearAllFilters = ({ filters, prefix }: ClearAllFiltersProps) => { + const { removeAllFilters } = useDataTableFilterContext() + const [_, setSearchParams] = useSearchParams() + + const handleRemoveAll = () => { + setSearchParams((prev) => { + const newValues = new URLSearchParams(prev) + + filters.forEach((filter) => { + newValues.delete(prefix ? `${prefix}_${filter.key}` : filter.key) + }) + + return newValues + }) + + removeAllFilters() + } + + return ( + + ) +} + +const getInitialFilters = ({ + searchParams, + filters, + prefix, +}: { + searchParams: URLSearchParams + filters: Filter[] + prefix?: string +}) => { + const params = new URLSearchParams(searchParams) + const activeFilters: (Filter & { openOnMount: boolean })[] = [] + + filters.forEach((filter) => { + const key = prefix ? `${prefix}_${filter.key}` : filter.key + const value = params.get(key) + if (value) { + if (filter.type === "select") { + activeFilters.push({ + ...filter, + multiple: filter.multiple, + options: filter.options, + openOnMount: false, + }) + } else { + activeFilters.push({ ...filter, openOnMount: false }) + } + } + }) + + return activeFilters +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx new file mode 100644 index 0000000000000..fd851b26b2b91 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/date-filter.tsx @@ -0,0 +1,322 @@ +import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons" +import { DatePicker, Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { format } from "date-fns" +import isEqual from "lodash/isEqual" +import { MouseEvent, useState } from "react" + +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +type DateFilterProps = IFilter + +type DateComparisonOperator = { + gte?: string + lte?: string + lt?: string + gt?: string +} + +export const DateFilter = ({ + filter, + prefix, + openOnMount, +}: DateFilterProps) => { + const [open, setOpen] = useState(openOnMount) + const [showCustom, setShowCustom] = useState(false) + const { key, label } = filter + const { removeFilter } = useDataTableFilterContext() + const selectedParams = useSelectedParams({ param: key, prefix }) + + const handleSelectPreset = (value: DateComparisonOperator) => { + selectedParams.add(JSON.stringify(value)) + setShowCustom(false) + } + + const handleSelectCustom = () => { + selectedParams.delete() + setShowCustom((prev) => !prev) + } + + const currentValue = selectedParams.get() + + const currentDateComparison = parseDateComparison(currentValue) + const customStartValue = getDateFromComparison(currentDateComparison, "gte") + const customEndValue = getDateFromComparison(currentDateComparison, "lte") + + const handleCustomDateChange = ( + value: Date | undefined, + pos: "start" | "end" + ) => { + const key = pos === "start" ? "gte" : "lte" + const dateValue = value ? value.toISOString() : undefined + + selectedParams.add( + JSON.stringify({ + ...(currentDateComparison || {}), + [key]: dateValue, + }) + ) + } + + const getDisplayValueFromPresets = () => { + const preset = presets.find((p) => isEqual(p.value, currentDateComparison)) + return preset?.label + } + + const formatCustomDate = (date: Date | undefined) => { + return date ? format(date, "dd MMM, yyyy") : undefined + } + + const getCustomDisplayValue = () => { + const formattedDates = [customStartValue, customEndValue].map( + formatCustomDate + ) + return formattedDates.filter(Boolean).join(" - ") + } + + const displayValue = getDisplayValueFromPresets() || getCustomDisplayValue() + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !currentValue.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + } + } + }} + > +
      + {presets.map((preset) => { + const isSelected = selectedParams + .get() + .includes(JSON.stringify(preset.value)) + return ( +
    • + +
    • + ) + })} +
    • + +
    • +
    + {showCustom && ( +
    +
    +
    + + Starting + +
    +
    + handleCustomDateChange(d, "start")} + /> +
    +
    +
    +
    + + Ending + +
    +
    + { + handleCustomDateChange(d, "end") + }} + /> +
    +
    +
    + )} +
    +
    +
    + ) +} + +type DateDisplayProps = { + label: string + value?: string + onRemove: () => void +} + +const DateDisplay = ({ label, value, onRemove }: DateDisplayProps) => { + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + onRemove() + } + + return ( + +
    +
    + + {label} + +
    + {value && ( +
    +
    + + {value} + +
    +
    + )} + {value && ( +
    + +
    + )} +
    +
    + ) +} + +const today = new Date() +today.setHours(0, 0, 0, 0) + +const presets: { label: string; value: DateComparisonOperator }[] = [ + { + label: "Today", + value: { + gte: today.toISOString(), + }, + }, + { + label: "Last 7 days", + value: { + gte: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago + }, + }, + { + label: "Last 30 days", + value: { + gte: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days ago + }, + }, + { + label: "Last 90 days", + value: { + gte: new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days ago + }, + }, + { + label: "Last 12 months", + value: { + gte: new Date(today.getTime() - 365 * 24 * 60 * 60 * 1000).toISOString(), // 365 days ago + }, + }, +] + +const parseDateComparison = (value: string[]) => { + return value?.length + ? (JSON.parse(value.join(",")) as DateComparisonOperator) + : null +} + +const getDateFromComparison = ( + comparison: DateComparisonOperator | null, + key: "gte" | "lte" +) => { + return comparison?.[key] ? new Date(comparison[key] as string) : undefined +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts new file mode 100644 index 0000000000000..8363bf9a5a049 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/index.ts @@ -0,0 +1 @@ +export * from "./data-table-filter" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx new file mode 100644 index 0000000000000..cf4a1d784f5d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/select-filter.tsx @@ -0,0 +1,261 @@ +import { CheckMini, EllipseMiniSolid, XMarkMini } from "@medusajs/icons" +import { Text, clx } from "@medusajs/ui" +import * as Popover from "@radix-ui/react-popover" +import { Command } from "cmdk" +import { MouseEvent, useState } from "react" +import { useTranslation } from "react-i18next" + +import { useSelectedParams } from "../hooks" +import { useDataTableFilterContext } from "./context" +import { IFilter } from "./types" + +interface SelectFilterProps extends IFilter { + options: { label: string; value: unknown }[] + multiple?: boolean + searchable?: boolean +} + +export const SelectFilter = ({ + filter, + prefix, + multiple, + searchable, + options, + openOnMount, +}: SelectFilterProps) => { + const [open, setOpen] = useState(openOnMount) + const [search, setSearch] = useState("") + const [searchRef, setSearchRef] = useState(null) + + const { t } = useTranslation() + const { removeFilter } = useDataTableFilterContext() + + const { key, label } = filter + const selectedParams = useSelectedParams({ param: key, prefix, multiple }) + const currentValue = selectedParams.get() + + const labelValues = currentValue + .map((v) => options.find((o) => o.value === v)?.label) + .filter(Boolean) as string[] + + const handleRemove = () => { + selectedParams.delete() + removeFilter(key) + } + + let timeoutId: ReturnType | null = null + + const handleOpenChange = (open: boolean) => { + setOpen(open) + + if (timeoutId) { + clearTimeout(timeoutId) + } + + if (!open && !currentValue.length) { + timeoutId = setTimeout(() => { + removeFilter(key) + }, 200) + } + } + + const handleClearSearch = () => { + setSearch("") + if (searchRef) { + searchRef.focus() + } + } + + const handleSelect = (value: unknown) => { + const isSelected = selectedParams.get().includes(String(value)) + + if (isSelected) { + selectedParams.delete(String(value)) + } else { + selectedParams.add(String(value)) + } + } + + return ( + + + + { + if (e.target instanceof HTMLElement) { + if ( + e.target.attributes.getNamedItem("data-name")?.value === + "filters_menu_content" + ) { + e.preventDefault() + e.stopPropagation() + } + } + }} + > + + {searchable && ( +
    +
    + +
    + +
    +
    +
    + )} + + + {t("general.noResultsTitle")} + + + + {options.map((option) => { + const isSelected = selectedParams + .get() + .includes(String(option.value)) + + return ( + { + handleSelect(option.value) + }} + > +
    + {multiple ? : } +
    + {option.label} +
    + ) + })} +
    +
    +
    +
    +
    + ) +} + +type SelectDisplayProps = { + label: string + value?: string | string[] + onRemove: () => void +} + +export const SelectDisplay = ({ + label, + value, + onRemove, +}: SelectDisplayProps) => { + const { t } = useTranslation() + const v = value ? (Array.isArray(value) ? value : [value]) : null + const count = v?.length || 0 + + const handleRemove = (e: MouseEvent) => { + e.stopPropagation() + onRemove() + } + + return ( + +
    +
    0, + } + )} + > + + {label} + +
    +
    + {count > 0 && ( +
    + + {t("general.is")} + +
    + )} + {count > 0 && ( +
    + + {v?.join(", ")} + +
    + )} +
    + {v && v.length > 0 && ( +
    + +
    + )} +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts new file mode 100644 index 0000000000000..d5dfb5e0a896a --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-filter/types.ts @@ -0,0 +1,8 @@ +export interface IFilter { + filter: { + key: string + label: string + } + openOnMount?: boolean + prefix?: string +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx new file mode 100644 index 0000000000000..43cec0d537f12 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/data-table-order-by.tsx @@ -0,0 +1,157 @@ +import { ArrowUpDown } from "@medusajs/icons" +import { DropdownMenu, IconButton } from "@medusajs/ui" +import { useState } from "react" +import { useTranslation } from "react-i18next" +import { useSearchParams } from "react-router-dom" + +type DataTableOrderByProps = { + keys: (keyof TData)[] + prefix?: string +} + +enum SortDirection { + ASC = "asc", + DESC = "desc", +} + +type SortState = { + key?: string + dir: SortDirection +} + +const initState = (params: URLSearchParams, prefix?: string): SortState => { + const param = prefix ? `${prefix}_order` : "order" + const sortParam = params.get(param) + + if (!sortParam) { + return { + dir: SortDirection.ASC, + } + } + + const dir = sortParam.startsWith("-") ? SortDirection.DESC : SortDirection.ASC + const key = sortParam.replace("-", "") + + return { + key, + dir, + } +} + +const formatKey = (key: string) => { + const words = key.split("_") + const formattedWords = words.map((word, index) => { + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1) + } else { + return word + } + }) + return formattedWords.join(" ") +} + +export const DataTableOrderBy = ({ + keys, + prefix, +}: DataTableOrderByProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const [state, setState] = useState<{ + key?: string + dir: SortDirection + }>(initState(searchParams, prefix)) + const param = prefix ? `${prefix}_order` : "order" + const { t } = useTranslation() + + const handleDirChange = (dir: string) => { + setState((prev) => ({ + ...prev, + dir: dir as SortDirection, + })) + updateOrderParam({ + key: state.key, + dir: dir as SortDirection, + }) + } + + const handleKeyChange = (value: string) => { + setState((prev) => ({ + ...prev, + key: value, + })) + + updateOrderParam({ + key: value, + dir: state.dir, + }) + } + + const updateOrderParam = (state: SortState) => { + if (!state.key) { + setSearchParams((prev) => { + prev.delete(param) + return prev + }) + + return + } + + const orderParam = + state.dir === SortDirection.ASC ? state.key : `-${state.key}` + setSearchParams((prev) => { + prev.set(param, orderParam) + return prev + }) + } + + return ( + + + + + + + + + {keys.map((key) => { + const stringKey = String(key) + + return ( + event.preventDefault()} + > + {formatKey(stringKey)} + + ) + })} + + + + event.preventDefault()} + > + {t("general.ascending")} + 1 - 30 + + event.preventDefault()} + > + {t("general.descending")} + 30 - 1 + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts new file mode 100644 index 0000000000000..6761c82fb8c51 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-order-by/index.ts @@ -0,0 +1 @@ +export * from "./data-table-order-by" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx new file mode 100644 index 0000000000000..60cfe1ba0ea0d --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/data-table-query.tsx @@ -0,0 +1,32 @@ +import { Filter } from ".." +import { DataTableFilter } from "../data-table-filter" +import { DataTableOrderBy } from "../data-table-order-by" +import { DataTableSearch } from "../data-table-search" + +export interface DataTableQueryProps { + search?: boolean + orderBy?: (string | number)[] + filters?: Filter[] + prefix?: string +} + +export const DataTableQuery = ({ + search, + orderBy, + filters, + prefix, +}: DataTableQueryProps) => { + return ( +
    +
    + {filters && filters.length > 0 && ( + + )} +
    +
    + {search && } + {orderBy && } +
    +
    + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts new file mode 100644 index 0000000000000..7449807df43c1 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-query/index.ts @@ -0,0 +1 @@ +export * from "./data-table-query" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx new file mode 100644 index 0000000000000..374cc0b2be178 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/data-table-root.tsx @@ -0,0 +1,272 @@ +import { CommandBar, Table, clx } from "@medusajs/ui" +import { + ColumnDef, + Table as ReactTable, + Row, + flexRender, +} from "@tanstack/react-table" +import { ComponentPropsWithoutRef, Fragment, UIEvent, useState } from "react" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import { NoResults } from "../../../common/empty-table-content" + +type BulkCommand = { + label: string + shortcut: string + action: (selection: Record) => void +} + +export interface DataTableRootProps { + /** + * The table instance to render + */ + table: ReactTable + /** + * The columns to render + */ + columns: ColumnDef[] + /** + * Function to generate a link to navigate to when clicking on a row + */ + navigateTo?: (row: Row) => string + /** + * Bulk actions to render + */ + commands?: BulkCommand[] + /** + * The total number of items in the table + */ + count?: number + /** + * Whether to display pagination controls + */ + pagination?: boolean + /** + * Whether the table is empty due to no results from the active query + */ + noResults?: boolean +} + +/** + * TODO + * + * Add a sticky header to the table that shows the column name when scrolling through the table vertically. + * + * This is a bit tricky as we can't support horizontal scrolling and sticky headers at the same time, natively + * with CSS. We need to implement a custom solution for this. One solution is to render a duplicate table header + * using a DIV that, but it will require rerendeing the duplicate header every time the window is resized, to keep + * the columns aligned. + */ + +/** + * Table component for rendering a table with pagination, filtering and ordering. + */ +export const DataTableRoot = ({ + table, + columns, + pagination, + navigateTo, + commands, + count = 0, + noResults = false, +}: DataTableRootProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + const [showStickyBorder, setShowStickyBorder] = useState(false) + + const hasSelect = columns.find((c) => c.id === "select") + const hasActions = columns.find((c) => c.id === "actions") + const hasCommandBar = commands && commands.length > 0 + + const rowSelection = table.getState().rowSelection + const { pageIndex, pageSize } = table.getState().pagination + + const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0) + const colWidth = 100 / colCount + + const handleHorizontalScroll = (e: UIEvent) => { + const scrollLeft = e.currentTarget.scrollLeft + + if (scrollLeft > 0) { + setShowStickyBorder(true) + } else { + setShowStickyBorder(false) + } + } + + return ( +
    +
    + {!noResults ? ( +
+ + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header, index) => { + const isActionHeader = header.id === "actions" + const isSelectHeader = header.id === "select" + const isSpecialHeader = isActionHeader || isSelectHeader + + const firstHeader = headerGroup.headers.findIndex( + (h) => h.id !== "select" + ) + const isFirstHeader = + firstHeader !== -1 + ? header.id === headerGroup.headers[firstHeader].id + : index === 0 + + const isStickyHeader = isSelectHeader || isFirstHeader + + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => { + const to = navigateTo ? navigateTo(row) : undefined + return ( + navigate(to) : undefined} + > + {row.getVisibleCells().map((cell, index) => { + const visibleCells = row.getVisibleCells() + const isSelectCell = cell.id === "select" + + const firstCell = visibleCells.findIndex( + (h) => h.id !== "select" + ) + const isFirstCell = + firstCell !== -1 + ? cell.id === visibleCells[firstCell].id + : index === 0 + + const isStickyCell = isSelectCell || isFirstCell + + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ) + })} + + ) + })} + +
+ ) : ( +
+ +
+ )} + + {pagination && ( + + )} + {hasCommandBar && ( + + + + {t("general.countSelected", { + count: Object.keys(rowSelection).length, + })} + + + {commands?.map((command, index) => { + return ( + + command.action(rowSelection)} + /> + {index < commands.length - 1 && } + + ) + })} + + + )} + + ) +} + +type PaginationProps = Omit< + ComponentPropsWithoutRef, + "translations" +> + +const Pagination = (props: PaginationProps) => { + const { t } = useTranslation() + + const translations = { + of: t("general.of"), + results: t("general.results"), + pages: t("general.pages"), + prev: t("general.prev"), + next: t("general.next"), + } + + return +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts new file mode 100644 index 0000000000000..8d47458cf9c89 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-root/index.ts @@ -0,0 +1 @@ +export * from "./data-table-root" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx new file mode 100644 index 0000000000000..f1bdb01027dc8 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/data-table-search.tsx @@ -0,0 +1,57 @@ +import { Input } from "@medusajs/ui" +import { ChangeEvent, useCallback, useEffect } from "react" +import { useTranslation } from "react-i18next" + +import { debounce } from "lodash" +import { useSelectedParams } from "../hooks" + +type DataTableSearchProps = { + placeholder?: string + prefix?: string +} + +export const DataTableSearch = ({ + placeholder, + prefix, +}: DataTableSearchProps) => { + const { t } = useTranslation() + const placeholderText = placeholder || t("general.search") + const selectedParams = useSelectedParams({ + param: "q", + prefix, + multiple: false, + }) + + const query = selectedParams.get() + + // eslint-disable-next-line react-hooks/exhaustive-deps + const debouncedOnChange = useCallback( + debounce((e: ChangeEvent) => { + const value = e.target.value + + if (!value) { + selectedParams.delete() + } else { + selectedParams.add(value) + } + }, 500), + [selectedParams] + ) + + useEffect(() => { + return () => { + debouncedOnChange.cancel() + } + }, [debouncedOnChange]) + + return ( + + ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts new file mode 100644 index 0000000000000..1f19f481bed4f --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-search/index.ts @@ -0,0 +1 @@ +export * from "./data-table-search" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx new file mode 100644 index 0000000000000..56174606a5924 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/data-table-skeleton.tsx @@ -0,0 +1,115 @@ +import { Table, clx } from "@medusajs/ui" +import { ColumnDef } from "@tanstack/react-table" +import { Skeleton } from "../../../common/skeleton" + +type DataTableSkeletonProps = { + columns: ColumnDef[] + rowCount: number + searchable: boolean + orderBy: boolean + filterable: boolean + pagination: boolean +} + +export const DataTableSkeleton = ({ + columns, + rowCount, + filterable, + searchable, + orderBy, + pagination, +}: DataTableSkeletonProps) => { + const rows = Array.from({ length: rowCount }, (_, i) => i) + + const hasToolbar = filterable || searchable || orderBy + const hasSearchOrOrder = searchable || orderBy + + const hasSelect = columns.find((c) => c.id === "select") + const hasActions = columns.find((c) => c.id === "actions") + const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0) + const colWidth = 100 / colCount + + return ( +
+ {hasToolbar && ( +
+ {filterable && } + {hasSearchOrOrder && ( +
+ {searchable && } + {orderBy && } +
+ )} +
+ )} + + + + {columns.map((col, i) => { + const isSelectHeader = col.id === "select" + const isActionsHeader = col.id === "actions" + + const isSpecialHeader = isSelectHeader || isActionsHeader + + return ( + + {isActionsHeader ? null : ( + + )} + + ) + })} + + + + {rows.map((_, j) => ( + + {columns.map((col, k) => { + const isSpecialCell = + col.id === "select" || col.id === "actions" + + return ( + + + + ) + })} + + ))} + +
+ {pagination && ( +
+ +
+ + + +
+
+ )} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts new file mode 100644 index 0000000000000..fbed89a8c7c18 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table-skeleton/index.ts @@ -0,0 +1 @@ +export * from "./data-table-skeleton" diff --git a/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx new file mode 100644 index 0000000000000..09446bb367a79 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/data-table.tsx @@ -0,0 +1,74 @@ +import { memo } from "react" +import { NoRecords } from "../../common/empty-table-content" +import { DataTableQuery, DataTableQueryProps } from "./data-table-query" +import { DataTableRoot, DataTableRootProps } from "./data-table-root" +import { DataTableSkeleton } from "./data-table-skeleton" + +interface DataTableProps + extends DataTableRootProps, + DataTableQueryProps { + isLoading?: boolean + rowCount: number + queryObject?: Record +} + +const MemoizedDataTableRoot = memo(DataTableRoot) as typeof DataTableRoot +const MemoizedDataTableQuery = memo(DataTableQuery) + +export const DataTable = ({ + table, + columns, + pagination, + navigateTo, + commands, + count = 0, + search = false, + orderBy, + filters, + prefix, + queryObject = {}, + rowCount, + isLoading = false, +}: DataTableProps) => { + if (isLoading) { + return ( + + ) + } + + const noQuery = + Object.values(queryObject).filter((v) => Boolean(v)).length === 0 + const noResults = !isLoading && count === 0 && !noQuery + const noRecords = !isLoading && count === 0 && noQuery + + if (noRecords) { + return + } + + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx b/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx new file mode 100644 index 0000000000000..aeda07245406c --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/hooks.tsx @@ -0,0 +1,73 @@ +import { useSearchParams } from "react-router-dom" + +export const useSelectedParams = ({ + param, + prefix, + multiple = false, +}: { + param: string + prefix?: string + multiple?: boolean +}) => { + const [searchParams, setSearchParams] = useSearchParams() + const identifier = prefix ? `${prefix}_${param}` : param + const offsetKey = prefix ? `${prefix}_offset` : "offset" + + const add = (value: string) => { + setSearchParams((prev) => { + const newValue = new URLSearchParams(prev) + + const updateMultipleValues = () => { + const existingValues = newValue.get(identifier)?.split(",") || [] + + if (!existingValues.includes(value)) { + existingValues.push(value) + newValue.set(identifier, existingValues.join(",")) + } + } + + const updateSingleValue = () => { + newValue.set(identifier, value) + } + + multiple ? updateMultipleValues() : updateSingleValue() + newValue.delete(offsetKey) + + return newValue + }) + } + + const deleteParam = (value?: string) => { + const deleteMultipleValues = (prev: URLSearchParams) => { + const existingValues = prev.get(identifier)?.split(",") || [] + const index = existingValues.indexOf(value || "") + if (index > -1) { + existingValues.splice(index, 1) + prev.set(identifier, existingValues.join(",")) + } + } + + const deleteSingleValue = (prev: URLSearchParams) => { + prev.delete(identifier) + } + + setSearchParams((prev) => { + if (value) { + multiple ? deleteMultipleValues(prev) : deleteSingleValue(prev) + if (!prev.get(identifier)) { + prev.delete(identifier) + } + } else { + prev.delete(identifier) + } + prev.delete(offsetKey) + return prev + }) + } + + const get = () => { + return searchParams.get(identifier)?.split(",").filter(Boolean) || [] + } + + return { add, delete: deleteParam, get } +} diff --git a/packages/admin-next/dashboard/src/components/table/data-table/index.ts b/packages/admin-next/dashboard/src/components/table/data-table/index.ts new file mode 100644 index 0000000000000..78f00d949d53b --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/data-table/index.ts @@ -0,0 +1,2 @@ +export * from "./data-table" +export type { Filter } from "./data-table-filter" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx new file mode 100644 index 0000000000000..44dda62fb4653 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/date-cell.tsx @@ -0,0 +1,41 @@ +import { Tooltip } from "@medusajs/ui" +import format from "date-fns/format" +import { useTranslation } from "react-i18next" + +type DateCellProps = { + date: Date +} + +export const DateCell = ({ date }: DateCellProps) => { + const value = new Date(date) + value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) + + const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12 + const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM" + + return ( +
+ {`${format( + value, + timestampFormat + )}`} + } + > + {format(value, "dd MMM yyyy")} + +
+ ) +} + +export const DateHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.date")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts new file mode 100644 index 0000000000000..9bf7e52ed0236 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/date-cell/index.ts @@ -0,0 +1 @@ +export * from "./date-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts new file mode 100644 index 0000000000000..8cc5d6026b4fb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/index.ts @@ -0,0 +1 @@ +export * from "./status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx new file mode 100644 index 0000000000000..168a94117e69e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/common/status-cell/status-cell.tsx @@ -0,0 +1,32 @@ +import { clx } from "@medusajs/ui" +import { PropsWithChildren } from "react" + +type StatusCellProps = PropsWithChildren<{ + color?: "green" | "red" | "blue" | "orange" | "grey" | "purple" +}> + +export const StatusCell = ({ color, children }: StatusCellProps) => { + return ( +
+
+
+
+ {children} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx new file mode 100644 index 0000000000000..af558b959cec9 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx @@ -0,0 +1,29 @@ +import { Customer } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const CustomerCell = ({ customer }: { customer: Customer | null }) => { + if (!customer) { + return - + } + + const { first_name, last_name, email } = customer + const name = [first_name, last_name].filter(Boolean).join(" ") + + return ( +
+
+ {name || email} +
+
+ ) +} + +export const CustomerHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.customer")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts new file mode 100644 index 0000000000000..dbdd97615af02 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/customer-cell/index.ts @@ -0,0 +1 @@ +export * from "./customer-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx new file mode 100644 index 0000000000000..e4f2e48aeb6ab --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/display-id-cell.tsx @@ -0,0 +1,19 @@ +import { useTranslation } from "react-i18next" + +export const DisplayIdCell = ({ displayId }: { displayId: number }) => { + return ( +
+ #{displayId} +
+ ) +} + +export const DisplayIdHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.order")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts new file mode 100644 index 0000000000000..057f15de5bdcb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/display-id-cell/index.ts @@ -0,0 +1 @@ +export * from "./display-id-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx new file mode 100644 index 0000000000000..d899e85468a31 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/fulfillment-status-cell.tsx @@ -0,0 +1,46 @@ +import type { FulfillmentStatus } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type FulfillmentStatusCellProps = { + status: FulfillmentStatus +} + +export const FulfillmentStatusCell = ({ + status, +}: FulfillmentStatusCellProps) => { + const { t } = useTranslation() + + const [label, color] = { + not_fulfilled: [t("orders.fulfillmentStatus.notFulfilled"), "red"], + partially_fulfilled: [ + t("orders.fulfillmentStatus.partiallyFulfilled"), + "orange", + ], + fulfilled: [t("orders.fulfillmentStatus.fulfilled"), "green"], + partially_shipped: [ + t("orders.fulfillmentStatus.partiallyShipped"), + "orange", + ], + shipped: [t("orders.fulfillmentStatus.shipped"), "green"], + partially_returned: [ + t("orders.fulfillmentStatus.partiallyReturned"), + "orange", + ], + returned: [t("orders.fulfillmentStatus.returned"), "green"], + canceled: [t("orders.fulfillmentStatus.canceled"), "red"], + requires_action: [t("orders.fulfillmentStatus.requresAction"), "orange"], + }[status] as [string, "red" | "orange" | "green"] + + return {label} +} + +export const FulfillmentStatusHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.fulfillment")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts new file mode 100644 index 0000000000000..a0f92c11b9f99 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/fulfillment-status-cell/index.ts @@ -0,0 +1 @@ +export * from "./fulfillment-status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts new file mode 100644 index 0000000000000..8df6791eaee23 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/index.ts @@ -0,0 +1 @@ +export * from "./items-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx new file mode 100644 index 0000000000000..fc0c3b3403005 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/items-cell/items-cell.tsx @@ -0,0 +1,26 @@ +import type { LineItem } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const ItemsCell = ({ items }: { items: LineItem[] }) => { + const { t } = useTranslation() + + return ( +
+ + {t("general.items", { + count: items.length, + })} + +
+ ) +} + +export const ItemsHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.items")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts new file mode 100644 index 0000000000000..f6c4b069ee991 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/index.ts @@ -0,0 +1 @@ +export * from "./payment-status-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx new file mode 100644 index 0000000000000..db8828fa26799 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx @@ -0,0 +1,33 @@ +import type { PaymentStatus } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" +import { StatusCell } from "../../common/status-cell" + +type PaymentStatusCellProps = { + status: PaymentStatus +} + +export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => { + const { t } = useTranslation() + + const [label, color] = { + not_paid: [t("orders.paymentStatus.notPaid"), "red"], + awaiting: [t("orders.paymentStatus.awaiting"), "orange"], + captured: [t("orders.paymentStatus.captured"), "green"], + refunded: [t("orders.paymentStatus.refunded"), "green"], + partially_refunded: [t("orders.paymentStatus.partiallyRefunded"), "orange"], + canceled: [t("orders.paymentStatus.canceled"), "red"], + requires_action: [t("orders.paymentStatus.requresAction"), "orange"], + }[status] as [string, "red" | "orange" | "green"] + + return {label} +} + +export const PaymentStatusHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.payment")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts new file mode 100644 index 0000000000000..8040cf7b1b40e --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/index.ts @@ -0,0 +1 @@ +export * from "./sales-channel-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx new file mode 100644 index 0000000000000..17e249d0418eb --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx @@ -0,0 +1,30 @@ +import { SalesChannel } from "@medusajs/medusa" +import { useTranslation } from "react-i18next" + +export const SalesChannelCell = ({ + channel, +}: { + channel: SalesChannel | null +}) => { + if (!channel) { + return - + } + + const { name } = channel + + return ( +
+ {name} +
+ ) +} + +export const SalesChannelHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.salesChannel")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts new file mode 100644 index 0000000000000..a58e61c257cee --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/index.ts @@ -0,0 +1 @@ +export * from "./total-cell" diff --git a/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx new file mode 100644 index 0000000000000..58646ee9339f4 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/table/table-cells/order/total-cell/total-cell.tsx @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next" +import { getPresentationalAmount } from "../../../../../lib/money-amount-helpers" + +type TotalCellProps = { + currencyCode: string + total: number | null +} + +export const TotalCell = ({ currencyCode, total }: TotalCellProps) => { + if (!total) { + return - + } + + const formatted = new Intl.NumberFormat(undefined, { + style: "currency", + currency: currencyCode, + currencyDisplay: "narrowSymbol", + }).format(0) + + const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() + + const presentationAmount = getPresentationalAmount(total, currencyCode) + const formattedTotal = new Intl.NumberFormat(undefined, { + style: "decimal", + }).format(presentationAmount) + + return ( +
+ + {symbol} {formattedTotal} {currencyCode.toUpperCase()} + +
+ ) +} + +export const TotalHeader = () => { + const { t } = useTranslation() + + return ( +
+ {t("fields.total")} +
+ ) +} diff --git a/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx new file mode 100644 index 0000000000000..490db287b1497 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/columns/use-order-table-columns.tsx @@ -0,0 +1,133 @@ +import { Order } from "@medusajs/medusa" +import { + ColumnDef, + ColumnDefBase, + createColumnHelper, +} from "@tanstack/react-table" +import { useMemo } from "react" +import { + DateCell, + DateHeader, +} from "../../../components/table/table-cells/common/date-cell" +import { + DisplayIdCell, + DisplayIdHeader, +} from "../../../components/table/table-cells/order/display-id-cell" +import { + FulfillmentStatusCell, + FulfillmentStatusHeader, +} from "../../../components/table/table-cells/order/fulfillment-status-cell" +import { + ItemsCell, + ItemsHeader, +} from "../../../components/table/table-cells/order/items-cell" +import { + PaymentStatusCell, + PaymentStatusHeader, +} from "../../../components/table/table-cells/order/payment-status-cell" +import { + SalesChannelCell, + SalesChannelHeader, +} from "../../../components/table/table-cells/order/sales-channel-cell" +import { + TotalCell, + TotalHeader, +} from "../../../components/table/table-cells/order/total-cell" + +// We have to use any here, as the type of Order is so complex that it lags the TS server +const columnHelper = createColumnHelper() + +type UseOrderTableColumnsProps = { + exclude?: string[] +} + +export const useOrderTableColumns = (props: UseOrderTableColumnsProps) => { + const { exclude = [] } = props ?? {} + + const columns = useMemo( + () => [ + columnHelper.accessor("display_id", { + header: () => , + cell: ({ getValue }) => { + const id = getValue() + + return + }, + }), + columnHelper.accessor("created_at", { + header: () => , + cell: ({ getValue }) => { + const date = new Date(getValue()) + + return + }, + }), + columnHelper.accessor("sales_channel", { + header: () => , + cell: ({ getValue }) => { + const channel = getValue() + + return + }, + }), + columnHelper.accessor("payment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + + return + }, + }), + columnHelper.accessor("fulfillment_status", { + header: () => , + cell: ({ getValue }) => { + const status = getValue() + + return + }, + }), + columnHelper.accessor("items", { + header: () => , + cell: ({ getValue }) => { + const items = getValue() + + return + }, + }), + columnHelper.accessor("total", { + header: () => , + cell: ({ getValue, row }) => { + const total = getValue() + const currencyCode = row.original.currency_code + + return + }, + }), + ], + [] + ) + + const isAccessorColumnDef = ( + c: any + ): c is ColumnDef & { accessorKey: string } => { + return c.accessorKey !== undefined + } + + const isDisplayColumnDef = ( + c: any + ): c is ColumnDef & { id: string } => { + return c.id !== undefined + } + + const shouldExclude = >(c: TDef) => { + if (isAccessorColumnDef(c)) { + return exclude.includes(c.accessorKey) + } else if (isDisplayColumnDef(c)) { + return exclude.includes(c.id) + } + + return false + } + + return columns.filter((c) => !shouldExclude(c)) as ColumnDef[] +} diff --git a/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx new file mode 100644 index 0000000000000..aa5b309f05684 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/filters/use-order-table-filters.tsx @@ -0,0 +1,154 @@ +import { useAdminRegions, useAdminSalesChannels } from "medusa-react" +import { useTranslation } from "react-i18next" + +import type { Filter } from "../../../components/table/data-table" + +export const useOrderTableFilters = (): Filter[] => { + const { t } = useTranslation() + + const { regions } = useAdminRegions({ + limit: 1000, + fields: "id,name", + expand: "", + }) + + const { sales_channels } = useAdminSalesChannels({ + limit: 1000, + fields: "id,name", + expand: "", + }) + + let filters: Filter[] = [] + + if (regions) { + const regionFilter: Filter = { + key: "region_id", + label: t("fields.region"), + type: "select", + options: regions.map((r) => ({ + label: r.name, + value: r.id, + })), + multiple: true, + searchable: true, + } + + filters = [...filters, regionFilter] + } + + if (sales_channels) { + const salesChannelFilter: Filter = { + key: "sales_channel_id", + label: t("fields.salesChannel"), + type: "select", + multiple: true, + searchable: true, + options: sales_channels.map((s) => ({ + label: s.name, + value: s.id, + })), + } + + filters = [...filters, salesChannelFilter] + } + + const paymentStatusFilter: Filter = { + key: "payment_status", + label: t("orders.paymentStatusLabel"), + type: "select", + multiple: true, + options: [ + { + label: t("orders.paymentStatus.notPaid"), + value: "not_paid", + }, + { + label: t("orders.paymentStatus.awaiting"), + value: "awaiting", + }, + { + label: t("orders.paymentStatus.captured"), + value: "captured", + }, + { + label: t("orders.paymentStatus.refunded"), + value: "refunded", + }, + { + label: t("orders.paymentStatus.partiallyRefunded"), + value: "partially_refunded", + }, + { + label: t("orders.paymentStatus.canceled"), + value: "canceled", + }, + { + label: t("orders.paymentStatus.requresAction"), + value: "requires_action", + }, + ], + } + + const fulfillmentStatusFilter: Filter = { + key: "fulfillment_status", + label: t("orders.fulfillmentStatusLabel"), + type: "select", + multiple: true, + options: [ + { + label: t("orders.fulfillmentStatus.notFulfilled"), + value: "not_fulfilled", + }, + { + label: t("orders.fulfillmentStatus.fulfilled"), + value: "fulfilled", + }, + { + label: t("orders.fulfillmentStatus.partiallyFulfilled"), + value: "partially_fulfilled", + }, + { + label: t("orders.fulfillmentStatus.returned"), + value: "returned", + }, + { + label: t("orders.fulfillmentStatus.partiallyReturned"), + value: "partially_returned", + }, + { + label: t("orders.fulfillmentStatus.shipped"), + value: "shipped", + }, + { + label: t("orders.fulfillmentStatus.partiallyShipped"), + value: "partially_shipped", + }, + { + label: t("orders.fulfillmentStatus.canceled"), + value: "canceled", + }, + { + label: t("orders.fulfillmentStatus.requresAction"), + value: "requires_action", + }, + ], + } + + const dateFilters: Filter[] = [ + { label: "Created At", key: "created_at" }, + { label: "Updated At", key: "updated_at" }, + ].map((f) => ({ + key: f.key, + label: f.label, + type: "date", + })) + + filters = [ + ...filters, + paymentStatusFilter, + fulfillmentStatusFilter, + ...dateFilters, + ] + + return filters +} diff --git a/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx new file mode 100644 index 0000000000000..bcf52dad14c1d --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/table/query/use-order-table-query.tsx @@ -0,0 +1,58 @@ +import { AdminGetOrdersParams } from "@medusajs/medusa" +import { useQueryParams } from "../../use-query-params" + +type UseOrderTableQueryProps = { + prefix?: string + pageSize?: number +} + +/** + * TODO: Enable `order` query param when staging is updated + */ + +export const useOrderTableQuery = ({ + prefix, + pageSize = 50, +}: UseOrderTableQueryProps) => { + const queryObject = useQueryParams( + [ + "offset", + "q", + "created_at", + "updated_at", + "region_id", + "sales_channel_id", + "payment_status", + "fulfillment_status", + ], + prefix + ) + + const { + offset, + sales_channel_id, + created_at, + updated_at, + fulfillment_status, + payment_status, + region_id, + q, + } = queryObject + + const searchParams: AdminGetOrdersParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + sales_channel_id: sales_channel_id?.split(","), + fulfillment_status: fulfillment_status?.split(","), + payment_status: payment_status?.split(","), + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + region_id: region_id?.split(","), + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin-next/dashboard/src/hooks/use-data-table.tsx b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx new file mode 100644 index 0000000000000..5be85018f4e4c --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-data-table.tsx @@ -0,0 +1,112 @@ +import { + ColumnDef, + OnChangeFn, + PaginationState, + Row, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useEffect, useMemo, useState } from "react" +import { useSearchParams } from "react-router-dom" + +type UseDataTableProps = { + data?: TData[] + columns: ColumnDef[] + count?: number + pageSize?: number + enableRowSelection?: boolean | ((row: Row) => boolean) + enablePagination?: boolean + getRowId?: (original: TData, index: number) => string + prefix?: string +} + +export const useDataTable = ({ + data = [], + columns, + count = 0, + pageSize: _pageSize = 50, + enablePagination = true, + enableRowSelection = false, + getRowId, + prefix, +}: UseDataTableProps) => { + const [searchParams, setSearchParams] = useSearchParams() + const offsetKey = `${prefix ? `${prefix}_` : ""}offset` + const offset = searchParams.get(offsetKey) + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: offset ? Math.ceil(Number(offset) / _pageSize) : 0, + pageSize: _pageSize, + }) + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + const [rowSelection, setRowSelection] = useState({}) + + useEffect(() => { + if (!enablePagination) { + return + } + + const index = offset ? Math.ceil(Number(offset) / _pageSize) : 0 + + if (index === pageIndex) { + return + } + + setPagination((prev) => ({ + ...prev, + pageIndex: index, + })) + }, [offset, enablePagination, _pageSize, pageIndex]) + + const onPaginationChange = ( + updater: (old: PaginationState) => PaginationState + ) => { + const state = updater(pagination) + const { pageIndex, pageSize } = state + + setSearchParams((prev) => { + if (!pageIndex) { + prev.delete(offsetKey) + return prev + } + + const newSearch = new URLSearchParams(prev) + newSearch.set(offsetKey, String(pageIndex * pageSize)) + + return newSearch + }) + + setPagination(state) + return state + } + + const table = useReactTable({ + data, + columns, + state: { + rowSelection, + pagination: enablePagination ? pagination : undefined, + }, + pageCount: Math.ceil((count ?? 0) / pageSize), + enableRowSelection, + getRowId, + onRowSelectionChange: enableRowSelection ? setRowSelection : undefined, + onPaginationChange: enablePagination + ? (onPaginationChange as OnChangeFn) + : undefined, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: enablePagination + ? getPaginationRowModel() + : undefined, + manualPagination: enablePagination ? true : undefined, + }) + + return { table } +} diff --git a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx index afb282362e58b..623975e5fb362 100644 --- a/packages/admin-next/dashboard/src/hooks/use-query-params.tsx +++ b/packages/admin-next/dashboard/src/hooks/use-query-params.tsx @@ -1,15 +1,23 @@ import { useSearchParams } from "react-router-dom" +type QueryParams = { + [key in T]: string | undefined +} + export function useQueryParams( - keys: T[] -): Record { + keys: T[], + prefix?: string +): QueryParams { const [params] = useSearchParams() // Use a type assertion to initialize the result - const result = {} as Record + const result = {} as QueryParams keys.forEach((key) => { - result[key] = params.get(key) || undefined + const prefixedKey = prefix ? `${prefix}_${key}` : key + const value = params.get(prefixedKey) || undefined + + result[key] = value }) return result diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index dbdb2d99dda0b..3cb8603b76591 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -74,7 +74,7 @@ const router = createBrowserRouter([ children: [ { index: true, - lazy: () => import("../../routes/orders/list"), + lazy: () => import("../../routes/orders/order-list"), }, { path: ":id", diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx index 026ad1f84db3c..a2f307c56867b 100644 --- a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -1,99 +1,62 @@ -import { ReceiptPercent } from "@medusajs/icons" -import { Customer, Order } from "@medusajs/medusa" -import { Button, Container, Heading, Table, clx } from "@medusajs/ui" -import { - PaginationState, - RowSelectionState, - createColumnHelper, - flexRender, - getCoreRowModel, - useReactTable, -} from "@tanstack/react-table" +import { Customer } from "@medusajs/medusa" +import { Button, Container, Heading } from "@medusajs/ui" import { useAdminOrders } from "medusa-react" -import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" -import { useNavigate } from "react-router-dom" -import { ActionMenu } from "../../../../../components/common/action-menu" -import { NoRecords } from "../../../../../components/common/empty-table-content" -import { - OrderDateCell, - OrderDisplayIdCell, - OrderFulfillmentStatusCell, - OrderPaymentStatusCell, - OrderTotalCell, -} from "../../../../../components/common/order-table-cells" -import { Query } from "../../../../../components/filtering/query" -import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" -import { useQueryParams } from "../../../../../hooks/use-query-params" +import { DataTable } from "../../../../../components/table/data-table" +import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" +import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" type CustomerGeneralSectionProps = { customer: Customer } const PAGE_SIZE = 10 +const DEFAULT_RELATIONS = "customer,items,sales_channel" +const DEFAULT_FIELDS = + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code" export const CustomerOrderSection = ({ customer, }: CustomerGeneralSectionProps) => { const { t } = useTranslation() - const navigate = useNavigate() - const [{ pageIndex, pageSize }, setPagination] = useState({ - pageIndex: 0, + const { searchParams, raw } = useOrderTableQuery({ pageSize: PAGE_SIZE, }) - - const pagination = useMemo( - () => ({ - pageIndex, - pageSize, - }), - [pageIndex, pageSize] - ) - - const [rowSelection, setRowSelection] = useState({}) - - const params = useQueryParams(["q"]) const { orders, count, isLoading, isError, error } = useAdminOrders( { customer_id: customer.id, - limit: PAGE_SIZE, - offset: pageIndex * PAGE_SIZE, - fields: - "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code", - ...params, + expand: DEFAULT_RELATIONS, + fields: DEFAULT_FIELDS, + ...searchParams, }, { keepPreviousData: true, } ) - const columns = useColumns() + const columns = useOrderTableColumns({ + exclude: ["customer"], + }) + const filters = useOrderTableFilters() - const table = useReactTable({ + const { table } = useDataTable({ data: orders ?? [], columns, - pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), - state: { - pagination, - rowSelection, - }, - onPaginationChange: setPagination, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - manualPagination: true, + enablePagination: true, + count, + pageSize: PAGE_SIZE, }) - const noRecords = - Object.values(params).every((v) => !v) && !isLoading && !orders?.length - if (isError) { throw error } return ( - -
+ +
{t("orders.domain")}
- {!noRecords && ( -
-
-
- -
-
- )} - {noRecords ? ( - - ) : ( -
- - - {table.getHeaderGroups().map((headerGroup) => { - return ( - - {headerGroup.headers.map((header) => { - return ( - - {flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ) - })} - - ) - })} - - - {table.getRowModel().rows.map((row) => ( - navigate(`/orders/${row.original.id}`)} - > - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - ))} - -
- -
- )} + `/orders/${row.original.id}`} + filters={filters} + count={count} + isLoading={isLoading} + rowCount={PAGE_SIZE} + orderBy={["display_id", "created_at", "updated_at"]} + search={true} + queryObject={raw} + />
) } - -const OrderActions = ({ order }: { order: Order }) => { - const { t } = useTranslation() - - return ( - , - label: t("customers.viewOrder"), - to: `/orders/${order.id}/edit`, - }, - ], - }, - ]} - /> - ) -} - -const columnHelper = createColumnHelper() - -const useColumns = () => { - const { t } = useTranslation() - - return useMemo( - () => [ - columnHelper.accessor("display_id", { - header: "Order", - cell: ({ getValue }) => , - }), - columnHelper.accessor("created_at", { - header: "Date", - cell: ({ getValue }) => , - }), - columnHelper.accessor("fulfillment_status", { - header: "Fulfillment Status", - cell: ({ getValue }) => ( - - ), - }), - columnHelper.accessor("payment_status", { - header: "Payment Status", - cell: ({ getValue }) => , - }), - columnHelper.accessor("total", { - header: () => t("fields.total"), - cell: ({ getValue, row }) => ( - - ), - }), - columnHelper.display({ - id: "actions", - cell: ({ row }) => , - }), - ], - [t] - ) -} diff --git a/packages/admin-next/dashboard/src/routes/orders/list/index.ts b/packages/admin-next/dashboard/src/routes/orders/list/index.ts deleted file mode 100644 index ad7ea56183413..0000000000000 --- a/packages/admin-next/dashboard/src/routes/orders/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { OrderList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/orders/list/list.tsx b/packages/admin-next/dashboard/src/routes/orders/list/list.tsx deleted file mode 100644 index d5498116ac06c..0000000000000 --- a/packages/admin-next/dashboard/src/routes/orders/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const OrderList = () => { - return ( -
- - Orders - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts new file mode 100644 index 0000000000000..f6520707347f2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/index.ts @@ -0,0 +1 @@ +export * from "./order-list-table" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx new file mode 100644 index 0000000000000..05ba27416a2b2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/components/order-list-table/order-list-table.tsx @@ -0,0 +1,67 @@ +import { Container, Heading } from "@medusajs/ui" +import { useAdminOrders } from "medusa-react" +import { useTranslation } from "react-i18next" +import { DataTable } from "../../../../../components/table/data-table/data-table" +import { useOrderTableColumns } from "../../../../../hooks/table/columns/use-order-table-columns" +import { useOrderTableFilters } from "../../../../../hooks/table/filters/use-order-table-filters" +import { useOrderTableQuery } from "../../../../../hooks/table/query/use-order-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" + +const PAGE_SIZE = 50 +const DEFAULT_RELATIONS = "customer,items,sales_channel" +const DEFAULT_FIELDS = + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code" + +export const OrderListTable = () => { + const { t } = useTranslation() + const { searchParams, raw } = useOrderTableQuery({ + pageSize: PAGE_SIZE, + }) + + const { orders, count, isError, error, isLoading } = useAdminOrders( + { + expand: DEFAULT_RELATIONS, + fields: DEFAULT_FIELDS, + ...searchParams, + }, + { + keepPreviousData: true, + } + ) + + const filters = useOrderTableFilters() + const columns = useOrderTableColumns({}) + + const { table } = useDataTable({ + data: orders ?? [], + columns, + enablePagination: true, + count, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("orders.domain")} +
+ `/orders/${row.original.id}`} + filters={filters} + count={count} + search + isLoading={isLoading} + rowCount={PAGE_SIZE} + orderBy={["display_id", "created_at", "updated_at"]} + queryObject={raw} + /> +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts new file mode 100644 index 0000000000000..0d3535fb846c6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/index.ts @@ -0,0 +1 @@ +export { OrderList as Component } from "./order-list" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx b/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx new file mode 100644 index 0000000000000..587cb88bf516d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-list/order-list.tsx @@ -0,0 +1,9 @@ +import { OrderListTable } from "./components/order-list-table" + +export const OrderList = () => { + return ( +
+ +
+ ) +} diff --git a/packages/auth/integration-tests/__fixtures__/auth-user/index.ts b/packages/auth/integration-tests/__fixtures__/auth-user/index.ts index 46e745ffdc18b..fefbdc9a6d90c 100644 --- a/packages/auth/integration-tests/__fixtures__/auth-user/index.ts +++ b/packages/auth/integration-tests/__fixtures__/auth-user/index.ts @@ -1,5 +1,5 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" import { AuthUser } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" export async function createAuthUsers( manager: SqlEntityManager, @@ -8,15 +8,18 @@ export async function createAuthUsers( id: "test-id", entity_id: "test-id", provider: "manual", + scope: "store", }, { id: "test-id-1", entity_id: "test-id-1", provider: "manual", + scope: "store", }, { entity_id: "test-id-2", provider: "store", + scope: "store", }, ] ): Promise { diff --git a/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts b/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts index 934bf94ec7d68..3ce97ecd458e4 100644 --- a/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/auth-provider/index.spec.ts @@ -1,16 +1,16 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" -import { AuthProviderService } from "@services" import { MikroOrmWrapper } from "../../../utils" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createMedusaContainer } from "@medusajs/utils" import { asValue } from "awilix" import ContainerLoader from "../../../../src/loaders/container" +import { ModulesSdkTypes } from "@medusajs/types" jest.setTimeout(30000) describe("AuthProvider Service", () => { - let service: AuthProviderService + let service: ModulesSdkTypes.InternalModuleService let testManager: SqlEntityManager let repositoryManager: SqlEntityManager @@ -180,7 +180,7 @@ describe("AuthProvider Service", () => { error = e } - expect(error.message).toEqual('"authProviderProvider" must be defined') + expect(error.message).toEqual("authProvider - provider must be defined") }) it("should return authProvider based on config select param", async () => { diff --git a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts index 07f7aa2426d9e..7dd4afe52e3b0 100644 --- a/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/auth-user/index.spec.ts @@ -1,12 +1,11 @@ -import { SqlEntityManager } from "@mikro-orm/postgresql" import { AuthUserService } from "@services" - +import ContainerLoader from "../../../../src/loaders/container" import { MikroOrmWrapper } from "../../../utils" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { asValue } from "awilix" import { createAuthProviders } from "../../../__fixtures__/auth-provider" import { createAuthUsers } from "../../../__fixtures__/auth-user" import { createMedusaContainer } from "@medusajs/utils" -import { asValue } from "awilix" -import ContainerLoader from "../../../../src/loaders/container" jest.setTimeout(30000) @@ -167,7 +166,7 @@ describe("AuthUser Service", () => { error = e } - expect(error.message).toEqual('"authUserId" must be defined') + expect(error.message).toEqual("authUser - id must be defined") }) }) @@ -229,7 +228,8 @@ describe("AuthUser Service", () => { { id: "test", provider_id: "manual", - entity_id: "test" + entity_id: "test", + scope: "store", }, ]) diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts index 2fdf373176674..37aaa50ad296d 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-provider.spec.ts @@ -43,9 +43,8 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAuthProviders", () => { it("should list AuthProviders", async () => { const authProviders = await service.listAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual( + expect(authProviders).toEqual( expect.arrayContaining([ expect.objectContaining({ provider: "manual", @@ -80,9 +79,7 @@ describe("AuthModuleService - AuthProvider", () => { is_active: true, }) - const serialized = JSON.parse(JSON.stringify(authProviders)) - - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -99,10 +96,9 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAndCountAuthProviders", () => { it("should list and count AuthProviders", async () => { const [authProviders, count] = await service.listAndCountAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) expect(count).toEqual(4) - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -136,10 +132,8 @@ describe("AuthModuleService - AuthProvider", () => { is_active: true, }) - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(count).toEqual(3) - expect(serialized).toEqual([ + expect(authProviders).toEqual([ expect.objectContaining({ provider: "manual", }), @@ -171,9 +165,7 @@ describe("AuthModuleService - AuthProvider", () => { select: ["provider"], }) - const serialized = JSON.parse(JSON.stringify(authProvider)) - - expect(serialized).toEqual({ + expect(authProvider).toEqual({ provider, }) }) @@ -201,7 +193,7 @@ describe("AuthModuleService - AuthProvider", () => { error = e } - expect(error.message).toEqual('"authProviderProvider" must be defined') + expect(error.message).toEqual("authProvider - provider must be defined") }) }) @@ -209,7 +201,7 @@ describe("AuthModuleService - AuthProvider", () => { const provider = "manual" it("should delete the authProviders given a provider successfully", async () => { - await service.deleteAuthProvider([provider]) + await service.deleteAuthProviders([provider]) const authProviders = await service.listAuthProviders({ provider: [provider], diff --git a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts index 86fde3c04bab6..3ca27ed83e01d 100644 --- a/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/auth-user.spec.ts @@ -43,17 +43,16 @@ describe("AuthModuleService - AuthUser", () => { describe("listAuthUsers", () => { it("should list authUsers", async () => { const authUsers = await service.listAuthUsers() - const serialized = JSON.parse(JSON.stringify(authUsers)) - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "store", + provider: { provider: "store" }, }), ]) }) @@ -75,9 +74,7 @@ describe("AuthModuleService - AuthUser", () => { provider_id: "manual", }) - const serialized = JSON.parse(JSON.stringify(authUsers)) - - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ id: "test-id", }), @@ -91,18 +88,17 @@ describe("AuthModuleService - AuthUser", () => { describe("listAndCountAuthUsers", () => { it("should list and count authUsers", async () => { const [authUsers, count] = await service.listAndCountAuthUsers() - const serialized = JSON.parse(JSON.stringify(authUsers)) expect(count).toEqual(3) - expect(serialized).toEqual([ + expect(authUsers).toEqual([ expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "manual", + provider: { provider: "manual" }, }), expect.objectContaining({ - provider: "store", + provider: { provider: "store" }, }), ]) }) @@ -171,7 +167,7 @@ describe("AuthModuleService - AuthUser", () => { error = e } - expect(error.message).toEqual('"authUserId" must be defined') + expect(error.message).toEqual("authUser - id must be defined") }) it("should return authUser based on config select param", async () => { @@ -179,9 +175,7 @@ describe("AuthModuleService - AuthUser", () => { select: ["id"], }) - const serialized = JSON.parse(JSON.stringify(authUser)) - - expect(serialized).toEqual({ + expect(authUser).toEqual({ id, }) }) @@ -191,7 +185,7 @@ describe("AuthModuleService - AuthUser", () => { const id = "test-id" it("should delete the authUsers given an id successfully", async () => { - await service.deleteAuthUser([id]) + await service.deleteAuthUsers([id]) const authUsers = await service.listAuthUsers({ id: [id], @@ -246,6 +240,7 @@ describe("AuthModuleService - AuthUser", () => { id: "test", provider_id: "manual", entity_id: "test", + scope: "store", }, ]) diff --git a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts index 556db598380d5..b118e54ac026e 100644 --- a/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/module/providers.spec.ts @@ -45,9 +45,8 @@ describe("AuthModuleService - AuthProvider", () => { describe("listAuthProviders", () => { it("should list default AuthProviders registered by loaders", async () => { const authProviders = await service.listAuthProviders() - const serialized = JSON.parse(JSON.stringify(authProviders)) - expect(serialized).toEqual( + expect(authProviders).toEqual( expect.arrayContaining([ expect.objectContaining({ provider: "emailpass", @@ -83,7 +82,7 @@ describe("AuthModuleService - AuthProvider", () => { const { success, error } = await service.authenticate( "emailpass", { - scope: "non-existing", + authScope: "non-existing", } as any ) diff --git a/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts b/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts index 3cd76abcd9408..7e3955d71b9b5 100644 --- a/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts +++ b/packages/auth/integration-tests/__tests__/services/providers/username-password.spec.ts @@ -1,6 +1,6 @@ -import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" import { MedusaModule, Modules } from "@medusajs/modules-sdk" +import { IAuthModuleService } from "@medusajs/types" import { MikroOrmWrapper } from "../../../utils" import Scrypt from "scrypt-kdf" import { SqlEntityManager } from "@mikro-orm/postgresql" @@ -62,6 +62,7 @@ describe("AuthModuleService - AuthProvider", () => { { provider: "emailpass", entity_id: email, + scope: "store", provider_metadata: { password: passwordHash, }, @@ -73,8 +74,8 @@ describe("AuthModuleService - AuthProvider", () => { email: "test@test.com", password: password, }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: true, @@ -92,8 +93,8 @@ describe("AuthModuleService - AuthProvider", () => { const res = await service.authenticate("emailpass", { body: { email: "test@test.com" }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, @@ -106,8 +107,8 @@ describe("AuthModuleService - AuthProvider", () => { const res = await service.authenticate("emailpass", { body: { password: "supersecret" }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, @@ -127,6 +128,7 @@ describe("AuthModuleService - AuthProvider", () => { // Add authenticated user { provider: "emailpass", + scope: "store", entity_id: email, provider_metadata: { password_hash: passwordHash, @@ -139,8 +141,8 @@ describe("AuthModuleService - AuthProvider", () => { email: "test@test.com", password: "password", }, - scope: "store", - }) + authScope: "store", + } as any) expect(res).toEqual({ success: false, diff --git a/packages/auth/package.json b/packages/auth/package.json index ac68f58918135..4516db7edd6ae 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.3.1", "jsonwebtoken": "^9.0.2", "knex": "2.4.2", "scrypt-kdf": "^2.0.1", diff --git a/packages/auth/src/migrations/.snapshot-medusa-authentication.json b/packages/auth/src/migrations/.snapshot-medusa-auth.json similarity index 91% rename from packages/auth/src/migrations/.snapshot-medusa-authentication.json rename to packages/auth/src/migrations/.snapshot-medusa-auth.json index a31fe796e7234..294b0a9ba5d4b 100644 --- a/packages/auth/src/migrations/.snapshot-medusa-authentication.json +++ b/packages/auth/src/migrations/.snapshot-medusa-auth.json @@ -24,20 +24,14 @@ "nullable": false, "mappedType": "text" }, - "domain": { - "name": "domain", + "scope": { + "name": "scope", "type": "text", "unsigned": false, "autoincrement": false, "primary": false, - "nullable": false, - "default": "'all'", - "enumItems": [ - "all", - "store", - "admin" - ], - "mappedType": "enum" + "nullable": true, + "mappedType": "text" }, "config": { "name": "config", @@ -104,6 +98,15 @@ "nullable": true, "mappedType": "text" }, + "scope": { + "name": "scope", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, "user_metadata": { "name": "user_metadata", "type": "jsonb", @@ -119,7 +122,7 @@ "unsigned": false, "autoincrement": false, "primary": false, - "nullable": true, + "nullable": false, "mappedType": "json" }, "provider_metadata": { @@ -136,9 +139,10 @@ "schema": "public", "indexes": [ { - "keyName": "IDX_auth_user_provider_entity_id", + "keyName": "IDX_auth_user_provider_scope_entity_id", "columnNames": [ "provider_id", + "scope", "entity_id" ], "composite": true, diff --git a/packages/auth/src/migrations/Migration20240122041959.ts b/packages/auth/src/migrations/Migration20240122041959.ts deleted file mode 100644 index 15f1526572d4f..0000000000000 --- a/packages/auth/src/migrations/Migration20240122041959.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Migration } from "@mikro-orm/migrations" - -export class Migration20240122041959 extends Migration { - async up(): Promise { - this.addSql( - 'create table if not exists "auth_provider" ("provider" text not null, "name" text not null, "domain" text check ("domain" in (\'all\', \'store\', \'admin\')) not null default \'all\', "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));' - ) - - this.addSql( - 'create table if not exists "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "user_metadata" jsonb null, "app_metadata" jsonb null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));' - ) - this.addSql( - 'alter table "auth_user" add constraint "IDX_auth_user_provider_entity_id" unique ("provider_id", "entity_id");' - ) - - this.addSql( - 'alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;' - ) - } - - async down(): Promise { - this.addSql( - 'alter table "auth_user" drop constraint if exists "auth_user_provider_id_foreign";' - ) - - this.addSql('drop table if exists "auth_provider" cascade;') - - this.addSql('drop table if exists "auth_user" cascade;') - } -} diff --git a/packages/auth/src/migrations/Migration20240201100135.ts b/packages/auth/src/migrations/Migration20240201100135.ts new file mode 100644 index 0000000000000..de100203d9a04 --- /dev/null +++ b/packages/auth/src/migrations/Migration20240201100135.ts @@ -0,0 +1,22 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20240201100135 extends Migration { + + async up(): Promise { + this.addSql('create table "auth_provider" ("provider" text not null, "name" text not null, "scope" text null, "config" jsonb null, "is_active" boolean not null default false, constraint "auth_provider_pkey" primary key ("provider"));'); + + this.addSql('create table "auth_user" ("id" text not null, "entity_id" text not null, "provider_id" text null, "scope" text not null, "user_metadata" jsonb null, "app_metadata" jsonb not null, "provider_metadata" jsonb null, constraint "auth_user_pkey" primary key ("id"));'); + this.addSql('alter table "auth_user" add constraint "IDX_auth_user_provider_scope_entity_id" unique ("provider_id", "scope", "entity_id");'); + + this.addSql('alter table "auth_user" add constraint "auth_user_provider_id_foreign" foreign key ("provider_id") references "auth_provider" ("provider") on delete cascade;'); + } + + async down(): Promise { + this.addSql('alter table "auth_user" drop constraint "auth_user_provider_id_foreign";'); + + this.addSql('drop table if exists "auth_provider" cascade;'); + + this.addSql('drop table if exists "auth_user" cascade;'); + } + +} diff --git a/packages/auth/src/models/auth-provider.ts b/packages/auth/src/models/auth-provider.ts index 0827186069bbd..39ba51823585d 100644 --- a/packages/auth/src/models/auth-provider.ts +++ b/packages/auth/src/models/auth-provider.ts @@ -20,8 +20,8 @@ export default class AuthProvider { @Property({ columnType: "text" }) name: string - @Enum({ items: () => ProviderDomain, default: ProviderDomain.ALL }) - domain: ProviderDomain = ProviderDomain.ALL + @Property({ columnType: "text", nullable: true }) + scope: string @Property({ columnType: "jsonb", nullable: true }) config: Record | null = null diff --git a/packages/auth/src/models/auth-user.ts b/packages/auth/src/models/auth-user.ts index 0c10053175fae..122462c26cdf1 100644 --- a/packages/auth/src/models/auth-user.ts +++ b/packages/auth/src/models/auth-user.ts @@ -17,7 +17,10 @@ import { generateEntityId } from "@medusajs/utils" type OptionalFields = "provider_metadata" | "app_metadata" | "user_metadata" @Entity() -@Unique({ properties: ["provider","entity_id" ], name: "IDX_auth_user_provider_entity_id" }) +@Unique({ + properties: ["provider", "scope", "entity_id"], + name: "IDX_auth_user_provider_scope_entity_id", +}) export default class AuthUser { [OptionalProps]: OptionalFields @@ -34,14 +37,17 @@ export default class AuthUser { }) provider: AuthProvider + @Property({ columnType: "text" }) + scope: string + @Property({ columnType: "jsonb", nullable: true }) user_metadata: Record | null - @Property({ columnType: "jsonb", nullable: true }) - app_metadata: Record | null + @Property({ columnType: "jsonb" }) + app_metadata: Record = {} @Property({ columnType: "jsonb", nullable: true }) - provider_metadata: Record | null + provider_metadata: Record | null = null @BeforeCreate() onCreate() { diff --git a/packages/auth/src/providers/email-password.ts b/packages/auth/src/providers/email-password.ts index 3e9760f991041..da2617ef2a81f 100644 --- a/packages/auth/src/providers/email-password.ts +++ b/packages/auth/src/providers/email-password.ts @@ -1,4 +1,8 @@ -import { AbstractAuthModuleProvider, isString } from "@medusajs/utils" +import { + AbstractAuthModuleProvider, + MedusaError, + isString, +} from "@medusajs/utils" import { AuthenticationInput, AuthenticationResponse } from "@medusajs/types" import { AuthUserService } from "@services" @@ -16,6 +20,17 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider { this.authUserSerivce_ = authUserService } + private getHashConfig(scope: string) { + const scopeConfig = this.scopes_[scope].hashConfig as + | Scrypt.ScryptParams + | undefined + + const defaultHashConfig = { logN: 15, r: 8, p: 1 } + + // Return custom defined hash config or default hash parameters + return scopeConfig ?? defaultHashConfig + } + async authenticate( userData: AuthenticationInput ): Promise { @@ -34,11 +49,38 @@ class EmailPasswordProvider extends AbstractAuthModuleProvider { error: "Email should be a string", } } - - const authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( - email, - EmailPasswordProvider.PROVIDER - ) + let authUser + + try { + authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + email, + EmailPasswordProvider.PROVIDER + ) + } catch (error) { + if (error.type === MedusaError.Types.NOT_FOUND) { + const password_hash = await Scrypt.kdf( + password, + this.getHashConfig(userData.authScope) + ) + + const [createdAuthUser] = await this.authUserSerivce_.create([ + { + entity_id: email, + provider: EmailPasswordProvider.PROVIDER, + scope: userData.authScope, + provider_metadata: { + password: password_hash.toString("base64"), + }, + }, + ]) + + return { + success: true, + authUser: JSON.parse(JSON.stringify(createdAuthUser)), + } + } + return { success: false, error: error.message } + } const password_hash = authUser.provider_metadata?.password diff --git a/packages/auth/src/providers/google.ts b/packages/auth/src/providers/google.ts index 5a94eb992260f..77f7d9d8788eb 100644 --- a/packages/auth/src/providers/google.ts +++ b/packages/auth/src/providers/google.ts @@ -1,20 +1,19 @@ +import { AbstractAuthModuleProvider, MedusaError } from "@medusajs/utils" import { - AbstractAuthModuleProvider, - MedusaError, -} from "@medusajs/utils" -import { - AuthProviderScope, AuthenticationInput, AuthenticationResponse, + AuthProviderScope, + ModulesSdkTypes, } from "@medusajs/types" -import { AuthProviderService, AuthUserService } from "@services" +import { AuthUserService } from "@services" import jwt, { JwtPayload } from "jsonwebtoken" + import { AuthorizationCode } from "simple-oauth2" import url from "url" type InjectedDependencies = { authUserService: AuthUserService - authProviderService: AuthProviderService + authProviderService: ModulesSdkTypes.InternalModuleService } type ProviderConfig = { @@ -27,13 +26,13 @@ class GoogleProvider extends AbstractAuthModuleProvider { public static PROVIDER = "google" public static DISPLAY_NAME = "Google Authentication" - protected readonly authUserSerivce_: AuthUserService - protected readonly authProviderService_: AuthProviderService + protected readonly authUserService_: AuthUserService + protected readonly authProviderService_: ModulesSdkTypes.InternalModuleService constructor({ authUserService, authProviderService }: InjectedDependencies) { super(arguments[0]) - this.authUserSerivce_ = authUserService + this.authUserService_ = authUserService this.authProviderService_ = authProviderService } @@ -78,7 +77,7 @@ class GoogleProvider extends AbstractAuthModuleProvider { const code = req.query?.code ?? req.body?.code - return await this.validateCallbackToken(code, req.scope, config) + return await this.validateCallbackToken(code, req.authScope, config) } // abstractable @@ -91,20 +90,21 @@ class GoogleProvider extends AbstractAuthModuleProvider { let authUser try { - authUser = await this.authUserSerivce_.retrieveByProviderAndEntityId( + authUser = await this.authUserService_.retrieveByProviderAndEntityId( entity_id, GoogleProvider.PROVIDER ) } catch (error) { if (error.type === MedusaError.Types.NOT_FOUND) { - authUser = await this.authUserSerivce_.create([ + const [createdAuthUser] = await this.authUserService_.create([ { entity_id, - provider_id: GoogleProvider.PROVIDER, + provider: GoogleProvider.PROVIDER, user_metadata: jwtData!.payload, - app_metadata: { scope }, + scope, }, ]) + authUser = createdAuthUser } else { return { success: false, error: error.message } } @@ -135,24 +135,20 @@ class GoogleProvider extends AbstractAuthModuleProvider { } } - private getConfigFromScope(config: AuthProviderScope): ProviderConfig { - const providerConfig: Partial = {} + private getConfigFromScope( + config: AuthProviderScope & Partial + ): ProviderConfig { + const providerConfig: Partial = { ...config } - if (config.clientId) { - providerConfig.clientID = config.clientId - } else { + if (!providerConfig.clientID) { throw new Error("Google clientID is required") } - if (config.clientSecret) { - providerConfig.clientSecret = config.clientSecret - } else { + if (!providerConfig.clientSecret) { throw new Error("Google clientSecret is required") } - if (config.callbackURL) { - providerConfig.callbackURL = config.callbackUrl - } else { + if (!providerConfig.callbackURL) { throw new Error("Google callbackUrl is required") } @@ -160,9 +156,8 @@ class GoogleProvider extends AbstractAuthModuleProvider { } private originalURL(req: AuthenticationInput) { - const tls = req.connection.encrypted const host = req.headers.host - const protocol = tls ? "https" : "http" + const protocol = req.protocol const path = req.url || "" return protocol + "://" + host + path @@ -173,7 +168,7 @@ class GoogleProvider extends AbstractAuthModuleProvider { ): Promise { await this.authProviderService_.retrieve(GoogleProvider.PROVIDER) - const scopeConfig = this.scopes_[req.scope] + const scopeConfig = this.scopes_[req.authScope] const config = this.getConfigFromScope(scopeConfig) diff --git a/packages/auth/src/services/auth-module.ts b/packages/auth/src/services/auth-module.ts index 3310c28c1f407..995ec3b734dc4 100644 --- a/packages/auth/src/services/auth-module.ts +++ b/packages/auth/src/services/auth-module.ts @@ -1,35 +1,35 @@ import jwt from "jsonwebtoken" import { + AuthenticationInput, + AuthenticationResponse, AuthProviderDTO, AuthTypes, AuthUserDTO, - AuthenticationInput, - AuthenticationResponse, Context, CreateAuthProviderDTO, CreateAuthUserDTO, DAL, - FilterableAuthProviderProps, - FilterableAuthUserProps, - FindConfig, InternalModuleDeclaration, JWTGenerationOptions, - MedusaContainer, ModuleJoinerConfig, + ModulesSdkTypes, UpdateAuthUserDTO, } from "@medusajs/types" + +import { AuthProvider, AuthUser } from "@models" + +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" + import { AbstractAuthModuleProvider, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" -import { AuthProvider, AuthUser } from "@models" -import { AuthProviderService, AuthUserService } from "@services" import { ServiceTypes } from "@types" -import { joinerConfig } from "../joiner-config" type AuthModuleOptions = { jwt_secret: string @@ -42,28 +42,32 @@ type AuthJWTPayload = { type InjectedDependencies = { baseRepository: DAL.RepositoryService - authUserService: AuthUserService - authProviderService: AuthProviderService + authUserService: ModulesSdkTypes.InternalModuleService + authProviderService: ModulesSdkTypes.InternalModuleService } +const generateMethodForModels = [AuthProvider, AuthUser] + export default class AuthModuleService< - TAuthUser extends AuthUser = AuthUser, - TAuthProvider extends AuthProvider = AuthProvider -> implements AuthTypes.IAuthModuleService + TAuthUser extends AuthUser = AuthUser, + TAuthProvider extends AuthProvider = AuthProvider + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + AuthTypes.AuthProviderDTO, + { + AuthUser: { dto: AuthUserDTO } + AuthProvider: { dto: AuthProviderDTO } + } + >(AuthProvider, generateMethodForModels, entityNameToLinkableKeysMap) + implements AuthTypes.IAuthModuleService { - __joinerConfig(): ModuleJoinerConfig { - return joinerConfig - } - __hooks = { onApplicationStart: async () => await this.createProvidersOnLoad(), } - - protected __container__: MedusaContainer protected baseRepository_: DAL.RepositoryService - - protected authUserService_: AuthUserService - protected authProviderService_: AuthProviderService + protected authUserService_: ModulesSdkTypes.InternalModuleService + protected authProviderService_: ModulesSdkTypes.InternalModuleService protected options_: AuthModuleOptions constructor( @@ -75,66 +79,17 @@ export default class AuthModuleService< options: AuthModuleOptions, protected readonly moduleDeclaration: InternalModuleDeclaration ) { - this.__container__ = arguments[0] + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.authUserService_ = authUserService this.authProviderService_ = authProviderService this.options_ = options } - async retrieveAuthProvider( - provider: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authProvider = await this.authProviderService_.retrieve( - provider, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authProvider, - { populate: true } - ) - } - - async listAuthProviders( - filters: FilterableAuthProviderProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authProviders = await this.authProviderService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authProviders, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountAuthProviders( - filters: FilterableAuthProviderProps = {}, - config: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise<[AuthTypes.AuthProviderDTO[], number]> { - const [authProviders, count] = await this.authProviderService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - authProviders, - { populate: true } - ), - count, - ] + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig } async generateJwtToken( @@ -205,18 +160,11 @@ export default class AuthModuleService< return Array.isArray(data) ? serializedProviders : serializedProviders[0] } - @InjectTransactionManager("baseRepository_") - protected async createAuthProviders_( - data: any[], - @MedusaContext() sharedContext: Context - ): Promise { - return await this.authProviderService_.create(data, sharedContext) - } - updateAuthProvider( data: AuthTypes.UpdateAuthProviderDTO[], sharedContext?: Context ): Promise + updateAuthProvider( data: AuthTypes.UpdateAuthProviderDTO, sharedContext?: Context @@ -247,78 +195,11 @@ export default class AuthModuleService< return await this.authProviderService_.update(data, sharedContext) } - @InjectTransactionManager("baseRepository_") - async deleteAuthProvider( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.authProviderService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveAuthUser( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authUser = await this.authUserService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authUser, - { - exclude: ["password_hash"], - } - ) - } - - @InjectManager("baseRepository_") - async listAuthUsers( - filters: FilterableAuthUserProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const authUsers = await this.authUserService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - authUsers, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountAuthUsers( - filters: FilterableAuthUserProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[AuthUserDTO[], number]> { - const [authUsers, count] = await this.authUserService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize(authUsers, { - populate: true, - }), - count, - ] - } - createAuthUser( data: CreateAuthUserDTO[], sharedContext?: Context ): Promise + createAuthUser( data: CreateAuthUserDTO, sharedContext?: Context @@ -342,23 +223,17 @@ export default class AuthModuleService< return Array.isArray(data) ? serializedUsers : serializedUsers[0] } - @InjectTransactionManager("baseRepository_") - protected async createAuthUsers_( - data: CreateAuthUserDTO[], - @MedusaContext() sharedContext: Context - ): Promise { - return await this.authUserService_.create(data, sharedContext) - } - updateAuthUser( data: UpdateAuthUserDTO[], sharedContext?: Context ): Promise + updateAuthUser( data: UpdateAuthUserDTO, sharedContext?: Context ): Promise + // TODO: should be pluralized, see convention about the methods naming or the abstract module service interface definition @engineering @InjectManager("baseRepository_") async updateAuthUser( data: UpdateAuthUserDTO | UpdateAuthUserDTO[], @@ -385,17 +260,9 @@ export default class AuthModuleService< return await this.authUserService_.update(data, sharedContext) } - @InjectTransactionManager("baseRepository_") - async deleteAuthUser( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.authUserService_.delete(ids, sharedContext) - } - protected getRegisteredAuthenticationProvider( provider: string, - { scope }: AuthenticationInput + { authScope }: AuthenticationInput ): AbstractAuthModuleProvider { let containerProvider: AbstractAuthModuleProvider try { @@ -407,7 +274,7 @@ export default class AuthModuleService< ) } - containerProvider.validateScope(scope) + containerProvider.validateScope(authScope) return containerProvider } @@ -448,6 +315,22 @@ export default class AuthModuleService< } } + @InjectTransactionManager("baseRepository_") + protected async createAuthProviders_( + data: any[], + @MedusaContext() sharedContext: Context + ): Promise { + return await this.authProviderService_.create(data, sharedContext) + } + + @InjectTransactionManager("baseRepository_") + protected async createAuthUsers_( + data: CreateAuthUserDTO[], + @MedusaContext() sharedContext: Context + ): Promise { + return await this.authUserService_.create(data, sharedContext) + } + private async createProvidersOnLoad() { const providersToLoad = this.__container__["auth_providers"] diff --git a/packages/auth/src/services/auth-provider.ts b/packages/auth/src/services/auth-provider.ts deleted file mode 100644 index 241fb56e800df..0000000000000 --- a/packages/auth/src/services/auth-provider.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { AuthProvider } from "@models" - -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - authProviderRepository: DAL.RepositoryService -} - -export default class AuthProviderService< - TEntity extends AuthProvider = AuthProvider -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateAuthProviderDTO - update: ServiceTypes.UpdateAuthProviderDTO - } ->(AuthProvider) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/auth/src/services/auth-user.ts b/packages/auth/src/services/auth-user.ts index 7625415da9360..a8e6fcc264df4 100644 --- a/packages/auth/src/services/auth-user.ts +++ b/packages/auth/src/services/auth-user.ts @@ -1,4 +1,10 @@ -import { AuthTypes, Context, DAL, FindConfig } from "@medusajs/types" +import { + AuthTypes, + Context, + DAL, + FindConfig, + RepositoryService, +} from "@medusajs/types" import { InjectManager, MedusaContext, @@ -6,7 +12,6 @@ import { ModulesSdkUtils, } from "@medusajs/utils" import { AuthUser } from "@models" -import { ServiceTypes, RepositoryTypes } from "@types" type InjectedDependencies = { authUserRepository: DAL.RepositoryService @@ -14,13 +19,11 @@ type InjectedDependencies = { export default class AuthUserService< TEntity extends AuthUser = AuthUser -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateAuthUserDTO - } ->(AuthUser) { - protected readonly authUserRepository_: RepositoryTypes.IAuthUserRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + AuthUser +) { + protected readonly authUserRepository_: RepositoryService + constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) @@ -28,9 +31,7 @@ export default class AuthUserService< } @InjectManager("authUserRepository_") - async retrieveByProviderAndEntityId< - TEntityMethod = AuthTypes.AuthUserDTO - >( + async retrieveByProviderAndEntityId( entityId: string, provider: string, config: FindConfig = {}, diff --git a/packages/auth/src/services/index.ts b/packages/auth/src/services/index.ts index 03a3c0933defc..547d1a5466ca0 100644 --- a/packages/auth/src/services/index.ts +++ b/packages/auth/src/services/index.ts @@ -1,3 +1,2 @@ export { default as AuthModuleService } from "./auth-module" -export { default as AuthProviderService } from "./auth-provider" export { default as AuthUserService } from "./auth-user" diff --git a/packages/auth/src/types/repositories/index.ts b/packages/auth/src/types/repositories/index.ts index 86ed73825729d..b4282c985c6a3 100644 --- a/packages/auth/src/types/repositories/index.ts +++ b/packages/auth/src/types/repositories/index.ts @@ -1,28 +1,2 @@ -import { AuthProvider, AuthUser } from "@models" -import { CreateAuthProviderDTO, UpdateAuthProviderDTO } from "./auth-provider" -import { DAL } from "@medusajs/types" -import { CreateAuthUserDTO, UpdateAuthUserDTO } from "./auth-user" - export * from "./auth-user" export * from "./auth-provider" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAuthProviderRepository< - TEntity extends AuthProvider = AuthProvider -> extends DAL.RepositoryService< - TEntity, - { - create: CreateAuthProviderDTO - update: UpdateAuthProviderDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAuthUserRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateAuthUserDTO - update: UpdateAuthUserDTO - } - > {} diff --git a/packages/auth/src/types/services/auth-provider.ts b/packages/auth/src/types/services/auth-provider.ts index 8ef5d9b3b9bdd..dc400222e43a1 100644 --- a/packages/auth/src/types/services/auth-provider.ts +++ b/packages/auth/src/types/services/auth-provider.ts @@ -1,7 +1,7 @@ export type AuthProviderDTO = { provider: string name: string - domain: ProviderDomain + scope: string is_active: boolean config: Record } @@ -9,7 +9,7 @@ export type AuthProviderDTO = { export type CreateAuthProviderDTO = { provider: string name: string - domain?: ProviderDomain + scope?: string is_active?: boolean config?: Record } @@ -17,15 +17,8 @@ export type CreateAuthProviderDTO = { export type UpdateAuthProviderDTO = { provider: string name?: string - domain?: ProviderDomain is_active?: boolean config?: Record } -export enum ProviderDomain { - ALL = "all", - STORE = "store", - ADMIN = "admin", -} - export type FilterableAuthProviderProps = {} diff --git a/packages/auth/src/types/services/auth-user.ts b/packages/auth/src/types/services/auth-user.ts index c059e980f8b9c..bab3e0c2e4f9d 100644 --- a/packages/auth/src/types/services/auth-user.ts +++ b/packages/auth/src/types/services/auth-user.ts @@ -4,6 +4,7 @@ export type AuthUserDTO = { id: string provider_id: string entity_id: string + scope: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -12,7 +13,8 @@ export type AuthUserDTO = { export type CreateAuthUserDTO = { entity_id: string - provider_id: string + provider: string + scope: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record diff --git a/packages/cart/src/services/address.ts b/packages/cart/src/services/address.ts deleted file mode 100644 index 383a07707bc37..0000000000000 --- a/packages/cart/src/services/address.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Address } from "@models" -import { CreateAddressDTO, UpdateAddressDTO } from "@types" - -type InjectedDependencies = { - addressRepository: DAL.RepositoryService -} - -export default class AddressService< - TEntity extends Address = Address -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } ->(Address) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/cart-module.ts b/packages/cart/src/services/cart-module.ts index e3eaff368aa6f..79274eed6b21d 100644 --- a/packages/cart/src/services/cart-module.ts +++ b/packages/cart/src/services/cart-module.ts @@ -3,20 +3,22 @@ import { Context, DAL, FilterableLineItemTaxLineProps, - FindConfig, ICartModuleService, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, - MedusaContext, - MedusaError, isObject, isString, + MedusaContext, + MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" import { + Address, Cart, LineItem, LineItemAdjustment, @@ -25,32 +27,73 @@ import { ShippingMethodAdjustment, ShippingMethodTaxLine, } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" -import { joinerConfig } from "../joiner-config" -import * as services from "../services" +import { + CreateLineItemDTO, + CreateLineItemTaxLineDTO, + CreateShippingMethodDTO, + CreateShippingMethodTaxLineDTO, + UpdateLineItemDTO, + UpdateLineItemTaxLineDTO, + UpdateShippingMethodTaxLineDTO, +} from "@types" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - cartService: services.CartService - addressService: services.AddressService - lineItemService: services.LineItemService - shippingMethodAdjustmentService: services.ShippingMethodAdjustmentService - shippingMethodService: services.ShippingMethodService - lineItemAdjustmentService: services.LineItemAdjustmentService - lineItemTaxLineService: services.LineItemTaxLineService - shippingMethodTaxLineService: services.ShippingMethodTaxLineService + cartService: ModulesSdkTypes.InternalModuleService + addressService: ModulesSdkTypes.InternalModuleService + lineItemService: ModulesSdkTypes.InternalModuleService + shippingMethodAdjustmentService: ModulesSdkTypes.InternalModuleService + shippingMethodService: ModulesSdkTypes.InternalModuleService + lineItemAdjustmentService: ModulesSdkTypes.InternalModuleService + lineItemTaxLineService: ModulesSdkTypes.InternalModuleService + shippingMethodTaxLineService: ModulesSdkTypes.InternalModuleService } -export default class CartModuleService implements ICartModuleService { +const generateMethodForModels = [ + Address, + LineItem, + LineItemAdjustment, + LineItemTaxLine, + ShippingMethod, + ShippingMethodAdjustment, + ShippingMethodTaxLine, +] + +export default class CartModuleService< + TCart extends Cart = Cart, + TAddress extends Address = Address, + TLineItem extends LineItem = LineItem, + TLineItemAdjustment extends LineItemAdjustment = LineItemAdjustment, + TLineItemTaxLine extends LineItemTaxLine = LineItemTaxLine, + TShippingMethodAdjustment extends ShippingMethodAdjustment = ShippingMethodAdjustment, + TShippingMethodTaxLine extends ShippingMethodTaxLine = ShippingMethodTaxLine, + TShippingMethod extends ShippingMethod = ShippingMethod + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + CartTypes.CartDTO, + { + Address: { dto: CartTypes.CartAddressDTO } + LineItem: { dto: CartTypes.CartLineItemDTO } + LineItemAdjustment: { dto: CartTypes.LineItemAdjustmentDTO } + LineItemTaxLine: { dto: CartTypes.LineItemTaxLineDTO } + ShippingMethod: { dto: CartTypes.CartShippingMethodDTO } + ShippingMethodAdjustment: { dto: CartTypes.ShippingMethodAdjustmentDTO } + ShippingMethodTaxLine: { dto: CartTypes.ShippingMethodTaxLineDTO } + } + >(Cart, generateMethodForModels, entityNameToLinkableKeysMap) + implements ICartModuleService +{ protected baseRepository_: DAL.RepositoryService - protected cartService_: services.CartService - protected addressService_: services.AddressService - protected lineItemService_: services.LineItemService - protected shippingMethodAdjustmentService_: services.ShippingMethodAdjustmentService - protected shippingMethodService_: services.ShippingMethodService - protected lineItemAdjustmentService_: services.LineItemAdjustmentService - protected lineItemTaxLineService_: services.LineItemTaxLineService - protected shippingMethodTaxLineService_: services.ShippingMethodTaxLineService + protected cartService_: ModulesSdkTypes.InternalModuleService + protected addressService_: ModulesSdkTypes.InternalModuleService + protected lineItemService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodAdjustmentService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodService_: ModulesSdkTypes.InternalModuleService + protected lineItemAdjustmentService_: ModulesSdkTypes.InternalModuleService + protected lineItemTaxLineService_: ModulesSdkTypes.InternalModuleService + protected shippingMethodTaxLineService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -66,6 +109,9 @@ export default class CartModuleService implements ICartModuleService { }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.cartService_ = cartService this.addressService_ = addressService @@ -81,52 +127,6 @@ export default class CartModuleService implements ICartModuleService { return joinerConfig } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const cart = await this.cartService_.retrieve(id, config, sharedContext) - - return await this.baseRepository_.serialize(cart, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: CartTypes.FilterableCartProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const carts = await this.cartService_.list(filters, config, sharedContext) - - return this.baseRepository_.serialize(carts, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: CartTypes.FilterableCartProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CartTypes.CartDTO[], number]> { - const [carts, count] = await this.cartService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize(carts, { - populate: true, - }), - count, - ] - } - async create( data: CartTypes.CreateCartDTO[], sharedContext?: Context @@ -229,98 +229,6 @@ export default class CartModuleService implements ICartModuleService { return await this.cartService_.update(data, sharedContext) } - async delete(ids: string[], sharedContext?: Context): Promise - - async delete(ids: string, sharedContext?: Context): Promise - - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const cartIds = Array.isArray(ids) ? ids : [ids] - await this.cartService_.delete(cartIds, sharedContext) - } - - @InjectManager("baseRepository_") - async listAddresses( - filters: CartTypes.FilterableAddressProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const addresses = await this.addressService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - addresses, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async retrieveLineItem( - itemId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const item = await this.lineItemService_.retrieve( - itemId, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - item, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listLineItems( - filters: CartTypes.FilterableLineItemProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const items = await this.lineItemService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - items, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listShippingMethods( - filters: CartTypes.FilterableShippingMethodProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const methods = await this.shippingMethodService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.CartShippingMethodDTO[] - >(methods, { - populate: true, - }) - } - addLineItems( data: CartTypes.CreateLineItemForCartDTO ): Promise @@ -585,18 +493,6 @@ export default class CartModuleService implements ICartModuleService { return await this.addressService_.update(data, sharedContext) } - async deleteAddresses(ids: string[], sharedContext?: Context): Promise - async deleteAddresses(ids: string, sharedContext?: Context): Promise - - @InjectTransactionManager("baseRepository_") - async deleteAddresses( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const addressIds = Array.isArray(ids) ? ids : [ids] - await this.addressService_.delete(addressIds, sharedContext) - } - async addShippingMethods( data: CartTypes.CreateShippingMethodDTO ): Promise @@ -665,7 +561,10 @@ export default class CartModuleService implements ICartModuleService { data: CartTypes.CreateShippingMethodDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { - return await this.shippingMethodService_.create(data, sharedContext) + return await this.shippingMethodService_.create( + data as unknown as CreateShippingMethodDTO[], + sharedContext + ) } async removeShippingMethods( @@ -708,25 +607,6 @@ export default class CartModuleService implements ICartModuleService { await this.shippingMethodService_.delete(toDelete, sharedContext) } - @InjectManager("baseRepository_") - async listLineItemAdjustments( - filters: CartTypes.FilterableLineItemAdjustmentProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const adjustments = await this.lineItemAdjustmentService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.LineItemAdjustmentDTO[] - >(adjustments, { - populate: true, - }) - } - async addLineItemAdjustments( adjustments: CartTypes.CreateLineItemAdjustmentDTO[] ): Promise @@ -882,25 +762,6 @@ export default class CartModuleService implements ICartModuleService { await this.lineItemAdjustmentService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listShippingMethodAdjustments( - filters: CartTypes.FilterableShippingMethodAdjustmentProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const adjustments = await this.shippingMethodAdjustmentService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.ShippingMethodAdjustmentDTO[] - >(adjustments, { - populate: true, - }) - } - @InjectTransactionManager("baseRepository_") async setShippingMethodAdjustments( cartId: string, @@ -1070,26 +931,6 @@ export default class CartModuleService implements ICartModuleService { await this.shippingMethodAdjustmentService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listLineItemTaxLines( - filters: CartTypes.FilterableLineItemTaxLineProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const taxLines = await this.lineItemTaxLineService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - taxLines, - { - populate: true, - } - ) - } - addLineItemTaxLines( taxLines: CartTypes.CreateLineItemTaxLineDTO[] ): Promise @@ -1123,14 +964,14 @@ export default class CartModuleService implements ICartModuleService { const lines = Array.isArray(taxLines) ? taxLines : [taxLines] addedTaxLines = await this.lineItemTaxLineService_.create( - lines as CartTypes.CreateLineItemTaxLineDTO[], + lines as CreateLineItemTaxLineDTO[], sharedContext ) } else { const data = Array.isArray(cartIdOrData) ? cartIdOrData : [cartIdOrData] addedTaxLines = await this.lineItemTaxLineService_.create( - data as CartTypes.CreateLineItemTaxLineDTO[], + data as CreateLineItemTaxLineDTO[], sharedContext ) } @@ -1184,13 +1025,15 @@ export default class CartModuleService implements ICartModuleService { } }) - await this.lineItemTaxLineService_.delete( - toDelete.map((taxLine) => taxLine!.id), - sharedContext - ) + if (toDelete.length) { + await this.lineItemTaxLineService_.delete( + toDelete.map((taxLine) => taxLine!.id), + sharedContext + ) + } const result = await this.lineItemTaxLineService_.upsert( - taxLines, + taxLines as UpdateLineItemTaxLineDTO[], sharedContext ) @@ -1242,25 +1085,6 @@ export default class CartModuleService implements ICartModuleService { await this.lineItemTaxLineService_.delete(ids, sharedContext) } - @InjectManager("baseRepository_") - async listShippingMethodTaxLines( - filters: CartTypes.FilterableShippingMethodTaxLineProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const taxLines = await this.shippingMethodTaxLineService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CartTypes.ShippingMethodTaxLineDTO[] - >(taxLines, { - populate: true, - }) - } - addShippingMethodTaxLines( taxLines: CartTypes.CreateShippingMethodTaxLineDTO[] ): Promise @@ -1296,12 +1120,12 @@ export default class CartModuleService implements ICartModuleService { const lines = Array.isArray(taxLines) ? taxLines : [taxLines] addedTaxLines = await this.shippingMethodTaxLineService_.create( - lines as CartTypes.CreateShippingMethodTaxLineDTO[], + lines as CreateShippingMethodTaxLineDTO[], sharedContext ) } else { addedTaxLines = await this.shippingMethodTaxLineService_.create( - taxLines as CartTypes.CreateShippingMethodTaxLineDTO[], + taxLines as CreateShippingMethodTaxLineDTO[], sharedContext ) } @@ -1367,7 +1191,7 @@ export default class CartModuleService implements ICartModuleService { } const result = await this.shippingMethodTaxLineService_.upsert( - taxLines, + taxLines as UpdateShippingMethodTaxLineDTO[], sharedContext ) diff --git a/packages/cart/src/services/cart.ts b/packages/cart/src/services/cart.ts deleted file mode 100644 index a9c594555b1ca..0000000000000 --- a/packages/cart/src/services/cart.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Cart } from "@models" -import { CreateCartDTO, UpdateCartDTO } from "@types" - -type InjectedDependencies = { - cartRepository: DAL.RepositoryService -} - -export default class CartService< - TEntity extends Cart = Cart -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCartDTO - update: UpdateCartDTO - } ->(Cart) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/index.ts b/packages/cart/src/services/index.ts index 7120b701d7fee..2ed2053ffcb36 100644 --- a/packages/cart/src/services/index.ts +++ b/packages/cart/src/services/index.ts @@ -1,10 +1 @@ -export { default as AddressService } from "./address" -export { default as CartService } from "./cart" export { default as CartModuleService } from "./cart-module" -export { default as LineItemService } from "./line-item" -export { default as LineItemAdjustmentService } from "./line-item-adjustment" -export { default as LineItemTaxLineService } from "./line-item-tax-line" -export { default as ShippingMethodService } from "./shipping-method" -export { default as ShippingMethodAdjustmentService } from "./shipping-method-adjustment" -export { default as ShippingMethodTaxLineService } from "./shipping-method-tax-line" - diff --git a/packages/cart/src/services/line-item-adjustment.ts b/packages/cart/src/services/line-item-adjustment.ts deleted file mode 100644 index 9ee93b2d95820..0000000000000 --- a/packages/cart/src/services/line-item-adjustment.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { LineItemAdjustment } from "@models" -import { - CreateLineItemAdjustmentDTO, - UpdateLineItemAdjustmentDTO, -} from "@types" - -type InjectedDependencies = { - lineItemAdjustmentRepository: DAL.RepositoryService -} - -export default class LineItemAdjustmentService< - TEntity extends LineItemAdjustment = LineItemAdjustment -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateLineItemAdjustmentDTO - update: UpdateLineItemAdjustmentDTO - } ->(LineItemAdjustment) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/line-item-tax-line.ts b/packages/cart/src/services/line-item-tax-line.ts deleted file mode 100644 index a740fae4366d1..0000000000000 --- a/packages/cart/src/services/line-item-tax-line.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { - CreateLineItemTaxLineDTO, - DAL, - UpdateLineItemTaxLineDTO, -} from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { LineItemTaxLine } from "@models" - -type InjectedDependencies = { - lineItemTaxLineRepository: DAL.RepositoryService -} - -export default class LineItemTaxLineService< - TEntity extends LineItemTaxLine = LineItemTaxLine -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateLineItemTaxLineDTO - update: UpdateLineItemTaxLineDTO - } ->(LineItemTaxLine) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/line-item.ts b/packages/cart/src/services/line-item.ts deleted file mode 100644 index ec736f4010249..0000000000000 --- a/packages/cart/src/services/line-item.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { LineItem } from "@models" -import { CreateLineItemDTO, UpdateLineItemDTO } from "@types" - -type InjectedDependencies = { - lineItemRepository: DAL.RepositoryService -} - -export default class LineItemService< - TEntity extends LineItem = LineItem -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateLineItemDTO - update: UpdateLineItemDTO - } ->(LineItem) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method-adjustment.ts b/packages/cart/src/services/shipping-method-adjustment.ts deleted file mode 100644 index 5688cd662babe..0000000000000 --- a/packages/cart/src/services/shipping-method-adjustment.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethodAdjustment } from "@models" -import { - CreateShippingMethodAdjustmentDTO, - UpdateShippingMethodAdjustmentDTO, -} from "@types" - -type InjectedDependencies = { - shippingMethodAdjustmentRepository: DAL.RepositoryService -} - -export default class ShippingMethodAdjustmentService< - TEntity extends ShippingMethodAdjustment = ShippingMethodAdjustment -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodAdjustmentDTO - update: UpdateShippingMethodAdjustmentDTO - } ->(ShippingMethodAdjustment) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method-tax-line.ts b/packages/cart/src/services/shipping-method-tax-line.ts deleted file mode 100644 index 9229d6556717b..0000000000000 --- a/packages/cart/src/services/shipping-method-tax-line.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CreateShippingMethodTaxLineDTO, DAL, UpdateShippingMethodTaxLineDTO } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethodTaxLine } from "@models" - -type InjectedDependencies = { - shippingMethodTaxLineRepository: DAL.RepositoryService -} - -export default class ShippingMethodTaxLineService< - TEntity extends ShippingMethodTaxLine = ShippingMethodTaxLine -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodTaxLineDTO - update: UpdateShippingMethodTaxLineDTO - } ->(ShippingMethodTaxLine) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/services/shipping-method.ts b/packages/cart/src/services/shipping-method.ts deleted file mode 100644 index f3cf6671442ee..0000000000000 --- a/packages/cart/src/services/shipping-method.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ShippingMethod } from "@models" -import { CreateShippingMethodDTO, UpdateShippingMethodDTO } from "../types" - -type InjectedDependencies = { - shippingMethodRepository: DAL.RepositoryService -} - -export default class ShippingMethodService< - TEntity extends ShippingMethod = ShippingMethod -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateShippingMethodDTO - update: UpdateShippingMethodDTO - } ->(ShippingMethod) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/cart/src/types/index.ts b/packages/cart/src/types/index.ts index 03f39f0916b24..9e420fcb7f908 100644 --- a/packages/cart/src/types/index.ts +++ b/packages/cart/src/types/index.ts @@ -5,7 +5,6 @@ export * from "./cart" export * from "./line-item" export * from "./line-item-adjustment" export * from "./line-item-tax-line" -export * from "./repositories" export * from "./shipping-method" export * from "./shipping-method-adjustment" export * from "./shipping-method-tax-line" diff --git a/packages/cart/src/types/repositories.ts b/packages/cart/src/types/repositories.ts deleted file mode 100644 index 3d028f6c8e20a..0000000000000 --- a/packages/cart/src/types/repositories.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { DAL } from "@medusajs/types" -import { - Cart, - LineItem, - LineItemAdjustment, - LineItemTaxLine, - ShippingMethod, - ShippingMethodAdjustment, - ShippingMethodTaxLine, -} from "@models" -import { CreateAddressDTO, UpdateAddressDTO } from "./address" -import { CreateCartDTO, UpdateCartDTO } from "./cart" -import { CreateLineItemDTO, UpdateLineItemDTO } from "./line-item" -import { - CreateLineItemAdjustmentDTO, - UpdateLineItemAdjustmentDTO, -} from "./line-item-adjustment" -import { - CreateLineItemTaxLineDTO, - UpdateLineItemTaxLineDTO, -} from "./line-item-tax-line" -import { - CreateShippingMethodDTO, - UpdateShippingMethodDTO, -} from "./shipping-method" -import { - CreateShippingMethodAdjustmentDTO, - UpdateShippingMethodAdjustmentDTO, -} from "./shipping-method-adjustment" -import { - CreateShippingMethodTaxLineDTO, - UpdateShippingMethodTaxLineDTO, -} from "./shipping-method-tax-line" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IAddressRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICartRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCartDTO - update: UpdateCartDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ILineItemRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemDTO - update: UpdateLineItemDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IShippingMethodRepository< - TEntity extends ShippingMethod = ShippingMethod -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodDTO - update: UpdateShippingMethodDTO - } - > {} - -export interface ILineItemAdjustmentRepository< - TEntity extends LineItemAdjustment = LineItemAdjustment -> extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemAdjustmentDTO - update: UpdateLineItemAdjustmentDTO - } - > {} - -export interface IShippingMethodAdjustmentRepository< - TEntity extends ShippingMethodAdjustment = ShippingMethodAdjustment -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodAdjustmentDTO - update: UpdateShippingMethodAdjustmentDTO - } - > {} - -export interface IShippingMethodTaxLineRepository< - TEntity extends ShippingMethodTaxLine = ShippingMethodTaxLine -> extends DAL.RepositoryService< - TEntity, - { - create: CreateShippingMethodTaxLineDTO - update: UpdateShippingMethodTaxLineDTO - } - > {} - -export interface ILineItemTaxLineRepository< - TEntity extends LineItemTaxLine = LineItemTaxLine -> extends DAL.RepositoryService< - TEntity, - { - create: CreateLineItemTaxLineDTO - update: UpdateLineItemTaxLineDTO - } - > {} diff --git a/packages/cart/src/types/shipping-method.ts b/packages/cart/src/types/shipping-method.ts index 6e70a4056bc0c..6d15473f1b6ea 100644 --- a/packages/cart/src/types/shipping-method.ts +++ b/packages/cart/src/types/shipping-method.ts @@ -1,6 +1,6 @@ export interface CreateShippingMethodDTO { name: string - cart_id: string + shippingMethod_id: string amount: number data?: Record } diff --git a/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts b/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts index b2b074842c701..d9ad730678c8a 100644 --- a/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts +++ b/packages/core-flows/src/customer-group/steps/delete-customer-groups.ts @@ -12,7 +12,7 @@ export const deleteCustomerGroupStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.softDeleteCustomerGroup(ids) + await service.softDeleteCustomerGroups(ids) return new StepResponse(void 0, ids) }, @@ -25,6 +25,6 @@ export const deleteCustomerGroupStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.restoreCustomerGroup(prevCustomerGroups) + await service.restoreCustomerGroups(prevCustomerGroups) } ) diff --git a/packages/core-flows/src/customer-group/steps/update-customer-groups.ts b/packages/core-flows/src/customer-group/steps/update-customer-groups.ts index 553f1d5ae6c09..7acced28ebe40 100644 --- a/packages/core-flows/src/customer-group/steps/update-customer-groups.ts +++ b/packages/core-flows/src/customer-group/steps/update-customer-groups.ts @@ -1,8 +1,8 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { + CustomerGroupUpdatableFields, FilterableCustomerGroupProps, ICustomerModuleService, - CustomerGroupUpdatableFields, } from "@medusajs/types" import { getSelectsAndRelationsFromObjectArray, @@ -31,7 +31,7 @@ export const updateCustomerGroupsStep = createStep( relations, }) - const customers = await service.updateCustomerGroup( + const customers = await service.updateCustomerGroups( data.selector, data.update ) @@ -49,7 +49,7 @@ export const updateCustomerGroupsStep = createStep( await promiseAll( prevCustomerGroups.map((c) => - service.updateCustomerGroup(c.id, { + service.updateCustomerGroups(c.id, { name: c.name, }) ) diff --git a/packages/core-flows/src/customer/steps/create-addresses.ts b/packages/core-flows/src/customer/steps/create-addresses.ts index 139aebf0d248a..a14686069d371 100644 --- a/packages/core-flows/src/customer/steps/create-addresses.ts +++ b/packages/core-flows/src/customer/steps/create-addresses.ts @@ -1,8 +1,8 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, + ICustomerModuleService, } from "@medusajs/types" -import { StepResponse, createStep } from "@medusajs/workflows-sdk" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" export const createCustomerAddressesStepId = "create-customer-addresses" @@ -29,6 +29,6 @@ export const createCustomerAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await service.deleteAddress(ids) + await service.deleteAddresses(ids) } ) diff --git a/packages/core-flows/src/customer/steps/delete-addresses.ts b/packages/core-flows/src/customer/steps/delete-addresses.ts index c6ed1732993ac..46bfcab654cf7 100644 --- a/packages/core-flows/src/customer/steps/delete-addresses.ts +++ b/packages/core-flows/src/customer/steps/delete-addresses.ts @@ -14,7 +14,7 @@ export const deleteCustomerAddressesStep = createStep( const existing = await service.listAddresses({ id: ids, }) - await service.deleteAddress(ids) + await service.deleteAddresses(ids) return new StepResponse(void 0, existing) }, diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts index 50a2b36aca19d..70212f2c51482 100644 --- a/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-billing-addresses.ts @@ -1,12 +1,12 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, - FilterableCustomerAddressProps, CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, } from "@medusajs/types" import { createStep } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { unsetForUpdate, unsetForCreate } from "./utils" +import { unsetForCreate, unsetForUpdate } from "./utils" import { isDefined } from "@medusajs/utils" type StepInput = { @@ -53,7 +53,7 @@ export const maybeUnsetDefaultBillingAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await customerModuleService.updateAddress( + await customerModuleService.updateAddresses( { id: addressesToSet }, { is_default_billing: true } ) diff --git a/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts index 7ffbaf41b0e16..b484c31a5f089 100644 --- a/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts +++ b/packages/core-flows/src/customer/steps/maybe-unset-default-shipping-addresses.ts @@ -1,12 +1,12 @@ import { - ICustomerModuleService, CreateCustomerAddressDTO, - FilterableCustomerAddressProps, CustomerAddressDTO, + FilterableCustomerAddressProps, + ICustomerModuleService, } from "@medusajs/types" import { createStep } from "@medusajs/workflows-sdk" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { unsetForUpdate, unsetForCreate } from "./utils" +import { unsetForCreate, unsetForUpdate } from "./utils" import { isDefined } from "@medusajs/utils" type StepInput = { @@ -52,7 +52,7 @@ export const maybeUnsetDefaultShippingAddressesStep = createStep( ModuleRegistrationName.CUSTOMER ) - await customerModuleService.updateAddress( + await customerModuleService.updateAddresses( { id: addressesToSet }, { is_default_shipping: true } ) diff --git a/packages/core-flows/src/customer/steps/update-addresses.ts b/packages/core-flows/src/customer/steps/update-addresses.ts index 17d7d68be1901..49b793e36b09b 100644 --- a/packages/core-flows/src/customer/steps/update-addresses.ts +++ b/packages/core-flows/src/customer/steps/update-addresses.ts @@ -31,7 +31,7 @@ export const updateCustomerAddressesStep = createStep( relations, }) - const customerAddresses = await service.updateAddress( + const customerAddresses = await service.updateAddresses( data.selector, data.update ) @@ -48,7 +48,7 @@ export const updateCustomerAddressesStep = createStep( ) await promiseAll( - prevCustomerAddresses.map((c) => service.updateAddress(c.id, { ...c })) + prevCustomerAddresses.map((c) => service.updateAddresses(c.id, { ...c })) ) } ) diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts index 25c6e15df2479..870e67a7a416b 100644 --- a/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-create.ts @@ -21,7 +21,7 @@ export const unsetForCreate = async ( [field]: true, }) - await customerService.updateAddress( + await customerService.updateAddresses( { customer_id: customerIds, [field]: true }, { [field]: false } ) diff --git a/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts index aa12bb2122f2b..404b550a9cf5a 100644 --- a/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts +++ b/packages/core-flows/src/customer/steps/utils/unset-address-for-update.ts @@ -28,7 +28,7 @@ export const unsetForUpdate = async ( [field]: true, }) - await customerService.updateAddress( + await customerService.updateAddresses( { customer_id: customerIds, [field]: true }, { [field]: false } ) diff --git a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts index 26beb2c4abf66..ae5236c734d76 100644 --- a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts +++ b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -535,7 +535,7 @@ describe("Customer Module Service", () => { await service.delete(customer.id) - const res = await service.listCustomerGroupRelations({ + const res = await service.listCustomerGroupCustomers({ customer_id: customer.id, customer_group_id: group.id, }) @@ -546,7 +546,7 @@ describe("Customer Module Service", () => { describe("deleteCustomerGroup", () => { it("should delete a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.deleteCustomerGroup(group.id) + await service.deleteCustomerGroups(group.id) await expect( service.retrieveCustomerGroup(group.id) @@ -560,7 +560,7 @@ describe("Customer Module Service", () => { ]) const groupIds = groups.map((group) => group.id) - await service.deleteCustomerGroup(groupIds) + await service.deleteCustomerGroups(groupIds) for (const group of groups) { await expect( @@ -575,7 +575,7 @@ describe("Customer Module Service", () => { await service.createCustomerGroup([{ name: "VIP" }, { name: "Regular" }]) const selector = { name: "VIP" } - await service.deleteCustomerGroup(selector) + await service.deleteCustomerGroups(selector) const remainingGroups = await service.listCustomerGroups({ name: "VIP" }) expect(remainingGroups.length).toBe(0) @@ -595,9 +595,9 @@ describe("Customer Module Service", () => { customer_group_id: group.id, }) - await service.deleteCustomerGroup(group.id) + await service.deleteCustomerGroups(group.id) - const res = await service.listCustomerGroupRelations({ + const res = await service.listCustomerGroupCustomers({ customer_id: customer.id, customer_group_id: group.id, }) @@ -743,7 +743,7 @@ describe("Customer Module Service", () => { address_1: "123 Main St", }) - await service.updateAddress(address.id, { + await service.updateAddresses(address.id, { address_name: "Work", address_1: "456 Main St", }) @@ -778,7 +778,7 @@ describe("Customer Module Service", () => { address_1: "456 Main St", }) - await service.updateAddress( + await service.updateAddresses( { customer_id: customer.id }, { address_name: "Under Construction", @@ -822,7 +822,7 @@ describe("Customer Module Service", () => { }, ]) - await service.updateAddress([address1.id, address2.id], { + await service.updateAddresses([address1.id, address2.id], { address_name: "Under Construction", }) @@ -864,7 +864,7 @@ describe("Customer Module Service", () => { }) await expect( - service.updateAddress(address.id, { is_default_shipping: true }) + service.updateAddresses(address.id, { is_default_shipping: true }) ).rejects.toThrow("A default shipping address already exists") }) }) @@ -1087,7 +1087,7 @@ describe("Customer Module Service", () => { describe("softDeleteCustomerGroup", () => { it("should soft delete a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.softDeleteCustomerGroup([group.id]) + await service.softDeleteCustomerGroups([group.id]) const res = await service.listCustomerGroups({ id: group.id }) expect(res.length).toBe(0) @@ -1105,7 +1105,7 @@ describe("Customer Module Service", () => { { name: "Regular" }, ]) const groupIds = groups.map((group) => group.id) - await service.softDeleteCustomerGroup(groupIds) + await service.softDeleteCustomerGroups(groupIds) const res = await service.listCustomerGroups({ id: groupIds }) expect(res.length).toBe(0) @@ -1121,12 +1121,12 @@ describe("Customer Module Service", () => { describe("restoreCustomerGroup", () => { it("should restore a single customer group", async () => { const [group] = await service.createCustomerGroup([{ name: "VIP" }]) - await service.softDeleteCustomerGroup([group.id]) + await service.softDeleteCustomerGroups([group.id]) const res = await service.listCustomerGroups({ id: group.id }) expect(res.length).toBe(0) - await service.restoreCustomerGroup([group.id]) + await service.restoreCustomerGroups([group.id]) const restoredGroup = await service.retrieveCustomerGroup(group.id, { withDeleted: true, @@ -1140,12 +1140,12 @@ describe("Customer Module Service", () => { { name: "Regular" }, ]) const groupIds = groups.map((group) => group.id) - await service.softDeleteCustomerGroup(groupIds) + await service.softDeleteCustomerGroups(groupIds) const res = await service.listCustomerGroups({ id: groupIds }) expect(res.length).toBe(0) - await service.restoreCustomerGroup(groupIds) + await service.restoreCustomerGroups(groupIds) const restoredGroups = await service.listCustomerGroups( { id: groupIds }, diff --git a/packages/customer/package.json b/packages/customer/package.json index 6f58a6cf6c197..637c791163066 100644 --- a/packages/customer/package.json +++ b/packages/customer/package.json @@ -55,7 +55,7 @@ "@mikro-orm/migrations": "5.9.7", "@mikro-orm/postgresql": "5.9.7", "awilix": "^8.0.0", - "dotenv": "^16.1.4", + "dotenv": "16.3.1", "knex": "2.4.2" } } diff --git a/packages/customer/src/models/address.ts b/packages/customer/src/models/address.ts index 13aacd7c005a5..817d4c2771cc4 100644 --- a/packages/customer/src/models/address.ts +++ b/packages/customer/src/models/address.ts @@ -2,27 +2,32 @@ import { DAL } from "@medusajs/types" import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, + Cascade, Entity, + Index, + ManyToOne, OnInit, OptionalProps, PrimaryKey, Property, - ManyToOne, - Cascade, - Index, } from "@mikro-orm/core" import Customer from "./customer" type OptionalAddressProps = DAL.EntityDateColumns // TODO: To be revisited when more clear +export const UNIQUE_CUSTOMER_SHIPPING_ADDRESS = + "IDX_customer_address_unique_customer_shipping" +export const UNIQUE_CUSTOMER_BILLING_ADDRESS = + "IDX_customer_address_unique_customer_billing" + @Entity({ tableName: "customer_address" }) @Index({ - name: "IDX_customer_address_unique_customer_shipping", + name: UNIQUE_CUSTOMER_SHIPPING_ADDRESS, expression: 'create unique index "IDX_customer_address_unique_customer_shipping" on "customer_address" ("customer_id") where "is_default_shipping" = true', }) @Index({ - name: "IDX_customer_address_unique_customer_billing", + name: UNIQUE_CUSTOMER_BILLING_ADDRESS, expression: 'create unique index "IDX_customer_address_unique_customer_billing" on "customer_address" ("customer_id") where "is_default_billing" = true', }) diff --git a/packages/customer/src/services/address.ts b/packages/customer/src/services/address.ts deleted file mode 100644 index 383a07707bc37..0000000000000 --- a/packages/customer/src/services/address.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Address } from "@models" -import { CreateAddressDTO, UpdateAddressDTO } from "@types" - -type InjectedDependencies = { - addressRepository: DAL.RepositoryService -} - -export default class AddressService< - TEntity extends Address = Address -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateAddressDTO - update: UpdateAddressDTO - } ->(Address) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-group-customer.ts b/packages/customer/src/services/customer-group-customer.ts deleted file mode 100644 index cc61f576af834..0000000000000 --- a/packages/customer/src/services/customer-group-customer.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CustomerGroupCustomer } from "@models" - -type CreateCustomerGroupCustomerDTO = { - customer_id: string - customer_group_id: string - created_by?: string -} - -type InjectedDependencies = { - customerGroupRepository: DAL.RepositoryService -} - -export default class CustomerGroupCustomerService< - TEntity extends CustomerGroupCustomer = CustomerGroupCustomer -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { create: CreateCustomerGroupCustomerDTO } ->(CustomerGroupCustomer) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-group.ts b/packages/customer/src/services/customer-group.ts deleted file mode 100644 index 830e949c58ddb..0000000000000 --- a/packages/customer/src/services/customer-group.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CustomerGroup } from "@models" -import { CreateCustomerGroupDTO, UpdateCustomerGroupDTO } from "@medusajs/types" - -type InjectedDependencies = { - customerGroupRepository: DAL.RepositoryService -} - -export default class CustomerGroupService< - TEntity extends CustomerGroup = CustomerGroup -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCustomerGroupDTO - update: UpdateCustomerGroupDTO - } ->(CustomerGroup) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts index 7a41d481b84f9..49b6422e80a94 100644 --- a/packages/customer/src/services/customer-module.ts +++ b/packages/customer/src/services/customer-module.ts @@ -1,48 +1,69 @@ import { Context, + CustomerDTO, + CustomerTypes, DAL, - FindConfig, ICustomerModuleService, InternalModuleDeclaration, ModuleJoinerConfig, - CustomerTypes, - SoftDeleteReturn, - RestoreReturn, + ModulesSdkTypes, } from "@medusajs/types" import { InjectManager, InjectTransactionManager, - MedusaContext, - mapObjectTo, - isString, - isObject, isDuplicateError, + isString, + MedusaContext, + MedusaError, + ModulesSdkUtils, } from "@medusajs/utils" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" -import * as services from "../services" -import { MedusaError } from "@medusajs/utils" +import { + Address, + Customer, + CustomerGroup, + CustomerGroupCustomer, +} from "@models" import { EntityManager } from "@mikro-orm/core" - -const UNIQUE_CUSTOMER_SHIPPING_ADDRESS = - "IDX_customer_address_unique_customer_shipping" -const UNIQUE_CUSTOMER_BILLING_ADDRESS = - "IDX_customer_address_unique_customer_billing" +import { + UNIQUE_CUSTOMER_BILLING_ADDRESS, + UNIQUE_CUSTOMER_SHIPPING_ADDRESS, +} from "../models/address" type InjectedDependencies = { baseRepository: DAL.RepositoryService - customerService: services.CustomerService - addressService: services.AddressService - customerGroupService: services.CustomerGroupService - customerGroupCustomerService: services.CustomerGroupCustomerService + customerService: ModulesSdkTypes.InternalModuleService + addressService: ModulesSdkTypes.InternalModuleService + customerGroupService: ModulesSdkTypes.InternalModuleService + customerGroupCustomerService: ModulesSdkTypes.InternalModuleService } -export default class CustomerModuleService implements ICustomerModuleService { +const generateMethodForModels = [Address, CustomerGroup, CustomerGroupCustomer] + +export default class CustomerModuleService< + TAddress extends Address = Address, + TCustomer extends Customer = Customer, + TCustomerGroup extends CustomerGroup = CustomerGroup, + TCustomerGroupCustomer extends CustomerGroupCustomer = CustomerGroupCustomer + > + // TODO seb I let you manage that when you are moving forward + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + CustomerDTO, + { + Address: { dto: any } + CustomerGroup: { dto: any } + CustomerGroupCustomer: { dto: any } + } + >(Customer, generateMethodForModels, entityNameToLinkableKeysMap) + implements ICustomerModuleService +{ protected baseRepository_: DAL.RepositoryService - protected customerService_: services.CustomerService - protected addressService_: services.AddressService - protected customerGroupService_: services.CustomerGroupService - protected customerGroupCustomerService_: services.CustomerGroupCustomerService + protected customerService_: ModulesSdkTypes.InternalModuleService + protected addressService_: ModulesSdkTypes.InternalModuleService + protected customerGroupService_: ModulesSdkTypes.InternalModuleService + protected customerGroupCustomerService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -54,6 +75,9 @@ export default class CustomerModuleService implements ICustomerModuleService { }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.customerService_ = customerService this.addressService_ = addressService @@ -65,26 +89,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return joinerConfig } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const customer = await this.customerService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - customer, - { - populate: true, - } - ) - } - async create( data: CustomerTypes.CreateCustomerDTO, sharedContext?: Context @@ -95,13 +99,33 @@ export default class CustomerModuleService implements ICustomerModuleService { sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async create( dataOrArray: | CustomerTypes.CreateCustomerDTO | CustomerTypes.CreateCustomerDTO[], @MedusaContext() sharedContext: Context = {} - ) { + ): Promise { + const customers = await this.create_(dataOrArray, sharedContext).catch( + this.handleDbErrors + ) + + const serialized = await this.baseRepository_.serialize< + CustomerTypes.CustomerDTO[] + >(customers, { + populate: true, + }) + + return Array.isArray(dataOrArray) ? serialized : serialized[0] + } + + @InjectTransactionManager("baseRepository_") + async create_( + dataOrArray: + | CustomerTypes.CreateCustomerDTO + | CustomerTypes.CreateCustomerDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { const data = Array.isArray(dataOrArray) ? dataOrArray : [dataOrArray] const customers = await this.customerService_.create(data, sharedContext) @@ -121,12 +145,7 @@ export default class CustomerModuleService implements ICustomerModuleService { await this.addAddresses(addressDataWithCustomerIds, sharedContext) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerDTO[] - >(customers, { - populate: true, - }) - return Array.isArray(dataOrArray) ? serialized : serialized[0] + return customers as unknown as CustomerTypes.CustomerDTO[] } update( @@ -151,37 +170,38 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.CustomerUpdatableFields, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerDTO[] = [] + let updateData: + | CustomerTypes.UpdateCustomerDTO + | CustomerTypes.UpdateCustomerDTO[] + | { + selector: CustomerTypes.FilterableCustomerProps + data: CustomerTypes.CustomerUpdatableFields + } = [] + if (isString(idsOrSelector)) { - updateData = [ - { - id: idsOrSelector, - ...data, - }, - ] + updateData = { + id: idsOrSelector, + ...data, + } } else if (Array.isArray(idsOrSelector)) { updateData = idsOrSelector.map((id) => ({ id, ...data, })) } else { - const ids = await this.customerService_.list( - idsOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: idsOrSelector, + data: data, + } } const customers = await this.customerService_.update( updateData, sharedContext ) + const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerDTO[] + CustomerTypes.CustomerDTO | CustomerTypes.CustomerDTO[] >(customers, { populate: true, }) @@ -189,78 +209,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return isString(idsOrSelector) ? serialized[0] : serialized } - delete(customerId: string, sharedContext?: Context): Promise - delete(customerIds: string[], sharedContext?: Context): Promise - delete( - selector: CustomerTypes.FilterableCustomerProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async delete( - idsOrSelector: string | string[] | CustomerTypes.FilterableCustomerProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(idsOrSelector) - ? idsOrSelector - : [idsOrSelector as string] - if (isObject(idsOrSelector)) { - const ids = await this.customerService_.list( - idsOrSelector, - { - select: ["id"], - }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - return await this.customerService_.delete(toDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async list( - filters: CustomerTypes.FilterableCustomerProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const customers = await this.customerService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - customers, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: CustomerTypes.FilterableCustomerProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerDTO[], number]> { - const [customers, count] = await this.customerService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - customers, - { - populate: true, - } - ), - count, - ] - } - async createCustomerGroup( dataOrArrayOfData: CustomerTypes.CreateCustomerGroupDTO, sharedContext?: Context @@ -278,55 +226,36 @@ export default class CustomerModuleService implements ICustomerModuleService { | CustomerTypes.CreateCustomerGroupDTO[], @MedusaContext() sharedContext: Context = {} ) { - const data = Array.isArray(dataOrArrayOfData) - ? dataOrArrayOfData - : [dataOrArrayOfData] + const groups = await this.customerGroupService_.create( + dataOrArrayOfData, + sharedContext + ) - const groups = await this.customerGroupService_.create(data, sharedContext) - const serialized = await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupDTO[] + return await this.baseRepository_.serialize< + CustomerTypes.CustomerGroupDTO | CustomerTypes.CustomerGroupDTO[] >(groups, { populate: true, }) - - return Array.isArray(dataOrArrayOfData) ? serialized : serialized[0] } - @InjectManager("baseRepository_") - async retrieveCustomerGroup( - groupId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const group = await this.customerGroupService_.retrieve( - groupId, - config, - sharedContext - ) - return await this.baseRepository_.serialize( - group, - { populate: true } - ) - } - - async updateCustomerGroup( + async updateCustomerGroups( groupId: string, data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - async updateCustomerGroup( + async updateCustomerGroups( groupIds: string[], data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - async updateCustomerGroup( + async updateCustomerGroups( selector: CustomerTypes.FilterableCustomerGroupProps, data: CustomerTypes.CustomerGroupUpdatableFields, sharedContext?: Context ): Promise @InjectTransactionManager("baseRepository_") - async updateCustomerGroup( + async updateCustomerGroups( groupIdOrSelector: | string | string[] @@ -334,29 +263,27 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.CustomerGroupUpdatableFields, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerGroupDTO[] = [] - if (isString(groupIdOrSelector)) { - updateData = [ - { - id: groupIdOrSelector, - ...data, - }, - ] - } else if (Array.isArray(groupIdOrSelector)) { - updateData = groupIdOrSelector.map((id) => ({ + let updateData: + | CustomerTypes.UpdateCustomerGroupDTO + | CustomerTypes.UpdateCustomerGroupDTO[] + | { + selector: CustomerTypes.FilterableCustomerGroupProps + data: CustomerTypes.CustomerGroupUpdatableFields + } = [] + + if (isString(groupIdOrSelector) || Array.isArray(groupIdOrSelector)) { + const groupIdOrSelectorArray = Array.isArray(groupIdOrSelector) + ? groupIdOrSelector + : [groupIdOrSelector] + updateData = groupIdOrSelectorArray.map((id) => ({ id, ...data, })) } else { - const ids = await this.customerGroupService_.list( - groupIdOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: groupIdOrSelector, + data: data, + } } const groups = await this.customerGroupService_.update( @@ -376,39 +303,6 @@ export default class CustomerModuleService implements ICustomerModuleService { >(groups, { populate: true }) } - deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise - deleteCustomerGroup( - groupIds: string[], - sharedContext?: Context - ): Promise - deleteCustomerGroup( - selector: CustomerTypes.FilterableCustomerGroupProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deleteCustomerGroup( - groupIdOrSelector: - | string - | string[] - | CustomerTypes.FilterableCustomerGroupProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(groupIdOrSelector) - ? groupIdOrSelector - : [groupIdOrSelector as string] - if (isObject(groupIdOrSelector)) { - const ids = await this.customerGroupService_.list( - groupIdOrSelector, - { select: ["id"] }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - return await this.customerGroupService_.delete(toDelete, sharedContext) - } - async addCustomerToGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context @@ -425,17 +319,20 @@ export default class CustomerModuleService implements ICustomerModuleService { @MedusaContext() sharedContext: Context = {} ): Promise<{ id: string } | { id: string }[]> { const groupCustomers = await this.customerGroupCustomerService_.create( - Array.isArray(data) ? data : [data], + data, sharedContext ) if (Array.isArray(data)) { - return groupCustomers.map((gc) => ({ id: gc.id })) + return (groupCustomers as unknown as TCustomerGroupCustomer[]).map( + (gc) => ({ id: gc.id }) + ) } - return { id: groupCustomers[0].id } + return { id: groupCustomers.id } } + // TODO: should be createAddresses to conform to the convention async addAddresses( addresses: CustomerTypes.CreateCustomerAddressDTO[], sharedContext?: Context @@ -445,7 +342,7 @@ export default class CustomerModuleService implements ICustomerModuleService { sharedContext?: Context ): Promise - @InjectTransactionManager("baseRepository_") + @InjectManager("baseRepository_") async addAddresses( data: | CustomerTypes.CreateCustomerAddressDTO @@ -454,13 +351,10 @@ export default class CustomerModuleService implements ICustomerModuleService { ): Promise< CustomerTypes.CustomerAddressDTO | CustomerTypes.CustomerAddressDTO[] > { - const addresses = await this.addressService_.create( - Array.isArray(data) ? data : [data], - sharedContext + const addresses = await this.addAddresses_(data, sharedContext).catch( + this.handleDbErrors ) - await this.flush(sharedContext).catch(this.handleDbErrors) - const serialized = await this.baseRepository_.serialize< CustomerTypes.CustomerAddressDTO[] >(addresses, { populate: true }) @@ -472,24 +366,39 @@ export default class CustomerModuleService implements ICustomerModuleService { return serialized[0] } - async updateAddress( + @InjectTransactionManager("baseRepository_") + private async addAddresses_( + data: + | CustomerTypes.CreateCustomerAddressDTO + | CustomerTypes.CreateCustomerAddressDTO[], + @MedusaContext() sharedContext: Context = {} + ) { + const addresses = await this.addressService_.create( + Array.isArray(data) ? data : [data], + sharedContext + ) + + return addresses + } + + async updateAddresses( addressId: string, data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - async updateAddress( + async updateAddresses( addressIds: string[], data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - async updateAddress( + async updateAddresses( selector: CustomerTypes.FilterableCustomerAddressProps, data: CustomerTypes.UpdateCustomerAddressDTO, sharedContext?: Context ): Promise @InjectTransactionManager("baseRepository_") - async updateAddress( + async updateAddresses( addressIdOrSelector: | string | string[] @@ -497,7 +406,12 @@ export default class CustomerModuleService implements ICustomerModuleService { data: CustomerTypes.UpdateCustomerAddressDTO, @MedusaContext() sharedContext: Context = {} ) { - let updateData: CustomerTypes.UpdateCustomerAddressDTO[] = [] + let updateData: + | CustomerTypes.UpdateCustomerAddressDTO[] + | { + selector: CustomerTypes.FilterableCustomerAddressProps + data: CustomerTypes.UpdateCustomerAddressDTO + } = [] if (isString(addressIdOrSelector)) { updateData = [ { @@ -511,15 +425,10 @@ export default class CustomerModuleService implements ICustomerModuleService { ...data, })) } else { - const ids = await this.addressService_.list( - addressIdOrSelector, - { select: ["id"] }, - sharedContext - ) - updateData = ids.map(({ id }) => ({ - id, - ...data, - })) + updateData = { + selector: addressIdOrSelector, + data, + } } const addresses = await this.addressService_.update( @@ -540,78 +449,6 @@ export default class CustomerModuleService implements ICustomerModuleService { return serialized } - async deleteAddress(addressId: string, sharedContext?: Context): Promise - async deleteAddress( - addressIds: string[], - sharedContext?: Context - ): Promise - async deleteAddress( - selector: CustomerTypes.FilterableCustomerAddressProps, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deleteAddress( - addressIdOrSelector: - | string - | string[] - | CustomerTypes.FilterableCustomerAddressProps, - @MedusaContext() sharedContext: Context = {} - ) { - let toDelete = Array.isArray(addressIdOrSelector) - ? addressIdOrSelector - : [addressIdOrSelector as string] - - if (isObject(addressIdOrSelector)) { - const ids = await this.addressService_.list( - addressIdOrSelector, - { select: ["id"] }, - sharedContext - ) - toDelete = ids.map(({ id }) => id) - } - - await this.addressService_.delete(toDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async listAddresses( - filters?: CustomerTypes.FilterableCustomerAddressProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const addresses = await this.addressService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerAddressDTO[] - >(addresses, { populate: true }) - } - - @InjectManager("baseRepository_") - async listAndCountAddresses( - filters?: CustomerTypes.FilterableCustomerAddressProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerAddressDTO[], number]> { - const [addresses, count] = await this.addressService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - addresses, - { populate: true } - ), - count, - ] - } - async removeCustomerFromGroup( groupCustomerPair: CustomerTypes.GroupCustomerPair, sharedContext?: Context @@ -636,153 +473,6 @@ export default class CustomerModuleService implements ICustomerModuleService { ) } - @InjectManager("baseRepository_") - async listCustomerGroupRelations( - filters?: CustomerTypes.FilterableCustomerGroupCustomerProps, - config?: FindConfig, - @MedusaContext() sharedContext: Context = {} - ) { - const groupCustomers = await this.customerGroupCustomerService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupCustomerDTO[] - >(groupCustomers, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listCustomerGroups( - filters: CustomerTypes.FilterableCustomerGroupProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const groups = await this.customerGroupService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize< - CustomerTypes.CustomerGroupDTO[] - >(groups, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountCustomerGroups( - filters: CustomerTypes.FilterableCustomerGroupProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[CustomerTypes.CustomerGroupDTO[], number]> { - const [groups, count] = await this.customerGroupService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - groups, - { - populate: true, - } - ), - count, - ] - } - - @InjectTransactionManager("baseRepository_") - async softDeleteCustomerGroup< - TReturnableLinkableKeys extends string = string - >( - groupIds: string[], - config: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = - await this.customerGroupService_.softDelete(groupIds, sharedContext) - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restoreCustomerGroup( - groupIds: string[], - config: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerGroupService_.restore( - groupIds, - sharedContext - ) - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async softDelete( - customerIds: string[], - config: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerService_.softDelete( - customerIds, - sharedContext - ) - - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore( - customerIds: string[], - config: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ) { - const [_, cascadedEntitiesMap] = await this.customerService_.restore( - customerIds, - sharedContext - ) - - return config.returnLinkableKeys - ? mapObjectTo>( - cascadedEntitiesMap, - entityNameToLinkableKeysMap, - { - pick: config.returnLinkableKeys, - } - ) - : void 0 - } - private async flush(context: Context) { const em = (context.manager ?? context.transactionManager) as EntityManager await em.flush() diff --git a/packages/customer/src/services/customer.ts b/packages/customer/src/services/customer.ts deleted file mode 100644 index 3aec4769e441d..0000000000000 --- a/packages/customer/src/services/customer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CustomerTypes, DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Customer } from "@models" - -type InjectedDependencies = { - customerRepository: DAL.RepositoryService -} - -export default class CustomerService< - TEntity extends Customer = Customer -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CustomerTypes.CreateCustomerDTO - update: CustomerTypes.UpdateCustomerDTO - } ->(Customer) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/customer/src/services/index.ts b/packages/customer/src/services/index.ts index 07e39a5c1c62d..398679936abf0 100644 --- a/packages/customer/src/services/index.ts +++ b/packages/customer/src/services/index.ts @@ -1,5 +1 @@ -export { default as AddressService } from "./address" -export { default as CustomerGroupService } from "./customer-group" -export { default as CustomerService } from "./customer" export { default as CustomerModuleService } from "./customer-module" -export { default as CustomerGroupCustomerService } from "./customer-group-customer" diff --git a/packages/customer/src/types/index.ts b/packages/customer/src/types/index.ts index e70f89b103e9e..c993481e0d955 100644 --- a/packages/customer/src/types/index.ts +++ b/packages/customer/src/types/index.ts @@ -1,5 +1,8 @@ import { Logger } from "@medusajs/types" -export * from "./address" + +export * as ServiceTypes from "./services" +export * from "./services" + export type InitializeModuleInjectableDependencies = { logger?: Logger } diff --git a/packages/customer/src/types/address.ts b/packages/customer/src/types/services/address.ts similarity index 100% rename from packages/customer/src/types/address.ts rename to packages/customer/src/types/services/address.ts diff --git a/packages/customer/src/types/services/customer-group-customer.ts b/packages/customer/src/types/services/customer-group-customer.ts new file mode 100644 index 0000000000000..f51d3b57abbee --- /dev/null +++ b/packages/customer/src/types/services/customer-group-customer.ts @@ -0,0 +1,5 @@ +export interface CreateCustomerGroupCustomerDTO { + customer_id: string + customer_group_id: string + created_by?: string +} diff --git a/packages/customer/src/types/services/index.ts b/packages/customer/src/types/services/index.ts new file mode 100644 index 0000000000000..c7ac451a5e8be --- /dev/null +++ b/packages/customer/src/types/services/index.ts @@ -0,0 +1,2 @@ +export * from "./address" +export * from "./customer-group-customer" diff --git a/packages/design-system/ui-preset/package.json b/packages/design-system/ui-preset/package.json index 53bb8ebf8f041..22776839f6483 100644 --- a/packages/design-system/ui-preset/package.json +++ b/packages/design-system/ui-preset/package.json @@ -31,7 +31,7 @@ }, "devDependencies": { "@medusajs/toolbox": "^0.0.1", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tsup": "^7.1.0", "typescript": "^5.1.6" }, diff --git a/packages/design-system/ui/package.json b/packages/design-system/ui/package.json index d0b0734d22ba1..9653eff581901 100644 --- a/packages/design-system/ui/package.json +++ b/packages/design-system/ui/package.json @@ -72,7 +72,7 @@ "resize-observer-polyfill": "^1.5.1", "rimraf": "^5.0.1", "storybook": "^7.0.23", - "tailwindcss": "^3.3.2", + "tailwindcss": "^3.4.1", "tsc-alias": "^1.8.7", "typescript": "^5.1.6", "vite": "^4.3.9", diff --git a/packages/design-system/ui/src/components/table/table.tsx b/packages/design-system/ui/src/components/table/table.tsx index 246371cbadf77..40f58e18d9028 100644 --- a/packages/design-system/ui/src/components/table/table.tsx +++ b/packages/design-system/ui/src/components/table/table.tsx @@ -49,7 +49,7 @@ const Cell = React.forwardRef< HTMLTableCellElement, React.HTMLAttributes >(({ className, ...props }, ref) => ( - + )) Cell.displayName = "Table.Cell" @@ -72,7 +72,11 @@ const HeaderCell = React.forwardRef< HTMLTableCellElement, React.TdHTMLAttributes >(({ className, ...props }, ref) => ( - + )) HeaderCell.displayName = "Table.HeaderCell" diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts new file mode 100644 index 0000000000000..54066dce4f3fd --- /dev/null +++ b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/callback/route.ts @@ -0,0 +1,46 @@ +import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../../types/routing" + +import { MedusaError } from "@medusajs/utils" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { scope, authProvider } = req.params + + const service: IAuthModuleService = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + const authData = { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: scope, + protocol: req.protocol, + } as AuthenticationInput + + const authResult = await service.validateCallback(authProvider, authData) + + const { success, error, authUser, location } = authResult + if (location) { + res.redirect(location) + return + } + + if (success) { + req.session.auth_user = authUser + req.session.scope = authUser.scope + + return res.status(200).json({ authUser }) + } + + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Authentication failed" + ) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + await GET(req, res) +} diff --git a/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts new file mode 100644 index 0000000000000..7d3873a320ccd --- /dev/null +++ b/packages/medusa/src/api-v2/auth/[scope]/[authProvider]/route.ts @@ -0,0 +1,46 @@ +import { AuthenticationInput, IAuthModuleService } from "@medusajs/types" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" + +import { MedusaError } from "@medusajs/utils" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const { scope, authProvider } = req.params + + const service: IAuthModuleService = req.scope.resolve( + ModuleRegistrationName.AUTH + ) + + const authData = { + url: req.url, + headers: req.headers, + query: req.query, + body: req.body, + authScope: scope, + protocol: req.protocol, + } as AuthenticationInput + + const authResult = await service.authenticate(authProvider, authData) + + const { success, error, authUser, location } = authResult + if (location) { + res.redirect(location) + return + } + + if (success) { + req.session.auth_user = authUser + req.session.scope = authUser.scope + + return res.status(200).json({ authUser }) + } + + throw new MedusaError( + MedusaError.Types.UNAUTHORIZED, + error || "Authentication failed" + ) +} + +export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + await GET(req, res) +} diff --git a/packages/medusa/src/api-v2/store/customers/me/route.ts b/packages/medusa/src/api-v2/store/customers/me/route.ts index 22a9746d97428..83b654f84cbb5 100644 --- a/packages/medusa/src/api-v2/store/customers/me/route.ts +++ b/packages/medusa/src/api-v2/store/customers/me/route.ts @@ -1,8 +1,9 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { ModuleRegistrationName } from "@medusajs/modules-sdk" + export const GET = async (req: MedusaRequest, res: MedusaResponse) => { - const id = req.auth_user!.app_metadata.customer_id + const id = req.auth_user!.app_metadata?.customer_id const customerModule = req.scope.resolve(ModuleRegistrationName.CUSTOMER) diff --git a/packages/medusa/src/api-v2/store/customers/middlewares.ts b/packages/medusa/src/api-v2/store/customers/middlewares.ts index c60b5f1ea2598..f0bf0e8c2821a 100644 --- a/packages/medusa/src/api-v2/store/customers/middlewares.ts +++ b/packages/medusa/src/api-v2/store/customers/middlewares.ts @@ -7,9 +7,10 @@ import { StorePostCustomersMeAddressesAddressReq, StoreGetCustomersMeAddressesParams, } from "./validators" -import authenticate from "../../../utils/authenticate-middleware" import * as QueryConfig from "./query-config" +import { authenticate } from "../../../utils/authenticate-middleware" + export const storeCustomerRoutesMiddlewares: MiddlewareRoute[] = [ { method: "ALL", diff --git a/packages/medusa/src/api-v2/store/customers/query-config.ts b/packages/medusa/src/api-v2/store/customers/query-config.ts index 5a54fd3bcea0e..7a503843c647f 100644 --- a/packages/medusa/src/api-v2/store/customers/query-config.ts +++ b/packages/medusa/src/api-v2/store/customers/query-config.ts @@ -17,7 +17,7 @@ export const defaultStoreCustomersFields: (keyof CustomerDTO)[] = [ ] export const retrieveTransformQueryConfig = { - defaultFields: defaultStoreCustomersFields, + defaultFields: defaultStoreCustomersFields as string[], defaultRelations: defaultStoreCustomersRelations, allowedRelations: allowedStoreCustomersRelations, isList: false, diff --git a/packages/medusa/src/api-v2/store/customers/route.ts b/packages/medusa/src/api-v2/store/customers/route.ts index b7600fc55dd78..14fb4f1653e91 100644 --- a/packages/medusa/src/api-v2/store/customers/route.ts +++ b/packages/medusa/src/api-v2/store/customers/route.ts @@ -1,8 +1,30 @@ import { MedusaRequest, MedusaResponse } from "../../../types/routing" -import { createCustomerAccountWorkflow } from "@medusajs/core-flows" + +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" import { CreateCustomerDTO } from "@medusajs/types" +import { createCustomerAccountWorkflow } from "@medusajs/core-flows" export const POST = async (req: MedusaRequest, res: MedusaResponse) => { + if (req.auth_user?.app_metadata?.customer_id) { + const remoteQuery = req.scope.resolve( + ContainerRegistrationKeys.REMOTE_QUERY + ) + + const query = remoteQueryObjectFromString({ + entryPoint: "customer", + variables: { id: req.auth_user.app_metadata.customer_id }, + fields: [], + }) + const [customer] = await remoteQuery(query) + + res.status(200).json({ customer }) + + return + } + const createCustomers = createCustomerAccountWorkflow(req.scope) const customersData = req.validatedBody as CreateCustomerDTO @@ -10,5 +32,9 @@ export const POST = async (req: MedusaRequest, res: MedusaResponse) => { input: { customersData, authUserId: req.auth_user!.id }, }) + // Set customer_id on session user if we are in session + if (req.session.auth_user) { + req.session.auth_user.app_metadata.customer_id = result.id + } res.status(200).json({ customer: result }) } diff --git a/packages/medusa/src/types/routing.ts b/packages/medusa/src/types/routing.ts index 8a17b7eee061c..d270d7adc87cc 100644 --- a/packages/medusa/src/types/routing.ts +++ b/packages/medusa/src/types/routing.ts @@ -1,11 +1,13 @@ +import type { Customer, User } from "../models" import type { NextFunction, Request, Response } from "express" -import type { Customer, User } from "../models" +import { AuthUserDTO } from "@medusajs/types" import type { MedusaContainer } from "./global" export interface MedusaRequest extends Request { user?: (User | Customer) & { customer_id?: string; userId?: string } scope: MedusaContainer + session?: any requestId?: string auth_user?: { id: string; app_metadata: Record; scope: string } } diff --git a/packages/medusa/src/utils/authenticate-middleware.ts b/packages/medusa/src/utils/authenticate-middleware.ts index 0417017ef4ce3..ffc2326a09f53 100644 --- a/packages/medusa/src/utils/authenticate-middleware.ts +++ b/packages/medusa/src/utils/authenticate-middleware.ts @@ -1,22 +1,20 @@ -import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { AuthUserDTO, IAuthModuleService } from "@medusajs/types" -import { NextFunction, RequestHandler } from "express" import { MedusaRequest, MedusaResponse } from "../types/routing" +import { NextFunction, RequestHandler } from "express" + +import { ModuleRegistrationName } from "@medusajs/modules-sdk" const SESSION_AUTH = "session" const BEARER_AUTH = "bearer" type MedusaSession = { - auth: { - [authScope: string]: { - user_id: string - } - } + auth_user: AuthUserDTO + scope: string } type AuthType = "session" | "bearer" -export default ( +export const authenticate = ( authScope: string, authType: AuthType | AuthType[], options: { allowUnauthenticated?: boolean } = {} @@ -36,19 +34,18 @@ export default ( let authUser: AuthUserDTO | null = null if (authTypes.includes(SESSION_AUTH)) { - if (session.auth && session.auth[authScope]) { - authUser = await authModule - .retrieveAuthUser(session.auth[authScope].user_id) - .catch(() => null) + if (session.auth_user && session.scope === authScope) { + authUser = session.auth_user } } - if (authTypes.includes(BEARER_AUTH)) { + if (!authUser && authTypes.includes(BEARER_AUTH)) { const authHeader = req.headers.authorization if (authHeader) { const re = /(\S+)\s+(\S+)/ const matches = authHeader.match(re) + // TODO: figure out how to obtain token (and store correct data in token) if (matches) { const tokenType = matches[1] const token = matches[2] diff --git a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index 4aa4ba692695a..4f13d0822f622 100644 --- a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -116,7 +116,7 @@ describe("Payment Module Service", () => { expect(collection.length).toEqual(1) - await service.deletePaymentCollection(["pay-col-id-1"]) + await service.deletePaymentCollections(["pay-col-id-1"]) collection = await service.listPaymentCollections({ id: ["pay-col-id-1"], diff --git a/packages/payment/src/services/index.ts b/packages/payment/src/services/index.ts index 01f593ba08ead..a4f5ea37aea2e 100644 --- a/packages/payment/src/services/index.ts +++ b/packages/payment/src/services/index.ts @@ -1,2 +1 @@ export { default as PaymentModuleService } from "./payment-module" -export { default as PaymentCollectionService } from "./payment-collection" diff --git a/packages/payment/src/services/payment-collection.ts b/packages/payment/src/services/payment-collection.ts deleted file mode 100644 index abeef3e6625e7..0000000000000 --- a/packages/payment/src/services/payment-collection.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PaymentCollection } from "@models" -import { - CreatePaymentCollectionDTO, - DAL, - UpdatePaymentCollectionDTO, -} from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" - -type InjectedDependencies = { - paymentCollectionRepository: DAL.RepositoryService -} - -export default class PaymentCollectionService< - TEntity extends PaymentCollection = PaymentCollection -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePaymentCollectionDTO - update: UpdatePaymentCollectionDTO - } ->(PaymentCollection) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/payment/src/services/payment-module.ts b/packages/payment/src/services/payment-module.ts index 93d4a1310e4e1..1e62e664311eb 100644 --- a/packages/payment/src/services/payment-module.ts +++ b/packages/payment/src/services/payment-module.ts @@ -4,11 +4,10 @@ import { CreatePaymentDTO, CreatePaymentSessionDTO, DAL, - FilterablePaymentCollectionProps, - FindConfig, InternalModuleDeclaration, IPaymentModuleService, ModuleJoinerConfig, + ModulesSdkTypes, PaymentCollectionDTO, PaymentDTO, SetPaymentSessionsDTO, @@ -16,28 +15,64 @@ import { UpdatePaymentDTO, } from "@medusajs/types" import { - InjectManager, InjectTransactionManager, MedusaContext, + ModulesSdkUtils, } from "@medusajs/utils" -import * as services from "@services" - -import { joinerConfig } from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" +import { + Capture, + Payment, + PaymentCollection, + PaymentMethodToken, + PaymentProvider, + PaymentSession, + Refund, +} from "@models" type InjectedDependencies = { baseRepository: DAL.RepositoryService - paymentCollectionService: services.PaymentCollectionService + paymentCollectionService: ModulesSdkTypes.InternalModuleService } -export default class PaymentModuleService implements IPaymentModuleService { +const generateMethodForModels = [ + Capture, + PaymentCollection, + PaymentMethodToken, + PaymentProvider, + PaymentSession, + Refund, +] + +export default class PaymentModuleService< + TPaymentCollection extends PaymentCollection = PaymentCollection + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + // TODO revisit when moving forward frane + InjectedDependencies, + PaymentDTO, + { + Capture: { dto: any } + PaymentCollection: { dto: any } + PaymentMethodToken: { dto: any } + PaymentProvider: { dto: any } + PaymentSession: { dto: any } + Refund: { dto: any } + } + >(Payment, generateMethodForModels, entityNameToLinkableKeysMap) + implements IPaymentModuleService +{ protected baseRepository_: DAL.RepositoryService - protected paymentCollectionService_: services.PaymentCollectionService + protected paymentCollectionService_: ModulesSdkTypes.InternalModuleService constructor( { baseRepository, paymentCollectionService }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.paymentCollectionService_ = paymentCollectionService @@ -105,85 +140,6 @@ export default class PaymentModuleService implements IPaymentModuleService { ) } - deletePaymentCollection( - paymentCollectionId: string[], - sharedContext?: Context - ): Promise - deletePaymentCollection( - paymentCollectionId: string, - sharedContext?: Context - ): Promise - - @InjectTransactionManager("baseRepository_") - async deletePaymentCollection( - ids: string | string[], - @MedusaContext() sharedContext?: Context - ): Promise { - const paymentCollectionIds = Array.isArray(ids) ? ids : [ids] - await this.paymentCollectionService_.delete( - paymentCollectionIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async retrievePaymentCollection( - paymentCollectionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const paymentCollection = await this.paymentCollectionService_.retrieve( - paymentCollectionId, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - paymentCollection, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listPaymentCollections( - filters: FilterablePaymentCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext?: Context - ): Promise { - const paymentCollections = await this.paymentCollectionService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - paymentCollections, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPaymentCollections( - filters: FilterablePaymentCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext?: Context - ): Promise<[PaymentCollectionDTO[], number]> { - const [paymentCollections, count] = - await this.paymentCollectionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - paymentCollections, - { populate: true } - ), - count, - ] - } - /** * TODO */ diff --git a/packages/payment/src/types/repositories.ts b/packages/payment/src/types/repositories.ts deleted file mode 100644 index cc84166e9a0d9..0000000000000 --- a/packages/payment/src/types/repositories.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - DAL, - CreatePaymentCollectionDTO, - UpdatePaymentCollectionDTO, -} from "@medusajs/types" - -import { PaymentCollection } from "@models" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPaymentCollectionRepository< - TEntity extends PaymentCollection = PaymentCollection -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePaymentCollectionDTO - update: UpdatePaymentCollectionDTO - } - > {} diff --git a/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts index 33400946d48b4..cf054ed3aebe3 100644 --- a/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/currency/index.spec.ts @@ -183,7 +183,7 @@ describe("Currency Service", () => { error = e } - expect(error.message).toEqual('"currencyCode" must be defined') + expect(error.message).toEqual("currency - code must be defined") }) it("should return currency based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts index ae6a8cf2b6285..618857e6db566 100644 --- a/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/money-amount/index.spec.ts @@ -258,7 +258,7 @@ describe("MoneyAmount Service", () => { error = e } - expect(error.message).toEqual('"moneyAmountId" must be defined') + expect(error.message).toEqual("moneyAmount - id must be defined") }) it("should return moneyAmount based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts index f677a10fc04de..3e953563ead2d 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-list-rule/index.spec.ts @@ -162,7 +162,7 @@ describe("PriceListRule Service", () => { error = e } - expect(error.message).toEqual('"priceListRuleId" must be defined') + expect(error.message).toEqual("priceListRule - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts index 79c2985044ffe..026bc3186bc1e 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-list/index.spec.ts @@ -157,7 +157,7 @@ describe("PriceList Service", () => { error = e } - expect(error.message).toEqual('"priceListId" must be defined') + expect(error.message).toEqual("priceList - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts index 81c0eb3dff967..e8402dba64cd0 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-rule/index.spec.ts @@ -219,7 +219,7 @@ describe("PriceRule Service", () => { error = e } - expect(error.message).toEqual('"priceRuleId" must be defined') + expect(error.message).toEqual("priceRule - id must be defined") }) it("should return priceRule based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts index 4c80f1a47c46f..8f5848a1d0edf 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-set-money-amonut-rules/index.spec.ts @@ -168,7 +168,7 @@ describe("PriceSetMoneyAmountRules Service", () => { } expect(error.message).toEqual( - '"priceSetMoneyAmountRulesId" must be defined' + "priceSetMoneyAmountRules - id must be defined" ) }) diff --git a/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts index 70ef27e8b4840..161bced693161 100644 --- a/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/price-set/index.spec.ts @@ -305,7 +305,7 @@ describe("PriceSet Service", () => { error = e } - expect(error.message).toEqual('"priceSetId" must be defined') + expect(error.message).toEqual("priceSet - id must be defined") }) it("should return priceSet based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts index 3c76e717b8bf3..ae2865a504880 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/currency.spec.ts @@ -180,7 +180,7 @@ describe("PricingModule Service - Currency", () => { error = e } - expect(error.message).toEqual('"currencyCode" must be defined') + expect(error.message).toEqual("currency - code must be defined") }) it("should return currency based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts index 76adb62a22a17..c1e47a73a9083 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/money-amount.spec.ts @@ -245,7 +245,7 @@ describe("PricingModule Service - MoneyAmount", () => { error = e } - expect(error.message).toEqual('"moneyAmountId" must be defined') + expect(error.message).toEqual("moneyAmount - id must be defined") }) it("should return moneyAmount based on config select param", async () => { @@ -320,7 +320,7 @@ describe("PricingModule Service - MoneyAmount", () => { }) }) - describe("restoreDeletedMoneyAmounts", () => { + describe("restoreMoneyAmounts", () => { const id = "money-amount-USD" it("should restore softDeleted priceSetMoneyAmount and PriceRule when restoring soft-deleting money amount", async () => { @@ -330,7 +330,7 @@ describe("PricingModule Service - MoneyAmount", () => { await createPriceRules(testManager) await createPriceSetMoneyAmountRules(testManager) await service.softDeleteMoneyAmounts([id]) - await service.restoreDeletedMoneyAmounts([id]) + await service.restoreMoneyAmounts([id]) const [moneyAmount] = await service.listMoneyAmounts( { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts index f659615812d0c..713f8fb696d36 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list-rule.spec.ts @@ -171,7 +171,7 @@ describe("PriceListRule Service", () => { error = e } - expect(error.message).toEqual('"priceListRuleId" must be defined') + expect(error.message).toEqual("priceListRule - id must be defined") }) }) @@ -283,7 +283,7 @@ describe("PriceListRule Service", () => { expect(priceList.price_list_rules).toEqual( expect.arrayContaining([ expect.objectContaining({ - rule_type: "rule-type-3", + rule_type: { id: "rule-type-3" }, price_list_rule_values: [ expect.objectContaining({ value: "sc-1" }), ], @@ -323,7 +323,7 @@ describe("PriceListRule Service", () => { expect(priceList.price_list_rules).toEqual( expect.arrayContaining([ expect.objectContaining({ - rule_type: "rule-type-3", + rule_type: { id: "rule-type-3" }, price_list_rule_values: expect.arrayContaining([ expect.objectContaining({ value: "sc-1" }), expect.objectContaining({ value: "sc-2" }), @@ -351,7 +351,7 @@ describe("PriceListRule Service", () => { ) expect(priceList.price_list_rules).toEqual([ - expect.objectContaining({ rule_type: "rule-type-2" }), + expect.objectContaining({ rule_type: { id: "rule-type-2" } }), ]) }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts index 774ceec504608..eb41c50b9f0b3 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-list.spec.ts @@ -179,7 +179,7 @@ describe("PriceList Service", () => { error = e } - expect(error.message).toEqual('"priceListId" must be defined') + expect(error.message).toEqual("priceList - id must be defined") }) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts index d447055216850..7f3a619ec81a1 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-rule.spec.ts @@ -224,7 +224,7 @@ describe("PricingModule Service - PriceRule", () => { error = e } - expect(error.message).toEqual('"priceRuleId" must be defined') + expect(error.message).toEqual("priceRule - id must be defined") }) it("should return PriceRule based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts index 076a0d2c3f483..db74001808c6c 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set-money-amount-rules.spec.ts @@ -192,7 +192,7 @@ describe("PricingModule Service - PriceSetMoneyAmountRules", () => { } expect(error.message).toEqual( - '"priceSetMoneyAmountRulesId" must be defined' + "priceSetMoneyAmountRules - id must be defined" ) }) diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts index 41ffa5c4ddffe..ac6ee27716514 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/price-set.spec.ts @@ -248,7 +248,7 @@ describe("PricingModule Service - PriceSet", () => { error = e } - expect(error.message).toEqual('"priceSetId" must be defined') + expect(error.message).toEqual("priceSet - id must be defined") }) it("should return priceSet based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts index 2011087b97348..2c04056122961 100644 --- a/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/pricing-module/rule-type.spec.ts @@ -170,7 +170,7 @@ describe("PricingModuleService ruleType", () => { error = e } - expect(error.message).toEqual('"ruleTypeId" must be defined') + expect(error.message).toEqual("ruleType - id must be defined") }) it("should return ruleType based on config select param", async () => { diff --git a/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts index a2f038ee968b5..df7431aa38f90 100644 --- a/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts +++ b/packages/pricing/integration-tests/__tests__/services/rule-type/index.spec.ts @@ -164,7 +164,7 @@ describe("RuleType Service", () => { error = e } - expect(error.message).toEqual('"ruleTypeId" must be defined') + expect(error.message).toEqual("ruleType - id must be defined") }) it("should return ruleType based on config select param", async () => { diff --git a/packages/pricing/src/services/__fixtures__/currency.ts b/packages/pricing/src/services/__fixtures__/currency.ts index 27315b54dfdc2..6ecdd890225e3 100644 --- a/packages/pricing/src/services/__fixtures__/currency.ts +++ b/packages/pricing/src/services/__fixtures__/currency.ts @@ -1,6 +1,5 @@ import { Currency } from "@models" -import { CurrencyService } from "@services" -import { asClass, asValue, createContainer } from "awilix" +import { asValue } from "awilix" ;(Currency as any).meta = { /** @@ -10,10 +9,7 @@ import { asClass, asValue, createContainer } from "awilix" } export const nonExistingCurrencyCode = "non-existing-code" -export const mockContainer = createContainer() - -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const currencyRepositoryMock = { currencyRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { code } }) => { if (code === nonExistingCurrencyCode) { @@ -25,5 +21,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - currencyService: asClass(CurrencyService), -}) +} diff --git a/packages/pricing/src/services/__tests__/currency.spec.ts b/packages/pricing/src/services/__tests__/currency.spec.ts index 2c130ec5baedd..46cc0f789c9d4 100644 --- a/packages/pricing/src/services/__tests__/currency.spec.ts +++ b/packages/pricing/src/services/__tests__/currency.spec.ts @@ -1,18 +1,31 @@ import { - mockContainer, + currencyRepositoryMock, nonExistingCurrencyCode, } from "../__fixtures__/currency" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" +import { MedusaContainer } from "@medusajs/types" const code = "existing-currency" describe("Currency service", function () { - beforeEach(function () { + let container: MedusaContainer + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(currencyRepositoryMock) }) it("should retrieve a currency", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") await currencyService.retrieve(code) @@ -33,8 +46,8 @@ describe("Currency service", function () { }) it("should fail to retrieve a currency", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const err = await currencyService .retrieve(nonExistingCurrencyCode) @@ -62,8 +75,8 @@ describe("Currency service", function () { }) it("should list currencys", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = {} const config = { @@ -88,8 +101,8 @@ describe("Currency service", function () { }) it("should list currencys with filters", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { @@ -126,8 +139,8 @@ describe("Currency service", function () { }) it("should list currencys with filters and relations", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { @@ -163,9 +176,9 @@ describe("Currency service", function () { ) }) - it("should list and count the currencys with filters and relations", async function () { - const currencyService = mockContainer.resolve("currencyService") - const currencyRepository = mockContainer.resolve("currencyRepository") + it("should list and count the currencies with filters and relations", async function () { + const currencyService = container.resolve("currencyService") + const currencyRepository = container.resolve("currencyRepository") const filters = { tags: { diff --git a/packages/pricing/src/services/currency.ts b/packages/pricing/src/services/currency.ts deleted file mode 100644 index d7df5abda4613..0000000000000 --- a/packages/pricing/src/services/currency.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Currency } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - currencyRepository: DAL.RepositoryService -} - -export default class CurrencyService< - TEntity extends Currency = Currency -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateCurrencyDTO - update: ServiceTypes.UpdateCurrencyDTO - } ->(Currency) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/index.ts b/packages/pricing/src/services/index.ts index 7e67ff0a5543c..5d08291b40cd5 100644 --- a/packages/pricing/src/services/index.ts +++ b/packages/pricing/src/services/index.ts @@ -1,12 +1,6 @@ -export { default as CurrencyService } from "./currency" -export { default as MoneyAmountService } from "./money-amount" export { default as PriceListService } from "./price-list" export { default as PriceListRuleService } from "./price-list-rule" export { default as PriceListRuleValueService } from "./price-list-rule-value" export { default as PriceRuleService } from "./price-rule" -export { default as PriceSetService } from "./price-set" -export { default as PriceSetMoneyAmountService } from "./price-set-money-amount" -export { default as PriceSetMoneyAmountRulesService } from "./price-set-money-amount-rules" -export { default as PriceSetRuleTypeService } from "./price-set-rule-type" export { default as PricingModuleService } from "./pricing-module" export { default as RuleTypeService } from "./rule-type" diff --git a/packages/pricing/src/services/money-amount.ts b/packages/pricing/src/services/money-amount.ts deleted file mode 100644 index b73422969e25e..0000000000000 --- a/packages/pricing/src/services/money-amount.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { MoneyAmount } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - moneyAmountRepository: DAL.RepositoryService -} - -export default class MoneyAmountService< - TEntity extends MoneyAmount = MoneyAmount -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateMoneyAmountDTO - update: ServiceTypes.UpdateMoneyAmountDTO - }, - { - list: ServiceTypes.FilterableMoneyAmountProps - listAndCount: ServiceTypes.FilterableMoneyAmountProps - } ->(MoneyAmount) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-list-rule-value.ts b/packages/pricing/src/services/price-list-rule-value.ts index 10b5cc9ed37c1..88d4f0241439d 100644 --- a/packages/pricing/src/services/price-list-rule-value.ts +++ b/packages/pricing/src/services/price-list-rule-value.ts @@ -9,26 +9,32 @@ type InjectedDependencies = { export default class PriceListRuleValueService< TEntity extends PriceListRuleValue = PriceListRuleValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - update: ServiceTypes.UpdatePriceListRuleValueDTO - }, - { - list: ServiceTypes.FilterablePriceListRuleValueProps - listAndCount: ServiceTypes.FilterablePriceListRuleValueProps - } ->(PriceListRuleValue) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceListRuleValue +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListRuleValueDTO[], + context: Context + ): Promise + + create( + data: ServiceTypes.CreatePriceListRuleValueDTO, + context: Context + ): Promise + + async create( + data: + | ServiceTypes.CreatePriceListRuleValueDTO + | ServiceTypes.CreatePriceListRuleValueDTO[], context: Context = {} - ): Promise { - const priceListRuleValues = data.map((priceRuleValueData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRuleValues = data_.map((priceRuleValueData) => { const { price_list_rule_id: priceListRuleId, ...priceRuleValue } = priceRuleValueData diff --git a/packages/pricing/src/services/price-list-rule.ts b/packages/pricing/src/services/price-list-rule.ts index cb9ddb7487bd8..438b712b2fa7a 100644 --- a/packages/pricing/src/services/price-list-rule.ts +++ b/packages/pricing/src/services/price-list-rule.ts @@ -9,27 +9,31 @@ type InjectedDependencies = { export default class PriceListRuleService< TEntity extends PriceListRule = PriceListRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceListRuleDTO - update: ServiceTypes.UpdatePriceListRuleDTO - }, - { - list: ServiceTypes.FilterablePriceListRuleProps - listAndCount: ServiceTypes.FilterablePriceListRuleProps - } ->(PriceListRule) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceListRule +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListRuleDTO[], + sharedContext?: Context + ): Promise + create( + data: ServiceTypes.CreatePriceListRuleDTO, + sharedContext?: Context + ): Promise + + async create( + data: + | ServiceTypes.CreatePriceListRuleDTO + | ServiceTypes.CreatePriceListRuleDTO[], context: Context = {} - ): Promise { - const priceListRule = data.map((priceListRule) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRule = data_.map((priceListRule) => { const { price_list_id: priceListId, rule_type_id: ruleTypeId, @@ -50,11 +54,28 @@ export default class PriceListRuleService< return await super.create(priceListRule, context) } - async update( + // @ts-ignore + update( data: ServiceTypes.UpdatePriceListRuleDTO[], + context: Context + ): Promise + + // @ts-ignore + update( + data: ServiceTypes.UpdatePriceListRuleDTO, + context: Context + ): Promise + + // TODO add support for selector? and then rm ts ignore + // @ts-ignore + async update( + data: + | ServiceTypes.UpdatePriceListRuleDTO + | ServiceTypes.UpdatePriceListRuleDTO[], context: Context = {} - ): Promise { - const priceListRules = data.map((priceListRule) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceListRules = data_.map((priceListRule) => { const { price_list_id, rule_type_id, ...priceListRuleData } = priceListRule diff --git a/packages/pricing/src/services/price-list.ts b/packages/pricing/src/services/price-list.ts index c5ddc99398b30..9810a6a8da5c1 100644 --- a/packages/pricing/src/services/price-list.ts +++ b/packages/pricing/src/services/price-list.ts @@ -9,32 +9,45 @@ type InjectedDependencies = { export default class PriceListService< TEntity extends PriceList = PriceList -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - {}, - { - list: ServiceTypes.FilterablePriceListProps - listAndCount: ServiceTypes.FilterablePriceListProps - } ->(PriceList) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceList +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceListDTO[], sharedContext?: Context - ): Promise { - const priceLists = this.normalizePriceListDate(data) + ): Promise + create( + data: ServiceTypes.CreatePriceListDTO, + sharedContext?: Context + ): Promise + + async create( + data: ServiceTypes.CreatePriceListDTO | ServiceTypes.CreatePriceListDTO[], + sharedContext?: Context + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceLists = this.normalizePriceListDate(data_) return await super.create(priceLists, sharedContext) } + // @ts-ignore + update(data: any[], sharedContext?: Context): Promise + // @ts-ignore + update(data: any, sharedContext?: Context): Promise + + // TODO: Add support for selector? and then rm ts ignore + // @ts-ignore async update( - data: ServiceTypes.UpdatePriceListDTO[], + data: ServiceTypes.UpdatePriceListDTO | ServiceTypes.UpdatePriceListDTO[], sharedContext?: Context - ): Promise { - const priceLists = this.normalizePriceListDate(data) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const priceLists = this.normalizePriceListDate(data_) return await super.update(priceLists, sharedContext) } diff --git a/packages/pricing/src/services/price-rule.ts b/packages/pricing/src/services/price-rule.ts index 1c3f365e5c8d7..a0d8ce13ca48f 100644 --- a/packages/pricing/src/services/price-rule.ts +++ b/packages/pricing/src/services/price-rule.ts @@ -10,26 +10,29 @@ type InjectedDependencies = { export default class PriceRuleService< TEntity extends PriceRule = PriceRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - update: ServiceTypes.UpdatePriceRuleDTO - }, - { - list: ServiceTypes.FilterablePriceRuleProps - listAndCount: ServiceTypes.FilterablePriceRuleProps - } ->(PriceRule) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + PriceRule +) { constructor(container: InjectedDependencies) { // @ts-ignore super(...arguments) } - async create( + create( data: ServiceTypes.CreatePriceRuleDTO[], sharedContext?: Context - ): Promise { - const toCreate = data.map((ruleData) => { + ): Promise + create( + data: ServiceTypes.CreatePriceRuleDTO, + sharedContext?: Context + ): Promise + + async create( + data: ServiceTypes.CreatePriceRuleDTO | ServiceTypes.CreatePriceRuleDTO[], + sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const toCreate = data_.map((ruleData) => { const ruleDataClone = { ...ruleData } as any ruleDataClone.rule_type ??= ruleData.rule_type_id diff --git a/packages/pricing/src/services/price-set-money-amount-rules.ts b/packages/pricing/src/services/price-set-money-amount-rules.ts deleted file mode 100644 index a1915770cf8f4..0000000000000 --- a/packages/pricing/src/services/price-set-money-amount-rules.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetMoneyAmountRules } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetMoneyAmountRulesRepository: DAL.RepositoryService -} - -export default class PriceSetMoneyAmountRulesService< - TEntity extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetMoneyAmountRulesDTO - update: ServiceTypes.UpdatePriceSetMoneyAmountRulesDTO - }, - { - list: ServiceTypes.FilterablePriceSetMoneyAmountRulesProps - listAndCount: ServiceTypes.FilterablePriceSetMoneyAmountRulesProps - } ->(PriceSetMoneyAmountRules) { - constructor({ priceSetMoneyAmountRulesRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set-money-amount.ts b/packages/pricing/src/services/price-set-money-amount.ts deleted file mode 100644 index 78644213d605d..0000000000000 --- a/packages/pricing/src/services/price-set-money-amount.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetMoneyAmount } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetMoneyAmountRepository: DAL.RepositoryService -} - -export default class PriceSetMoneyAmountService< - TEntity extends PriceSetMoneyAmount = PriceSetMoneyAmount -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetMoneyAmountDTO - update: ServiceTypes.UpdatePriceSetMoneyAmountDTO - }, - { - list: ServiceTypes.FilterablePriceSetMoneyAmountProps - listAndCount: ServiceTypes.FilterablePriceSetMoneyAmountProps - } ->(PriceSetMoneyAmount) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set-rule-type.ts b/packages/pricing/src/services/price-set-rule-type.ts deleted file mode 100644 index 452050ebd6939..0000000000000 --- a/packages/pricing/src/services/price-set-rule-type.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSetRuleType } from "@models" -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetRuleTypeRepository: DAL.RepositoryService -} - -export default class PriceSetRuleTypeService< - TEntity extends PriceSetRuleType = PriceSetRuleType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreatePriceSetRuleTypeDTO - update: ServiceTypes.UpdatePriceSetRuleTypeDTO - }, - { - list: ServiceTypes.FilterablePriceSetRuleTypeProps - listAndCount: ServiceTypes.FilterablePriceSetRuleTypeProps - } ->(PriceSetRuleType) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/price-set.ts b/packages/pricing/src/services/price-set.ts deleted file mode 100644 index c850f045d77cd..0000000000000 --- a/packages/pricing/src/services/price-set.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PriceSet } from "@models" - -import { ServiceTypes } from "@types" - -type InjectedDependencies = { - priceSetRepository: DAL.RepositoryService -} - -export default class PriceSetService< - TEntity extends PriceSet = PriceSet -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: Omit - update: Omit - }, - { - list: ServiceTypes.FilterablePriceSetProps - listAndCount: ServiceTypes.FilterablePriceSetProps - } ->(PriceSet) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/pricing/src/services/pricing-module.ts b/packages/pricing/src/services/pricing-module.ts index c96c6e639e871..b588915df32ed 100644 --- a/packages/pricing/src/services/pricing-module.ts +++ b/packages/pricing/src/services/pricing-module.ts @@ -4,27 +4,26 @@ import { CreateMoneyAmountDTO, CreatePriceListRuleDTO, DAL, - FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, PriceSetDTO, PricingContext, PricingFilters, PricingRepositoryService, PricingTypes, - RestoreReturn, RuleTypeDTO, } from "@medusajs/types" import { + arrayDifference, + deduplicate, + groupBy, InjectManager, InjectTransactionManager, MedusaContext, MedusaError, + ModulesSdkUtils, PriceListType, - arrayDifference, - deduplicate, - groupBy, - mapObjectTo, removeNullish, } from "@medusajs/utils" @@ -43,66 +42,86 @@ import { } from "@models" import { - CurrencyService, - MoneyAmountService, PriceListRuleService, PriceListRuleValueService, PriceListService, PriceRuleService, - PriceSetMoneyAmountRulesService, - PriceSetMoneyAmountService, - PriceSetRuleTypeService, - PriceSetService, RuleTypeService, } from "@services" -import { ServiceTypes } from "@types" -import { validatePriceListDates } from "@utils" -import { CreatePriceListRuleValueDTO } from "src/types/services" -import { - LinkableKeys, - entityNameToLinkableKeysMap, - joinerConfig, -} from "../joiner-config" +import {entityNameToLinkableKeysMap, joinerConfig} from "../joiner-config" +import {validatePriceListDates} from "@utils" +import {ServiceTypes} from "@types" + type InjectedDependencies = { baseRepository: DAL.RepositoryService pricingRepository: PricingRepositoryService - currencyService: CurrencyService - moneyAmountService: MoneyAmountService - priceSetService: PriceSetService - priceSetMoneyAmountRulesService: PriceSetMoneyAmountRulesService + currencyService: ModulesSdkTypes.InternalModuleService + moneyAmountService: ModulesSdkTypes.InternalModuleService + priceSetService: ModulesSdkTypes.InternalModuleService + priceSetMoneyAmountRulesService: ModulesSdkTypes.InternalModuleService ruleTypeService: RuleTypeService priceRuleService: PriceRuleService - priceSetRuleTypeService: PriceSetRuleTypeService - priceSetMoneyAmountService: PriceSetMoneyAmountService + priceSetRuleTypeService: ModulesSdkTypes.InternalModuleService + priceSetMoneyAmountService: ModulesSdkTypes.InternalModuleService priceListService: PriceListService priceListRuleService: PriceListRuleService priceListRuleValueService: PriceListRuleValueService } +const generateMethodForModels = [ + Currency, + MoneyAmount, + PriceList, + PriceListRule, + PriceListRuleValue, + PriceRule, + PriceSetMoneyAmount, + PriceSetMoneyAmountRules, + PriceSetRuleType, + RuleType, +] + export default class PricingModuleService< - TPriceSet extends PriceSet = PriceSet, - TMoneyAmount extends MoneyAmount = MoneyAmount, - TCurrency extends Currency = Currency, - TRuleType extends RuleType = RuleType, - TPriceSetMoneyAmountRules extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules, - TPriceRule extends PriceRule = PriceRule, - TPriceSetRuleType extends PriceSetRuleType = PriceSetRuleType, - TPriceSetMoneyAmount extends PriceSetMoneyAmount = PriceSetMoneyAmount, - TPriceList extends PriceList = PriceList, - TPriceListRule extends PriceListRule = PriceListRule, - TPriceListRuleValue extends PriceListRuleValue = PriceListRuleValue -> implements PricingTypes.IPricingModuleService + TPriceSet extends PriceSet = PriceSet, + TMoneyAmount extends MoneyAmount = MoneyAmount, + TCurrency extends Currency = Currency, + TRuleType extends RuleType = RuleType, + TPriceSetMoneyAmountRules extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules, + TPriceRule extends PriceRule = PriceRule, + TPriceSetRuleType extends PriceSetRuleType = PriceSetRuleType, + TPriceSetMoneyAmount extends PriceSetMoneyAmount = PriceSetMoneyAmount, + TPriceList extends PriceList = PriceList, + TPriceListRule extends PriceListRule = PriceListRule, + TPriceListRuleValue extends PriceListRuleValue = PriceListRuleValue + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + PricingTypes.PriceSetDTO, + { + Currency: { dto: PricingTypes.CurrencyDTO } + MoneyAmount: { dto: PricingTypes.MoneyAmountDTO } + PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO } + PriceSetMoneyAmountRules: { + dto: PricingTypes.PriceSetMoneyAmountRulesDTO + } + PriceRule: { dto: PricingTypes.PriceRuleDTO } + RuleType: { dto: PricingTypes.RuleTypeDTO } + PriceList: { dto: PricingTypes.PriceListDTO } + PriceListRule: { dto: PricingTypes.PriceListRuleDTO } + } + >(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap) + implements PricingTypes.IPricingModuleService { protected baseRepository_: DAL.RepositoryService protected readonly pricingRepository_: PricingRepositoryService - protected readonly currencyService_: CurrencyService - protected readonly moneyAmountService_: MoneyAmountService + protected readonly currencyService_: ModulesSdkTypes.InternalModuleService + protected readonly moneyAmountService_: ModulesSdkTypes.InternalModuleService protected readonly ruleTypeService_: RuleTypeService - protected readonly priceSetService_: PriceSetService - protected readonly priceSetMoneyAmountRulesService_: PriceSetMoneyAmountRulesService + protected readonly priceSetService_: ModulesSdkTypes.InternalModuleService + protected readonly priceSetMoneyAmountRulesService_: ModulesSdkTypes.InternalModuleService protected readonly priceRuleService_: PriceRuleService - protected readonly priceSetRuleTypeService_: PriceSetRuleTypeService - protected readonly priceSetMoneyAmountService_: PriceSetMoneyAmountService + protected readonly priceSetRuleTypeService_: ModulesSdkTypes.InternalModuleService + protected readonly priceSetMoneyAmountService_: ModulesSdkTypes.InternalModuleService protected readonly priceListService_: PriceListService protected readonly priceListRuleService_: PriceListRuleService protected readonly priceListRuleValueService_: PriceListRuleValueService @@ -125,6 +144,9 @@ export default class PricingModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.pricingRepository_ = pricingRepository this.currencyService_ = currencyService @@ -215,66 +237,6 @@ export default class PricingModuleService< return JSON.parse(JSON.stringify(calculatedPrices)) } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceSet = await this.priceSetService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize(priceSet, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: PricingTypes.FilterablePriceSetProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceSets = await this.priceSetService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceSets, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: PricingTypes.FilterablePriceSetProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetDTO[], number]> { - const [priceSets, count] = await this.priceSetService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceSets, - { - populate: true, - } - ), - count, - ] - } - async create( data: PricingTypes.CreatePriceSetDTO, sharedContext?: Context @@ -743,7 +705,7 @@ export default class PricingModuleService< ) { const priceSets = await this.priceSetService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceSets, { populate: true, @@ -751,77 +713,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceSetService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveMoneyAmount( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const moneyAmount = await this.moneyAmountService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - moneyAmount, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listMoneyAmounts( - filters: PricingTypes.FilterableMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const moneyAmounts = await this.moneyAmountService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - moneyAmounts, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountMoneyAmounts( - filters: PricingTypes.FilterableMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.MoneyAmountDTO[], number]> { - const [moneyAmounts, count] = await this.moneyAmountService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - moneyAmounts, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createMoneyAmounts( data: PricingTypes.CreateMoneyAmountDTO[], @@ -832,7 +723,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( moneyAmounts, { populate: true, @@ -850,7 +741,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( moneyAmounts, { populate: true, @@ -858,109 +749,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteMoneyAmounts( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.moneyAmountService_.delete(ids, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - async softDeleteMoneyAmounts( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.moneyAmountService_.softDelete(ids, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - async restoreDeletedMoneyAmounts< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.moneyAmountService_.restore( - ids, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectManager("baseRepository_") - async retrieveCurrency( - code: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const currency = await this.currencyService_.retrieve( - code, - config, - sharedContext - ) - - return this.baseRepository_.serialize(currency, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listCurrencies( - filters: PricingTypes.FilterableCurrencyProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const currencies = await this.currencyService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - currencies, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCurrencies( - filters: PricingTypes.FilterableCurrencyProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.CurrencyDTO[], number]> { - const [currencies, count] = await this.currencyService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - currencies, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createCurrencies( data: PricingTypes.CreateCurrencyDTO[], @@ -968,7 +756,7 @@ export default class PricingModuleService< ) { const currencies = await this.currencyService_.create(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( currencies, { populate: true, @@ -983,7 +771,7 @@ export default class PricingModuleService< ) { const currencies = await this.currencyService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( currencies, { populate: true, @@ -991,74 +779,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteCurrencies( - currencyCodes: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.currencyService_.delete(currencyCodes, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveRuleType( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const ruleType = await this.ruleTypeService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize(ruleType, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listRuleTypes( - filters: PricingTypes.FilterableRuleTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const ruleTypes = await this.ruleTypeService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - ruleTypes, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountRuleTypes( - filters: PricingTypes.FilterableRuleTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.RuleTypeDTO[], number]> { - const [ruleTypes, count] = await this.ruleTypeService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - ruleTypes, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createRuleTypes( data: PricingTypes.CreateRuleTypeDTO[], @@ -1066,7 +786,7 @@ export default class PricingModuleService< ): Promise { const ruleTypes = await this.ruleTypeService_.create(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( ruleTypes, { populate: true, @@ -1081,7 +801,7 @@ export default class PricingModuleService< ): Promise { const ruleTypes = await this.ruleTypeService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( ruleTypes, { populate: true, @@ -1089,118 +809,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deleteRuleTypes( - ruleTypeIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.ruleTypeService_.delete(ruleTypeIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceSetMoneyAmountRules( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const record = await this.priceSetMoneyAmountRulesService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - record, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceSetMoneyAmountRules( - filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const records = await this.priceSetMoneyAmountRulesService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] - >(records, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountPriceSetMoneyAmountRules( - filters: PricingTypes.FilterablePriceSetMoneyAmountRulesProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetMoneyAmountRulesDTO[], number]> { - const [records, count] = - await this.priceSetMoneyAmountRulesService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountRulesDTO[] - >(records, { - populate: true, - }), - count, - ] - } - - @InjectManager("baseRepository_") - async listPriceSetMoneyAmounts( - filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const records = await this.priceSetMoneyAmountService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountDTO[] - >(records, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async listAndCountPriceSetMoneyAmounts( - filters: PricingTypes.FilterablePriceSetMoneyAmountProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceSetMoneyAmountDTO[], number]> { - const [records, count] = - await this.priceSetMoneyAmountService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize< - PricingTypes.PriceSetMoneyAmountDTO[] - >(records, { - populate: true, - }), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createPriceSetMoneyAmountRules( data: PricingTypes.CreatePriceSetMoneyAmountRulesDTO[], @@ -1211,7 +819,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< PricingTypes.PriceSetMoneyAmountRulesDTO[] >(records, { populate: true, @@ -1228,84 +836,13 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< PricingTypes.PriceSetMoneyAmountRulesDTO[] >(records, { populate: true, }) } - @InjectTransactionManager("baseRepository_") - async deletePriceSetMoneyAmountRules( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceSetMoneyAmountRulesService_.delete(ids, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceRule( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceRule = await this.priceRuleService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceRule, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceRules( - filters: PricingTypes.FilterablePriceRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceRules = await this.priceRuleService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceRules, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceRules( - filters: PricingTypes.FilterablePriceRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceRuleDTO[], number]> { - const [priceRules, count] = await this.priceRuleService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceRules, - { - populate: true, - } - ), - count, - ] - } - @InjectTransactionManager("baseRepository_") async createPriceRules( data: PricingTypes.CreatePriceRuleDTO[], @@ -1316,7 +853,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceRules, { populate: true, @@ -1331,7 +868,7 @@ export default class PricingModuleService< ): Promise { const priceRules = await this.priceRuleService_.update(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceRules, { populate: true, @@ -1339,77 +876,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deletePriceRules( - priceRuleIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceRuleService_.delete(priceRuleIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceList( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceList = await this.priceListService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceList, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceLists( - filters: PricingTypes.FilterablePriceListProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceLists = await this.priceListService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceLists( - filters: PricingTypes.FilterablePriceListProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceListDTO[], number]> { - const [priceLists, count] = await this.priceListService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ), - count, - ] - } - @InjectManager("baseRepository_") async createPriceLists( data: PricingTypes.CreatePriceListDTO[], @@ -1417,7 +883,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.createPriceLists_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1568,7 +1034,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.updatePriceLists_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1694,77 +1160,6 @@ export default class PricingModuleService< return updatedPriceLists } - @InjectTransactionManager("baseRepository_") - async deletePriceLists( - priceListIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceListService_.delete(priceListIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrievePriceListRule( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceList = await this.priceListRuleService_.retrieve( - id, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceList, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listPriceListRules( - filters: PricingTypes.FilterablePriceListRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const priceLists = await this.priceListRuleService_.list( - filters, - config, - sharedContext - ) - - return this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCountPriceListRules( - filters: PricingTypes.FilterablePriceListRuleProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PricingTypes.PriceListRuleDTO[], number]> { - const [priceLists, count] = await this.priceListRuleService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - priceLists, - { - populate: true, - } - ), - count, - ] - } - @InjectManager("baseRepository_") async createPriceListRules( data: PricingTypes.CreatePriceListRuleDTO[], @@ -1772,7 +1167,7 @@ export default class PricingModuleService< ): Promise { const priceLists = await this.createPriceListRules_(data, sharedContext) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1798,7 +1193,7 @@ export default class PricingModuleService< sharedContext ) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -1806,14 +1201,6 @@ export default class PricingModuleService< ) } - @InjectTransactionManager("baseRepository_") - async deletePriceListRules( - priceListRuleIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.priceListRuleService_.delete(priceListRuleIds, sharedContext) - } - @InjectManager("baseRepository_") async addPriceListPrices( data: PricingTypes.AddPriceListPricesDTO[], @@ -2085,14 +1472,16 @@ export default class PricingModuleService< await Promise.all([ this.priceListRuleValueService_.delete( - priceListValuesToDelete.map((p) => p.id) + priceListValuesToDelete.map((p) => p.id), + sharedContext ), this.priceListRuleValueService_.create( - priceListRuleValuesToCreate as CreatePriceListRuleValueDTO[] + priceListRuleValuesToCreate as ServiceTypes.CreatePriceListRuleValueDTO[], + sharedContext ), ]) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, @@ -2154,7 +1543,7 @@ export default class PricingModuleService< await this.priceListRuleService_.delete(idsToDelete) - return this.baseRepository_.serialize( + return await this.baseRepository_.serialize( priceLists, { populate: true, diff --git a/packages/pricing/src/services/rule-type.ts b/packages/pricing/src/services/rule-type.ts index 9bd78752c0a6c..147754ad5a354 100644 --- a/packages/pricing/src/services/rule-type.ts +++ b/packages/pricing/src/services/rule-type.ts @@ -14,17 +14,9 @@ type InjectedDependencies = { export default class RuleTypeService< TEntity extends RuleType = RuleType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ServiceTypes.CreateRuleTypeDTO - update: ServiceTypes.UpdateRuleTypeDTO - }, - { - list: ServiceTypes.FilterableRuleTypeProps - listAndCount: ServiceTypes.FilterableRuleTypeProps - } ->(RuleType) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + RuleType +) { protected readonly ruleTypeRepository_: DAL.RepositoryService constructor({ ruleTypeRepository }: InjectedDependencies) { @@ -33,21 +25,45 @@ export default class RuleTypeService< this.ruleTypeRepository_ = ruleTypeRepository } + create( + data: ServiceTypes.CreateRuleTypeDTO, + sharedContext: Context + ): Promise + create( + data: ServiceTypes.CreateRuleTypeDTO[], + sharedContext: Context + ): Promise + @InjectTransactionManager("ruleTypeRepository_") async create( - data: ServiceTypes.CreateRuleTypeDTO[], + data: ServiceTypes.CreateRuleTypeDTO | ServiceTypes.CreateRuleTypeDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - validateRuleAttributes(data.map((d) => d.rule_attribute)) - return await this.ruleTypeRepository_.create(data, sharedContext) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + validateRuleAttributes(data_.map((d) => d.rule_attribute)) + return await super.create(data, sharedContext) } + // @ts-ignore + update( + data: ServiceTypes.UpdateRuleTypeDTO[], + sharedContext: Context + ): Promise + // @ts-ignore + update( + data: ServiceTypes.UpdateRuleTypeDTO, + sharedContext: Context + ): Promise + @InjectTransactionManager("ruleTypeRepository_") + // TODO: add support for selector? and then rm ts ignore + // @ts-ignore async update( - data: ServiceTypes.UpdateRuleTypeDTO[], + data: ServiceTypes.UpdateRuleTypeDTO | ServiceTypes.UpdateRuleTypeDTO[], @MedusaContext() sharedContext: Context = {} - ): Promise { - validateRuleAttributes(data.map((d) => d.rule_attribute)) - return await this.ruleTypeRepository_.update(data, sharedContext) + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + validateRuleAttributes(data_.map((d) => d.rule_attribute)) + return await super.update(data, sharedContext) } } diff --git a/packages/pricing/src/types/repositories/index.ts b/packages/pricing/src/types/repositories/index.ts index 117d7bda46aa7..a1e262b975d75 100644 --- a/packages/pricing/src/types/repositories/index.ts +++ b/packages/pricing/src/types/repositories/index.ts @@ -1,44 +1,3 @@ -import { - Currency, - MoneyAmount, - PriceList, - PriceListRule, - PriceListRuleValue, - PriceRule, - PriceSet, - PriceSetMoneyAmount, - PriceSetMoneyAmountRules, - PriceSetRuleType, - RuleType, -} from "@models" -import { DAL } from "@medusajs/types" -import { CreateCurrencyDTO, UpdateCurrencyDTO } from "./currency" -import { CreateMoneyAmountDTO, UpdateMoneyAmountDTO } from "./money-amount" -import { - CreatePriceListRuleValueDTO, - UpdatePriceListRuleValueDTO, -} from "./price-list-rule-value" -import { - CreatePriceListRuleDTO, - UpdatePriceListRuleDTO, -} from "./price-list-rule" -import { CreatePriceListDTO, UpdatePriceListDTO } from "./price-list" -import { CreatePriceRuleDTO, UpdatePriceRuleDTO } from "./price-rule" -import { - CreatePriceSetMoneyAmountRulesDTO, - UpdatePriceSetMoneyAmountRulesDTO, -} from "./price-set-money-amount-rules" -import { - CreatePriceSetMoneyAmountDTO, - UpdatePriceSetMoneyAmountDTO, -} from "./price-set-money-amount" -import { - CreatePriceSetRuleTypeDTO, - UpdatePriceSetRuleTypeDTO, -} from "./price-set-rule-type" -import { CreatePriceSetDTO, UpdatePriceSetDTO } from "./price-set" -import { CreateRuleTypeDTO, UpdateRuleTypeDTO } from "./rule-type" - export * from "./currency" export * from "./money-amount" export * from "./price-list-rule-value" @@ -50,119 +9,3 @@ export * from "./price-set-money-amount" export * from "./price-set-rule-type" export * from "./price-set" export * from "./rule-type" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICurrencyRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCurrencyDTO - update: UpdateCurrencyDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IMoneyAmountRepository< - TEntity extends MoneyAmount = MoneyAmount -> extends DAL.RepositoryService< - TEntity, - { - create: CreateMoneyAmountDTO - update: UpdateMoneyAmountDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRuleValueRepository< - TEntity extends PriceListRuleValue = PriceListRuleValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListRuleValueDTO - update: UpdatePriceListRuleValueDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRuleRepository< - TEntity extends PriceListRule = PriceListRule -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListRuleDTO - update: UpdatePriceListRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceListRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceListDTO - update: UpdatePriceListDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceRuleRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceRuleDTO - update: UpdatePriceRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetMoneyAmountRulesRepository< - TEntity extends PriceSetMoneyAmountRules = PriceSetMoneyAmountRules -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetMoneyAmountRulesDTO - update: UpdatePriceSetMoneyAmountRulesDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetMoneyAmountRepository< - TEntity extends PriceSetMoneyAmount = PriceSetMoneyAmount -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetMoneyAmountDTO - update: UpdatePriceSetMoneyAmountDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetRuleTypeRepository< - TEntity extends PriceSetRuleType = PriceSetRuleType -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetRuleTypeDTO - update: UpdatePriceSetRuleTypeDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPriceSetRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePriceSetDTO - update: UpdatePriceSetDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IRuleTypeRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateRuleTypeDTO - update: UpdateRuleTypeDTO - } - > {} diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts index 98dc40be6c6e8..0f415c00c2728 100644 --- a/packages/product/integration-tests/__tests__/services/product-collection/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -240,7 +240,7 @@ describe("Product collection Service", () => { error = e } - expect(error.message).toEqual('"productCollectionId" must be defined') + expect(error.message).toEqual("productCollection - id must be defined") }) it("should return collection based on config select param", async () => { 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 98bbc6418ae0c..e1d9506abdd43 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 @@ -796,7 +796,23 @@ describe("ProductModuleService products", function () { const products = await module.create([data]) + let retrievedProducts = await module.list({ id: products[0].id }) + + expect(retrievedProducts).toHaveLength(1) + expect(retrievedProducts[0].deleted_at).toBeNull() + await module.softDelete([products[0].id]) + + retrievedProducts = await module.list( + { id: products[0].id }, + { + withDeleted: true, + } + ) + + expect(retrievedProducts).toHaveLength(1) + expect(retrievedProducts[0].deleted_at).not.toBeNull() + await module.restore([products[0].id]) const deletedProducts = await module.list( diff --git a/packages/product/integration-tests/__tests__/services/product-option/index.ts b/packages/product/integration-tests/__tests__/services/product-option/index.ts index 3c0a26c0e4781..6f7e05b14e09a 100644 --- a/packages/product/integration-tests/__tests__/services/product-option/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-option/index.ts @@ -205,7 +205,7 @@ describe("ProductOption Service", () => { error = e } - expect(error.message).toEqual('"productOptionId" must be defined') + expect(error.message).toEqual("productOption - id must be defined") }) it("should return option based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts index c0a6ebe5571eb..178f40a7e6ce9 100644 --- a/packages/product/integration-tests/__tests__/services/product-tag/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -237,7 +237,7 @@ describe("ProductTag Service", () => { error = e } - expect(error.message).toEqual('"productTagId" must be defined') + expect(error.message).toEqual("productTag - id must be defined") }) it("should return tag based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-type/index.ts b/packages/product/integration-tests/__tests__/services/product-type/index.ts index 9a878d308e6f0..da07bc92fb30c 100644 --- a/packages/product/integration-tests/__tests__/services/product-type/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-type/index.ts @@ -199,7 +199,7 @@ describe("ProductType Service", () => { error = e } - expect(error.message).toEqual('"productTypeId" must be defined') + expect(error.message).toEqual("productType - id must be defined") }) it("should return type based on config select param", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product-variant/index.ts b/packages/product/integration-tests/__tests__/services/product-variant/index.ts index 8f608d6dd8ecb..43c8229e01402 100644 --- a/packages/product/integration-tests/__tests__/services/product-variant/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-variant/index.ts @@ -320,7 +320,7 @@ describe("ProductVariant Service", () => { error = e } - expect(error.message).toEqual('"productVariantId" must be defined') + expect(error.message).toEqual("productVariant - id must be defined") }) }) }) diff --git a/packages/product/integration-tests/__tests__/services/product/index.ts b/packages/product/integration-tests/__tests__/services/product/index.ts index c4aaaddb1c264..b64de6ae6f98d 100644 --- a/packages/product/integration-tests/__tests__/services/product/index.ts +++ b/packages/product/integration-tests/__tests__/services/product/index.ts @@ -78,7 +78,7 @@ describe("Product Service", () => { error = e } - expect(error.message).toEqual('"productId" must be defined') + expect(error.message).toEqual("product - id must be defined") }) it("should throw an error when product with id does not exist", async () => { @@ -217,7 +217,7 @@ describe("Product Service", () => { error = e } - expect(error.message).toEqual(`Product with id "undefined" not found`) + expect(error.message).toEqual(`Product with id "" not found`) let result = await service.retrieve(productOne.id) diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 2415a148cb909..5113c51c22168 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -144,7 +144,7 @@ class Product { @ManyToMany(() => ProductCategory, "products", { owner: true, pivotTable: "product_category_product", - cascade: ["soft-remove"] as any, + // TODO: rm cascade: ["soft-remove"] as any, }) categories = new Collection(this) diff --git a/packages/product/src/repositories/product-image.ts b/packages/product/src/repositories/product-image.ts index 90eb181c0c5f9..c37bbf7b438e9 100644 --- a/packages/product/src/repositories/product-image.ts +++ b/packages/product/src/repositories/product-image.ts @@ -14,6 +14,6 @@ export class ProductImageRepository extends DALUtils.mikroOrmBaseRepositoryFacto async upsert(urls: string[], context: Context = {}): Promise { const data = urls.map((url) => ({ url })) - return await super.upsert(data, context) + return (await super.upsert(data, context)) as Image[] } } diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index eb430a06d4080..0ff5ebbd2ba92 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -17,8 +17,8 @@ import { DALUtils, isDefined, MedusaError, - promiseAll, ProductUtils, + promiseAll, } from "@medusajs/utils" import { ProductServiceTypes } from "../types/services" @@ -118,7 +118,10 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory[], + data: { + entity: Product + update: WithRequiredProperty + }[], context: Context = {} ): Promise { let categoryIds: string[] = [] @@ -128,7 +131,7 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory(context) - data.forEach((productData) => { + data.forEach(({ update: productData }) => { categoryIds = categoryIds.concat( productData?.categories?.map((c) => c.id) || [] ) @@ -144,16 +147,6 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory updateData.id), - }, - { - populate: ["tags", "categories"], - } - ) - const collectionsToAssign = collectionIds.length ? await manager.find(ProductCollection, { id: collectionIds, @@ -195,11 +188,11 @@ export class ProductRepository extends DALUtils.mikroOrmBaseRepositoryFactory( - productsToUpdate.map((product) => [product.id, product]) + data.map(({ entity }) => [entity.id, entity]) ) const products = await promiseAll( - data.map(async (updateData) => { + data.map(async ({ update: updateData }) => { const product = productsToUpdateMap.get(updateData.id) if (!product) { diff --git a/packages/product/src/services/__fixtures__/product.ts b/packages/product/src/services/__fixtures__/product.ts index ec32ccbb28646..7a185e0e24982 100644 --- a/packages/product/src/services/__fixtures__/product.ts +++ b/packages/product/src/services/__fixtures__/product.ts @@ -1,11 +1,8 @@ -import { asClass, asValue, createContainer } from "awilix" -import { ProductService } from "@services" +import { asValue } from "awilix" export const nonExistingProductId = "non-existing-id" -export const mockContainer = createContainer() -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const productRepositoryMock = { productRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { id } }) => { if (id === nonExistingProductId) { @@ -17,5 +14,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - productService: asClass(ProductService), -}) +} diff --git a/packages/product/src/services/__tests__/product.spec.ts b/packages/product/src/services/__tests__/product.spec.ts index 6d2820a19e9c7..fb1a6819d39d1 100644 --- a/packages/product/src/services/__tests__/product.spec.ts +++ b/packages/product/src/services/__tests__/product.spec.ts @@ -1,13 +1,28 @@ -import { mockContainer, nonExistingProductId } from "../__fixtures__/product" +import { + nonExistingProductId, + productRepositoryMock, +} from "../__fixtures__/product" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" describe("Product service", function () { - beforeEach(function () { + let container + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(productRepositoryMock) }) it("should retrieve a product", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const productId = "existing-product" await productService.retrieve(productId) @@ -30,8 +45,8 @@ describe("Product service", function () { }) it("should fail to retrieve a product", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const err = await productService .retrieve(nonExistingProductId) @@ -59,8 +74,8 @@ describe("Product service", function () { }) it("should list products", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = {} const config = { @@ -85,8 +100,8 @@ describe("Product service", function () { }) it("should list products with filters", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { @@ -123,8 +138,8 @@ describe("Product service", function () { }) it("should list products with filters and relations", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { @@ -161,8 +176,8 @@ describe("Product service", function () { }) it("should list and count the products with filters and relations", async function () { - const productService = mockContainer.resolve("productService") - const productRepository = mockContainer.resolve("productRepository") + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") const filters = { tags: { diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts index dc485567aae5c..efc05645e7dcf 100644 --- a/packages/product/src/services/index.ts +++ b/packages/product/src/services/index.ts @@ -6,5 +6,3 @@ export { default as ProductTagService } from "./product-tag" 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-collection.ts b/packages/product/src/services/product-collection.ts index 5ace64dea3783..4c51ece1e3a1c 100644 --- a/packages/product/src/services/product-collection.ts +++ b/packages/product/src/services/product-collection.ts @@ -7,14 +7,7 @@ import { } from "@medusajs/utils" import { ProductCollection } from "@models" -import { - IProductCollectionRepository, - ProductCollectionServiceTypes, -} from "@types" -import { - CreateProductCollection, - UpdateProductCollection, -} from "../types/services/product-collection" +import { ProductCollectionServiceTypes } from "@types" type InjectedDependencies = { productCollectionRepository: DAL.RepositoryService @@ -22,15 +15,11 @@ type InjectedDependencies = { export default class ProductCollectionService< TEntity extends ProductCollection = ProductCollection -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateProductCollection - update: UpdateProductCollection - } ->(ProductCollection) { +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductCollection +) { // eslint-disable-next-line max-len - protected readonly productCollectionRepository_: IProductCollectionRepository + protected readonly productCollectionRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -38,9 +27,9 @@ export default class ProductCollectionService< } @InjectManager("productCollectionRepository_") - async list( + async list( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productCollectionRepository_.find( @@ -50,9 +39,9 @@ export default class ProductCollectionService< } @InjectManager("productCollectionRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productCollectionRepository_.findAndCount( @@ -61,11 +50,9 @@ export default class ProductCollectionService< ) } - protected buildListQueryOptions< - TEntityMethod = ProductTypes.ProductCollectionDTO - >( + protected buildListQueryOptions( filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) @@ -80,12 +67,24 @@ export default class ProductCollectionService< return queryOptions } + create( + data: ProductCollectionServiceTypes.CreateProductCollection, + context?: Context + ): Promise + create( + data: ProductCollectionServiceTypes.CreateProductCollection[], + context?: Context + ): Promise + @InjectTransactionManager("productCollectionRepository_") async create( - data: ProductCollectionServiceTypes.CreateProductCollection[], + data: + | ProductCollectionServiceTypes.CreateProductCollection + | ProductCollectionServiceTypes.CreateProductCollection[], context: Context = {} - ): Promise { - const productCollections = data.map((collectionData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const productCollections = data_.map((collectionData) => { if (collectionData.product_ids) { collectionData.products = collectionData.product_ids @@ -98,12 +97,27 @@ export default class ProductCollectionService< return super.create(productCollections, context) } + // @ts-ignore + update( + data: ProductCollectionServiceTypes.UpdateProductCollection, + context?: Context + ): Promise + // @ts-ignore + update( + data: ProductCollectionServiceTypes.UpdateProductCollection[], + context?: Context + ): Promise + @InjectTransactionManager("productCollectionRepository_") + // @ts-ignore Do not implement all the expected overloads, see if we must do it async update( - data: ProductCollectionServiceTypes.UpdateProductCollection[], + data: + | ProductCollectionServiceTypes.UpdateProductCollection + | ProductCollectionServiceTypes.UpdateProductCollection[], context: Context = {} - ): Promise { - const productCollections = data.map((collectionData) => { + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const productCollections = data_.map((collectionData) => { if (collectionData.product_ids) { collectionData.products = collectionData.product_ids diff --git a/packages/product/src/services/product-image.ts b/packages/product/src/services/product-image.ts deleted file mode 100644 index 50ab03044e0dc..0000000000000 --- a/packages/product/src/services/product-image.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Image } from "@models" -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" - -type InjectedDependencies = { - productImageRepository: DAL.RepositoryService -} - -export default class ProductImageService< - TEntity extends Image = Image -> extends ModulesSdkUtils.abstractServiceFactory( - Image -) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 6285fd47733b9..84febcf50eab2 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -2,13 +2,11 @@ import { Context, CreateProductOnlyDTO, DAL, - FindConfig, IEventBusModuleService, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, ProductTypes, - RestoreReturn, - SoftDeleteReturn, } from "@medusajs/types" import { Image, @@ -25,22 +23,12 @@ import { ProductCategoryService, ProductCollectionService, ProductOptionService, - ProductOptionValueService, ProductService, ProductTagService, ProductTypeService, ProductVariantService, } from "@services" -import ProductImageService from "./product-image" - -import { - ProductCategoryServiceTypes, - ProductCollectionServiceTypes, - ProductServiceTypes, - ProductVariantServiceTypes, -} from "@types" - import { arrayDifference, groupBy, @@ -49,25 +37,24 @@ import { isDefined, isString, kebabCase, - mapObjectTo, MedusaContext, MedusaError, + ModulesSdkUtils, promiseAll, } from "@medusajs/utils" +import { entityNameToLinkableKeysMap, joinerConfig } from "./../joiner-config" import { ProductEventData, ProductEvents } from "../types/services/product" import { ProductCategoryEventData, ProductCategoryEvents, } from "../types/services/product-category" import { - CreateProductOptionValueDTO, - UpdateProductOptionValueDTO, -} from "../types/services/product-option-value" -import { - entityNameToLinkableKeysMap, - joinerConfig, - LinkableKeys, -} from "./../joiner-config" + ProductCategoryServiceTypes, + ProductCollectionServiceTypes, + ProductOptionValueServiceTypes, + ProductServiceTypes, + ProductVariantServiceTypes, +} from "@types" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -76,24 +63,70 @@ type InjectedDependencies = { productTagService: ProductTagService productCategoryService: ProductCategoryService productCollectionService: ProductCollectionService - productImageService: ProductImageService + productImageService: ModulesSdkTypes.InternalModuleService productTypeService: ProductTypeService productOptionService: ProductOptionService - productOptionValueService: ProductOptionValueService + productOptionValueService: ModulesSdkTypes.InternalModuleService eventBusModuleService?: IEventBusModuleService } +const generateMethodForModels = [ + { model: ProductCategory, singular: "Category", plural: "Categories" }, + { model: ProductCollection, singular: "Collection", plural: "Collections" }, + { model: ProductOption, singular: "Option", plural: "Options" }, + { model: ProductTag, singular: "Tag", plural: "Tags" }, + { model: ProductType, singular: "Type", plural: "Types" }, + { model: ProductVariant, singular: "Variant", plural: "Variants" }, +] + export default class ProductModuleService< - TProduct extends Product = Product, - TProductVariant extends ProductVariant = ProductVariant, - TProductTag extends ProductTag = ProductTag, - TProductCollection extends ProductCollection = ProductCollection, - TProductCategory extends ProductCategory = ProductCategory, - TProductImage extends Image = Image, - TProductType extends ProductType = ProductType, - TProductOption extends ProductOption = ProductOption, - TProductOptionValue extends ProductOptionValue = ProductOptionValue -> implements ProductTypes.IProductModuleService + TProduct extends Product = Product, + TProductVariant extends ProductVariant = ProductVariant, + TProductTag extends ProductTag = ProductTag, + TProductCollection extends ProductCollection = ProductCollection, + TProductCategory extends ProductCategory = ProductCategory, + TProductImage extends Image = Image, + TProductType extends ProductType = ProductType, + TProductOption extends ProductOption = ProductOption, + TProductOptionValue extends ProductOptionValue = ProductOptionValue + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + ProductTypes.ProductDTO, + { + ProductCategory: { + dto: ProductTypes.ProductCategoryDTO + singular: "Category" + plural: "Categories" + } + ProductCollection: { + dto: ProductTypes.ProductCollectionDTO + singular: "Collection" + plural: "Collections" + } + ProductOption: { + dto: ProductTypes.ProductOptionDTO + singular: "Option" + plural: "Options" + } + ProductTag: { + dto: ProductTypes.ProductTagDTO + singular: "Tag" + plural: "Tags" + } + ProductType: { + dto: ProductTypes.ProductTypeDTO + singular: "Type" + plural: "Types" + } + ProductVariant: { + dto: ProductTypes.ProductVariantDTO + singular: "Variant" + plural: "Variants" + } + } + >(Product, generateMethodForModels, entityNameToLinkableKeysMap) + implements ProductTypes.IProductModuleService { protected baseRepository_: DAL.RepositoryService protected readonly productService_: ProductService @@ -107,11 +140,12 @@ export default class ProductModuleService< protected readonly productTagService_: ProductTagService // eslint-disable-next-line max-len protected readonly productCollectionService_: ProductCollectionService - protected readonly productImageService_: ProductImageService + // eslint-disable-next-line max-len + protected readonly productImageService_: ModulesSdkTypes.InternalModuleService protected readonly productTypeService_: ProductTypeService protected readonly productOptionService_: ProductOptionService // eslint-disable-next-line max-len - protected readonly productOptionValueService_: ProductOptionValueService + protected readonly productOptionValueService_: ModulesSdkTypes.InternalModuleService protected readonly eventBusModuleService_?: IEventBusModuleService constructor( @@ -130,6 +164,10 @@ export default class ProductModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) + this.baseRepository_ = baseRepository this.productService_ = productService this.productVariantService_ = productVariantService @@ -147,96 +185,6 @@ export default class ProductModuleService< return joinerConfig } - @InjectManager("baseRepository_") - async list( - filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const products = await this.productService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(products)) - } - - @InjectManager("baseRepository_") - async retrieve( - productId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const product = await this.productService_.retrieve( - productId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(product)) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductDTO[], number]> { - const [products, count] = await this.productService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(products)), count] - } - - @InjectManager("baseRepository_") - async retrieveVariant( - productVariantId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productVariant = await this.productVariantService_.retrieve( - productVariantId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productVariant)) - } - - @InjectManager("baseRepository_") - async listVariants( - filters: ProductTypes.FilterableProductVariantProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const variants = await this.productVariantService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(variants)) - } - - @InjectManager("baseRepository_") - async listAndCountVariants( - filters: ProductTypes.FilterableProductVariantProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductVariantDTO[], number]> { - const [variants, count] = await this.productVariantService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(variants)), count] - } - async createVariants( data: ProductTypes.CreateProductVariantDTO[], @MedusaContext() sharedContext: Context = {} @@ -300,21 +248,6 @@ export default class ProductModuleService< 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[], @@ -324,9 +257,7 @@ export default class ProductModuleService< const updatedVariants = await this.baseRepository_.serialize< ProductTypes.ProductVariantDTO[] - >(productVariants, { - populate: true, - }) + >(productVariants) return updatedVariants } @@ -357,8 +288,8 @@ export default class ProductModuleService< } const optionValuesToUpsert: ( - | CreateProductOptionValueDTO - | UpdateProductOptionValueDTO + | ProductOptionValueServiceTypes.CreateProductOptionValueDTO + | ProductOptionValueServiceTypes.UpdateProductOptionValueDTO )[] = [] const optionsValuesToDelete: string[] = [] @@ -413,11 +344,7 @@ export default class ProductModuleService< const groups = groupBy(toUpdate, "product_id") - const [, , productVariants]: [ - void, - TProductOptionValue[], - TProductVariant[][] - ] = await promiseAll([ + const [, , productVariants] = await promiseAll([ await this.productOptionValueService_.delete( optionsValuesToDelete, sharedContext @@ -440,51 +367,6 @@ export default class ProductModuleService< return productVariants.flat() } - @InjectManager("baseRepository_") - async retrieveTag( - tagId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productTag = await this.productTagService_.retrieve( - tagId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productTag)) - } - - @InjectManager("baseRepository_") - async listTags( - filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const tags = await this.productTagService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(tags)) - } - - @InjectManager("baseRepository_") - async listAndCountTags( - filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductTagDTO[], number]> { - const [tags, count] = await this.productTagService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(tags)), count] - } - @InjectTransactionManager("baseRepository_") async createTags( data: ProductTypes.CreateProductTagDTO[], @@ -511,59 +393,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productTags)) } - @InjectTransactionManager("baseRepository_") - async deleteTags( - productTagIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productTagService_.delete(productTagIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveType( - typeId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productType = await this.productTypeService_.retrieve( - typeId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productType)) - } - - @InjectManager("baseRepository_") - async listTypes( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const types = await this.productTypeService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(types)) - } - - @InjectManager("baseRepository_") - async listAndCountTypes( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductTypeDTO[], number]> { - const [types, count] = await this.productTypeService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(types)), count] - } - @InjectTransactionManager("baseRepository_") async createTypes( data: ProductTypes.CreateProductTypeDTO[], @@ -590,60 +419,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productTypes)) } - @InjectTransactionManager("baseRepository_") - async deleteTypes( - productTypeIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productTypeService_.delete(productTypeIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveOption( - optionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productOptions = await this.productOptionService_.retrieve( - optionId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productOptions)) - } - - @InjectManager("baseRepository_") - async listOptions( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productOptions = await this.productOptionService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productOptions)) - } - - @InjectManager("baseRepository_") - async listAndCountOptions( - filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductOptionDTO[], number]> { - const [productOptions, count] = - await this.productOptionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [JSON.parse(JSON.stringify(productOptions)), count] - } - @InjectTransactionManager("baseRepository_") async createOptions( data: ProductTypes.CreateProductOptionDTO[], @@ -656,9 +431,7 @@ export default class ProductModuleService< return await this.baseRepository_.serialize< ProductTypes.ProductOptionDTO[] - >(productOptions, { - populate: true, - }) + >(productOptions) } @InjectTransactionManager("baseRepository_") @@ -673,62 +446,7 @@ export default class ProductModuleService< return await this.baseRepository_.serialize< ProductTypes.ProductOptionDTO[] - >(productOptions, { - populate: true, - }) - } - - @InjectTransactionManager("baseRepository_") - async deleteOptions( - productOptionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productOptionService_.delete(productOptionIds, sharedContext) - } - - @InjectManager("baseRepository_") - async retrieveCollection( - productCollectionId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productCollection = await this.productCollectionService_.retrieve( - productCollectionId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productCollection)) - } - - @InjectManager("baseRepository_") - async listCollections( - filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const collections = await this.productCollectionService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(collections)) - } - - @InjectManager("baseRepository_") - async listAndCountCollections( - filters: ProductTypes.FilterableProductCollectionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductCollectionDTO[], number]> { - const collections = await this.productCollectionService_.listAndCount( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(collections)) + >(productOptions) } @InjectTransactionManager("baseRepository_") @@ -777,57 +495,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productCollections)) } - @InjectTransactionManager("baseRepository_") - async deleteCollections( - productCollectionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productCollectionService_.delete( - productCollectionIds, - sharedContext - ) - - // eslint-disable-next-line max-len - await this.eventBusModuleService_?.emit( - productCollectionIds.map((id) => ({ - eventName: - ProductCollectionServiceTypes.ProductCollectionEvents - .COLLECTION_DELETED, - data: { id }, - })) - ) - } - - @InjectManager("baseRepository_") - async retrieveCategory( - productCategoryId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const productCategory = await this.productCategoryService_.retrieve( - productCategoryId, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(productCategory)) - } - - @InjectManager("baseRepository_") - async listCategories( - filters: ProductTypes.FilterableProductCategoryProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const categories = await this.productCategoryService_.list( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(categories)) - } - @InjectTransactionManager("baseRepository_") async createCategory( data: ProductCategoryServiceTypes.CreateProductCategoryDTO, @@ -866,34 +533,6 @@ export default class ProductModuleService< return JSON.parse(JSON.stringify(productCategory)) } - @InjectTransactionManager("baseRepository_") - async deleteCategory( - categoryId: string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this.productCategoryService_.delete(categoryId, sharedContext) - - await this.eventBusModuleService_?.emit( - ProductCategoryEvents.CATEGORY_DELETED, - { id: categoryId } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCategories( - filters: ProductTypes.FilterableProductCategoryProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[ProductTypes.ProductCategoryDTO[], number]> { - const categories = await this.productCategoryService_.listAndCount( - filters, - config, - sharedContext - ) - - return JSON.parse(JSON.stringify(categories)) - } - @InjectManager("baseRepository_") async create( data: ProductTypes.CreateProductDTO[], @@ -902,9 +541,7 @@ export default class ProductModuleService< const products = await this.create_(data, sharedContext) const createdProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) + >(products) await this.eventBusModuleService_?.emit( createdProducts.map(({ id }) => ({ @@ -925,9 +562,7 @@ export default class ProductModuleService< const updatedProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) + >(products) await this.eventBusModuleService_?.emit( updatedProducts.map(({ id }) => ({ @@ -1297,131 +932,15 @@ export default class ProductModuleService< } @InjectTransactionManager("baseRepository_") - async delete( - productIds: string[], + async deleteCategory( + categoryId: string, @MedusaContext() sharedContext: Context = {} ): Promise { - await this.productService_.delete(productIds, sharedContext) - - await this.eventBusModuleService_?.emit( - productIds.map((id) => ({ - eventName: ProductEvents.PRODUCT_DELETED, - data: { id }, - })) - ) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - productIds: string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [products, cascadedEntitiesMap] = await this.softDelete_( - productIds, - sharedContext - ) - - const softDeletedProducts = await this.baseRepository_.serialize< - ProductTypes.ProductDTO[] - >(products, { - populate: true, - }) - - await this.eventBusModuleService_?.emit( - softDeletedProducts.map(({ id }) => ({ - eventName: ProductEvents.PRODUCT_DELETED, - data: { id }, - })) - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - // Map internal table/column names to their respective external linkable keys - // eg: product.id = product_id, variant.id = variant_id - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - productIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TProduct[], Record]> { - return await this.productService_.softDelete(productIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - productIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.restore_( - productIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - // Map internal table/column names to their respective external linkable keys - // eg: product.id = product_id, variant.id = variant_id - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } + await this.productCategoryService_.delete(categoryId, sharedContext) - @InjectTransactionManager("baseRepository_") - async restoreVariants< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - variantIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.productVariantService_.restore( - variantIds, - sharedContext + await this.eventBusModuleService_?.emit( + ProductCategoryEvents.CATEGORY_DELETED, + { id: categoryId } ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - productIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TProduct[], Record]> { - return await this.productService_.restore(productIds, sharedContext) } } diff --git a/packages/product/src/services/product-option-value.ts b/packages/product/src/services/product-option-value.ts deleted file mode 100644 index 62c9bfa9f7a90..0000000000000 --- a/packages/product/src/services/product-option-value.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ProductOptionValue } from "@models" -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { ProductOptionValueServiceTypes } from "@types" - -type InjectedDependencies = { - productOptionValueRepository: DAL.RepositoryService -} - -export default class ProductOptionValueService< - TEntity extends ProductOptionValue = ProductOptionValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductOptionValueServiceTypes.CreateProductOptionValueDTO - update: ProductOptionValueServiceTypes.UpdateProductOptionValueDTO - } ->(ProductOptionValue) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/product/src/services/product-option.ts b/packages/product/src/services/product-option.ts index 026c2a6b81f7b..eca5140fdefd2 100644 --- a/packages/product/src/services/product-option.ts +++ b/packages/product/src/services/product-option.ts @@ -1,7 +1,6 @@ import { ProductOption } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductOptionRepository } from "@types" type InjectedDependencies = { productOptionRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductOptionService< TEntity extends ProductOption = ProductOption -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductOptionDTO - update: ProductTypes.UpdateProductOptionDTO - } ->(ProductOption) { - protected readonly productOptionRepository_: IProductOptionRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductOption +) { + protected readonly productOptionRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -24,9 +19,9 @@ export default class ProductOptionService< } @InjectManager("productOptionRepository_") - async list( + async list( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext?: Context ): Promise { return await this.productOptionRepository_.find( @@ -36,9 +31,9 @@ export default class ProductOptionService< } @InjectManager("productOptionRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext?: Context ): Promise<[TEntity[], number]> { return await this.productOptionRepository_.findAndCount( @@ -47,9 +42,9 @@ export default class ProductOptionService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductOptionProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-tag.ts b/packages/product/src/services/product-tag.ts index a6d8522307826..2111a12862c27 100644 --- a/packages/product/src/services/product-tag.ts +++ b/packages/product/src/services/product-tag.ts @@ -1,7 +1,6 @@ import { ProductTag } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductTagRepository } from "@types" type InjectedDependencies = { productTagRepository: DAL.RepositoryService @@ -9,23 +8,19 @@ type InjectedDependencies = { export default class ProductTagService< TEntity extends ProductTag = ProductTag -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductTagDTO - update: ProductTypes.UpdateProductTagDTO - } ->(ProductTag) { - protected readonly productTagRepository_: IProductTagRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductTag +) { + protected readonly productTagRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) this.productTagRepository_ = container.productTagRepository } @InjectManager("productTagRepository_") - async list( + async list( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productTagRepository_.find( @@ -35,9 +30,9 @@ export default class ProductTagService< } @InjectManager("productTagRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productTagRepository_.findAndCount( @@ -46,9 +41,9 @@ export default class ProductTagService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductTagProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-type.ts b/packages/product/src/services/product-type.ts index da5587f316dea..0c54344a916a5 100644 --- a/packages/product/src/services/product-type.ts +++ b/packages/product/src/services/product-type.ts @@ -1,7 +1,6 @@ import { ProductType } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" -import { IProductTypeRepository } from "@types" type InjectedDependencies = { productTypeRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductTypeService< TEntity extends ProductType = ProductType -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductTypeDTO - update: ProductTypes.UpdateProductTypeDTO - } ->(ProductType) { - protected readonly productTypeRepository_: IProductTypeRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductType +) { + protected readonly productTypeRepository_: DAL.RepositoryService constructor(container: InjectedDependencies) { super(container) @@ -24,9 +19,9 @@ export default class ProductTypeService< } @InjectManager("productTypeRepository_") - async list( + async list( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { return await this.productTypeRepository_.find( @@ -36,9 +31,9 @@ export default class ProductTypeService< } @InjectManager("productTypeRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { return await this.productTypeRepository_.findAndCount( @@ -47,9 +42,9 @@ export default class ProductTypeService< ) } - private buildQueryForList( + private buildQueryForList( filters: ProductTypes.FilterableProductTypeProps = {}, - config: FindConfig = {} + config: FindConfig = {} ): DAL.FindOptions { const queryOptions = ModulesSdkUtils.buildQuery(filters, config) diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts index e1a247aa1c8a6..823886728ec04 100644 --- a/packages/product/src/services/product-variant.ts +++ b/packages/product/src/services/product-variant.ts @@ -7,7 +7,7 @@ import { } from "@medusajs/utils" import { Product, ProductVariant } from "@models" -import { IProductVariantRepository, ProductVariantServiceTypes } from "@types" +import { ProductVariantServiceTypes } from "@types" import ProductService from "./product" type InjectedDependencies = { @@ -18,14 +18,10 @@ type InjectedDependencies = { export default class ProductVariantService< TEntity extends ProductVariant = ProductVariant, TProduct extends Product = Product -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductVariantOnlyDTO - update: ProductVariantServiceTypes.UpdateProductVariantDTO - } ->(ProductVariant) { - protected readonly productVariantRepository_: IProductVariantRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + ProductVariant +) { + protected readonly productVariantRepository_: DAL.RepositoryService protected readonly productService_: ProductService constructor({ @@ -92,8 +88,6 @@ export default class ProductVariantService< const variantsData = [...data] variantsData.forEach((variant) => Object.assign(variant, { product })) - return await this.productVariantRepository_.update(variantsData, { - transactionManager: sharedContext.transactionManager, - }) + return await super.update(variantsData, sharedContext) } } diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts index 3c514c0abea16..835f09b056bff 100644 --- a/packages/product/src/services/product.ts +++ b/packages/product/src/services/product.ts @@ -1,7 +1,6 @@ import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { InjectManager, MedusaContext, ModulesSdkUtils } from "@medusajs/utils" import { Product } from "@models" -import { IProductRepository, ProductServiceTypes } from "@types" type InjectedDependencies = { productRepository: DAL.RepositoryService @@ -9,14 +8,10 @@ type InjectedDependencies = { export default class ProductService< TEntity extends Product = Product -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: ProductTypes.CreateProductOnlyDTO - update: ProductServiceTypes.UpdateProductDTO - } ->(Product) { - protected readonly productRepository_: IProductRepository +> extends ModulesSdkUtils.internalModuleServiceFactory( + Product +) { + protected readonly productRepository_: DAL.RepositoryService constructor({ productRepository }: InjectedDependencies) { // @ts-ignore @@ -27,9 +22,9 @@ export default class ProductService< } @InjectManager("productRepository_") - async list( + async list( filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise { if (filters.category_id) { @@ -45,13 +40,13 @@ export default class ProductService< delete filters.category_id } - return await super.list(filters, config, sharedContext) + return await super.list(filters, config, sharedContext) } @InjectManager("productRepository_") - async listAndCount( + async listAndCount( filters: ProductTypes.FilterableProductProps = {}, - config: FindConfig = {}, + config: FindConfig = {}, @MedusaContext() sharedContext: Context = {} ): Promise<[TEntity[], number]> { if (filters.category_id) { @@ -67,10 +62,6 @@ export default class ProductService< delete filters.category_id } - return await super.listAndCount( - filters, - config, - sharedContext - ) + return await super.listAndCount(filters, config, sharedContext) } } diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts index 051d3d01a8d4f..2503374225c8e 100644 --- a/packages/product/src/types/index.ts +++ b/packages/product/src/types/index.ts @@ -6,4 +6,3 @@ export type InitializeModuleInjectableDependencies = { } export * from "./services" -export * from "./repositories" diff --git a/packages/product/src/types/repositories.ts b/packages/product/src/types/repositories.ts deleted file mode 100644 index 6f7f3a05108ef..0000000000000 --- a/packages/product/src/types/repositories.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { DAL, ProductTypes, WithRequiredProperty } from "@medusajs/types" -import { - Image, - Product, - ProductCollection, - ProductOption, - ProductOptionValue, - ProductTag, - ProductType, - ProductVariant, -} from "@models" -import { UpdateProductDTO } from "./services/product" -import { - CreateProductCollection, - UpdateProductCollection, -} from "./services/product-collection" -import { - CreateProductOptionValueDTO, - UpdateProductOptionValueDTO, -} from "./services/product-option-value" -import { UpdateProductVariantDTO } from "./services/product-variant" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductRepository - extends DAL.RepositoryService< - TEntity, - { - create: WithRequiredProperty - update: WithRequiredProperty - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductCollectionRepository< - TEntity extends ProductCollection = ProductCollection -> extends DAL.RepositoryService< - TEntity, - { - create: CreateProductCollection - update: UpdateProductCollection - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductImageRepository - extends DAL.RepositoryService {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductOptionRepository< - TEntity extends ProductOption = ProductOption -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductOptionDTO - update: ProductTypes.UpdateProductOptionDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductOptionValueRepository< - TEntity extends ProductOptionValue = ProductOptionValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreateProductOptionValueDTO - update: UpdateProductOptionValueDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductTagRepository - extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductTagDTO - update: ProductTypes.UpdateProductTagDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductTypeRepository< - TEntity extends ProductType = ProductType -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductTypeDTO - update: ProductTypes.UpdateProductTypeDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IProductVariantRepository< - TEntity extends ProductVariant = ProductVariant -> extends DAL.RepositoryService< - TEntity, - { - create: ProductTypes.CreateProductVariantOnlyDTO - update: UpdateProductVariantDTO - } - > {} diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts index 122cde64afbbb..a5d075f9fa451 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/campaign.spec.ts @@ -55,7 +55,7 @@ describe("Promotion Module Service: Campaigns", () => { campaign_identifier: "test-1", starts_at: expect.any(Date), ends_at: expect.any(Date), - budget: expect.any(String), + budget: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -68,7 +68,7 @@ describe("Promotion Module Service: Campaigns", () => { campaign_identifier: "test-2", starts_at: expect.any(Date), ends_at: expect.any(Date), - budget: expect.any(String), + budget: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -378,7 +378,7 @@ describe("Promotion Module Service: Campaigns", () => { error = e } - expect(error.message).toEqual('"campaignId" must be defined') + expect(error.message).toEqual("campaign - id must be defined") }) it("should return campaign based on config select param", async () => { diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index ee44ad54453b3..7ba8ffcd66f21 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -1,6 +1,6 @@ import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" -import { PromotionType } from "@medusajs/utils" +import { ApplicationMethodType, PromotionType } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" @@ -49,7 +49,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -60,7 +60,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -95,7 +95,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, adjustments: [ { id: "test-adjustment", @@ -106,7 +106,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, }, ], }) @@ -138,12 +138,12 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, }, { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, adjustments: [ { id: "test-adjustment", @@ -162,221 +162,1789 @@ describe("Promotion Service: computeActions", () => { }) describe("when promotion is for items and allocation is each", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "200", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 100, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 5, + subtotal: 750, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - items: [ + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", + customer: { + customer_group: { + id: "VIP", + }, }, - product: { - id: "prod_tshirt", + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, + ]) + + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, - product: { - id: "prod_sweater", + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", }, - ], + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - is_automatic: true, - type: PromotionType.STANDARD, - rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "200", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 500, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "each", + value: "500", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 100, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 5, + subtotal: 750, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "30", + max_quantity: 2, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 1, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 3, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 30, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 45, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 2, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 10.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "50", + max_quantity: 10, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "100", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 10000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "each", + value: "10", + max_quantity: 5, + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + }) + + describe("when promotion is for items and allocation is across", () => { + describe("when application type is fixed", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "400", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 100, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 300, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "30", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 7.5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 22.5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 12.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 37.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "50", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "1500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "items", + allocation: "across", + value: "500", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct item amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 2, + subtotal: 200, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 2, + subtotal: 600, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + }) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 20, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 60, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ + { + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", }, - }, - items: [ + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, + ]) + + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_sweater", - quantity: 5, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + ]) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "30", - max_quantity: 2, - target_rules: [ + items: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "item_cotton_tshirt", + quantity: 1, + subtotal: 50, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_tshirt", + }, + }, + { + id: "item_cotton_sweater", + quantity: 1, + subtotal: 150, + product_category: { + id: "catg_cotton", + }, + product: { + id: "prod_sweater", + }, }, ], + } + ) + + expect(result).toEqual([ + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 5, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 15, + code: "PROMOTION_TEST", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_tshirt", + amount: 4.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addItemAdjustment", + item_id: "item_cotton_sweater", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) + }) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -385,8 +1953,8 @@ describe("Promotion Service: computeActions", () => { items: [ { id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, + quantity: 5, + subtotal: 5000, product_category: { id: "catg_cotton", }, @@ -394,976 +1962,1128 @@ describe("Promotion Service: computeActions", () => { id: "prod_tshirt", }, }, + ], + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "items", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "product_category.id", + operator: "eq", + values: ["catg_cotton"], + }, + ], + }, + }, + ]) + + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + items: [ { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, + id: "item_cotton_tshirt", + quantity: 5, + subtotal: 5000, product_category: { id: "catg_cotton", }, product: { - id: "prod_sweater", + id: "prod_tshirt", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 30, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 20, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ + describe("when promotion is for shipping_method and allocation is each", () => { + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "50", - max_quantity: 1, - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", }, }, - items: [ + shipping_methods: [ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", }, }, { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", }, }, - ], - } - ) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ - { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], - }, - ], - }, - }, - ]) + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", }, - }, - items: [ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "each", - value: "500", - max_quantity: 5, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], - }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) - - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for items and allocation is across", () => { - it("should compute the correct item amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, - { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", - }, - }, - ], + expect(result).toEqual([]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "400", - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + const [createdPromotionTwo] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 2, - unit_price: 100, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "item_cotton_sweater", - quantity: 2, - unit_price: 300, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 200, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 100, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 300, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "500", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "30", - target_rules: [ + }, + ]) + + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 250, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 150, + code: "PROMOTION_TEST", + }, + ]) + }) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) + + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", }, }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", - }, - }, + shipping_methods: [ { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_sweater", + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 7.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 22.5, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: "fixed", + target_type: "shipping_methods", + allocation: "each", + value: "1200", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "50", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", }, }, - items: [ + shipping_methods: [ { - id: "item_cotton_tshirt", - quantity: 1, - unit_price: 50, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", }, }, { - id: "item_cotton_sweater", - quantity: 1, - unit_price: 150, - product_category: { - id: "catg_cotton", + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", }, - product: { - id: "prod_sweater", + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 50, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, - ]) - }) + }) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) + }) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "1500", - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const result = await service.computeActions([], { + customer: { + customer_group: { + id: "VIP", + }, }, - }, - items: [ - { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, }, - product: { - id: "prod_tshirt", + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, + ], + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) + it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + [], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: "500", - target_rules: [ + shipping_methods: [ { - attribute: "product_category.id", - operator: "eq", - values: ["catg_cotton"], + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], }, - }, - ]) + { prevent_auto_promotions: true } + ) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, + expect(result).toEqual([]) }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - items: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ { - id: "item_cotton_tshirt", - quantity: 5, - unit_price: 1000, - product_category: { - id: "catg_cotton", - }, - product: { - id: "prod_tshirt", + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for shipping_method and allocation is each", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + const [createdPromotionTwo] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotionTwo] = await service.create([ { - id: "shipping_method_express", - unit_price: 250, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 250, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 150, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 25, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 15, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 22.5, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 13.5, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct shipping_method amendments when promotion is automatic and prevent_auto_promotions is false", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "100", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - [], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1372,93 +3092,111 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 1200, shipping_option: { id: "express", }, }, - { - id: "shipping_method_standard", - unit_price: 150, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, ], - }, - { prevent_auto_promotions: true } - ) + }) - expect(result).toEqual([]) - }) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "each", + value: "10", + max_quantity: 2, + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + }) + }) + + describe("when promotion is for shipping_method and allocation is across", () => { + describe("when application type is fixed", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1467,111 +3205,74 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 200, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 50, - code: "PROMOTION_TEST_2", - }, - ]) - }) + }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "500", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - }, - ]) + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", + }, + ]) + }) - const [createdPromotionTwo] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "200", - max_quantity: 2, - target_rules: [ + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -1580,369 +3281,356 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 250, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 150, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) - - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 250, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 150, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ - { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], - }, - ], - }, - }, - ]) + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - }, - shipping_methods: [ { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) - - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "each", - value: "1200", - max_quantity: 2, - target_rules: [ + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) - - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotion2] = await service.create([ { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, - ], - }) - - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) - }) + ]) - describe("when promotion is for shipping_method and allocation is across", () => { - it("should compute the correct shipping_method amendments", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + shipping_methods: [ { - attribute: "shipping_option.id", - operator: "in", - values: ["express", "standard"], + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - }, - }, - ]) + } + ) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + expect(result).toEqual([ { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 166.66666666666669, + code: "PROMOTION_TEST", }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 33.33333333333333, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 83.33333333333331, + code: "PROMOTION_TEST_2", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 16.66666666666667, + code: "PROMOTION_TEST_2", + }, + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) - - it("should compute the correct shipping_method amendments when promotion is automatic", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - is_automatic: true, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1000", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions([], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ + const [createdPromotion2] = await service.create([ { - id: "shipping_method_express", - unit_price: 500, - shipping_option: { - id: "express", + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], }, }, + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", }, { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", }, - ], + ]) }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute the correct item amendments when there are multiple promotions to apply", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.FIXED, + target_type: "shipping_methods", + allocation: "across", + value: "1200", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) + + const result = await service.computeActions(["PROMOTION_TEST"], { customer: { customer_group: { id: "VIP", @@ -1951,115 +3639,126 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 1200, shipping_option: { id: "express", }, }, - { - id: "shipping_method_standard", - unit_price: 100, - shipping_option: { - id: "standard", - }, - }, - { - id: "shipping_method_snail", - unit_price: 200, - shipping_option: { - id: "snail", - }, - }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 166.66666666666669, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 33.33333333333333, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 83.33333333333331, - code: "PROMOTION_TEST_2", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 16.66666666666667, - code: "PROMOTION_TEST_2", - }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) - it("should not compute actions when applicable total is 0", async () => { - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], - }, - ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1000", - target_rules: [ + describe("when application type is percentage", () => { + it("should compute the correct shipping_method amendments", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const [createdPromotion2] = await service.create([ - { - code: "PROMOTION_TEST_2", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, }, ], - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "200", - target_rules: [ + }) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct shipping_method amendments when promotion is automatic", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + is_automatic: true, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions( - ["PROMOTION_TEST", "PROMOTION_TEST_2"], - { + const result = await service.computeActions([], { customer: { customer_group: { id: "VIP", @@ -2068,154 +3767,376 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, }, ], - } - ) + }) - expect(result).toEqual([ - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_express", - amount: 500, - code: "PROMOTION_TEST", - }, - { - action: "addShippingMethodAdjustment", - shipping_method_id: "shipping_method_standard", - amount: 100, - code: "PROMOTION_TEST", - }, - ]) - }) + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + ]) + }) + + it("should compute the correct item amendments when there are multiple promotions to apply", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { - await createCampaigns(repositoryManager) + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ - { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], + { + customer: { + customer_group: { + id: "VIP", + }, }, - ], - campaign_id: "campaign-id-1", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + shipping_methods: [ { - attribute: "shipping_option.id", + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 50, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 10, + code: "PROMOTION_TEST", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 45, + code: "PROMOTION_TEST_2", + }, + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 9, + code: "PROMOTION_TEST_2", + }, + ]) + }) + + it("should not compute actions when applicable total is 0", async () => { + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", + const [createdPromotion2] = await service.create([ + { + code: "PROMOTION_TEST_2", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - shipping_methods: [ + ]) + + const result = await service.computeActions( + ["PROMOTION_TEST", "PROMOTION_TEST_2"], { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + customer: { + customer_group: { + id: "VIP", + }, }, + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 500, + shipping_option: { + id: "express", + }, + }, + { + id: "shipping_method_standard", + subtotal: 100, + shipping_option: { + id: "standard", + }, + }, + { + id: "shipping_method_snail", + subtotal: 200, + shipping_option: { + id: "snail", + }, + }, + ], + } + ) + + expect(result).toEqual([ + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_express", + amount: 500, + code: "PROMOTION_TEST", }, - ], + { + action: "addShippingMethodAdjustment", + shipping_method_id: "shipping_method_standard", + amount: 100, + code: "PROMOTION_TEST", + }, + ]) }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) - }) + it("should compute budget exceeded action when applicable total exceeds campaign budget for type spend", async () => { + await createCampaigns(repositoryManager) - it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { - await createCampaigns(repositoryManager) + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ + { + attribute: "customer.customer_group.id", + operator: "in", + values: ["VIP", "top100"], + }, + ], + campaign_id: "campaign-id-1", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "100", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, + }, + ]) - const [createdPromotion] = await service.create([ - { - code: "PROMOTION_TEST", - type: PromotionType.STANDARD, - rules: [ + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", + }, + }, + shipping_methods: [ { - attribute: "customer.customer_group.id", - operator: "in", - values: ["VIP", "top100"], + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, }, ], - campaign_id: "campaign-id-2", - application_method: { - type: "fixed", - target_type: "shipping_methods", - allocation: "across", - value: "1200", - target_rules: [ + }) + + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) + + it("should compute budget exceeded action when applicable total exceeds campaign budget for type usage", async () => { + await createCampaigns(repositoryManager) + + const [createdPromotion] = await service.create([ + { + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + rules: [ { - attribute: "shipping_option.id", + attribute: "customer.customer_group.id", operator: "in", - values: ["express", "standard"], + values: ["VIP", "top100"], }, ], + campaign_id: "campaign-id-2", + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: "shipping_methods", + allocation: "across", + value: "10", + target_rules: [ + { + attribute: "shipping_option.id", + operator: "in", + values: ["express", "standard"], + }, + ], + }, }, - }, - ]) + ]) - await service.updateCampaigns({ - id: "campaign-id-2", - budget: { used: 1000 }, - }) + await service.updateCampaigns({ + id: "campaign-id-2", + budget: { used: 1000 }, + }) - const result = await service.computeActions(["PROMOTION_TEST"], { - customer: { - customer_group: { - id: "VIP", - }, - }, - shipping_methods: [ - { - id: "shipping_method_express", - unit_price: 1200, - shipping_option: { - id: "express", + const result = await service.computeActions(["PROMOTION_TEST"], { + customer: { + customer_group: { + id: "VIP", }, }, - ], - }) + shipping_methods: [ + { + id: "shipping_method_express", + subtotal: 1200, + shipping_option: { + id: "express", + }, + }, + ], + }) - expect(result).toEqual([ - { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, - ]) + expect(result).toEqual([ + { action: "campaignBudgetExceeded", code: "PROMOTION_TEST" }, + ]) + }) }) }) @@ -2251,7 +4172,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2262,7 +4183,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2321,7 +4242,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2332,7 +4253,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 150, + subtotal: 300, product_category: { id: "catg_cotton", }, @@ -2412,7 +4333,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2423,7 +4344,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2516,7 +4437,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 50, + subtotal: 50, product_category: { id: "catg_cotton", }, @@ -2527,7 +4448,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 1, - unit_price: 150, + subtotal: 150, product_category: { id: "catg_cotton", }, @@ -2552,18 +4473,6 @@ describe("Promotion Service: computeActions", () => { amount: 150, code: "PROMOTION_TEST", }, - { - action: "addItemAdjustment", - item_id: "item_cotton_tshirt", - amount: 12.5, - code: "PROMOTION_TEST_2", - }, - { - action: "addItemAdjustment", - item_id: "item_cotton_sweater", - amount: 37.5, - code: "PROMOTION_TEST_2", - }, ]) }) }) @@ -2615,7 +4524,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 1, - unit_price: 100, + subtotal: 100, product_category: { id: "catg_cotton", }, @@ -2632,7 +4541,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 5, - unit_price: 150, + subtotal: 750, product_category: { id: "catg_cotton", }, @@ -2708,7 +4617,7 @@ describe("Promotion Service: computeActions", () => { shipping_methods: [ { id: "shipping_method_express", - unit_price: 500, + subtotal: 500, shipping_option: { id: "express", }, @@ -2721,14 +4630,14 @@ describe("Promotion Service: computeActions", () => { }, { id: "shipping_method_standard", - unit_price: 100, + subtotal: 100, shipping_option: { id: "standard", }, }, { id: "shipping_method_snail", - unit_price: 200, + subtotal: 200, shipping_option: { id: "snail", }, @@ -2770,7 +4679,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2781,7 +4690,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2792,7 +4701,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2862,7 +4771,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2873,7 +4782,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2884,7 +4793,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -2947,7 +4856,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -2958,7 +4867,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -2969,7 +4878,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, @@ -3045,7 +4954,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt", quantity: 2, - unit_price: 500, + subtotal: 1000, product_category: { id: "catg_tshirt", }, @@ -3056,7 +4965,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_tshirt2", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_tshirt", }, @@ -3067,7 +4976,7 @@ describe("Promotion Service: computeActions", () => { { id: "item_cotton_sweater", quantity: 2, - unit_price: 1000, + subtotal: 2000, product_category: { id: "catg_sweater", }, diff --git a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts index 909f31dbc49ac..94f6953370364 100644 --- a/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts +++ b/packages/promotion/integration-tests/__tests__/services/promotion-module/promotion.spec.ts @@ -1,16 +1,17 @@ +import { Modules } from "@medusajs/modules-sdk" import { IPromotionModuleService } from "@medusajs/types" import { + ApplicationMethodTargetType, ApplicationMethodType, CampaignBudgetType, PromotionType, } from "@medusajs/utils" import { SqlEntityManager } from "@mikro-orm/postgresql" +import { initModules } from "medusa-test-utils" import { createCampaigns } from "../../../__fixtures__/campaigns" import { createPromotions } from "../../../__fixtures__/promotion" import { MikroOrmWrapper } from "../../../utils" import { getInitModuleConfig } from "../../../utils/get-init-module-config" -import { Modules } from "@medusajs/modules-sdk" -import { initModules } from "medusa-test-utils/dist" jest.setTimeout(30000) @@ -114,6 +115,24 @@ describe("Promotion Service", () => { ) }) + it("should throw error when percentage type and value is greater than 100", async () => { + const error = await service + .create({ + code: "PROMOTION_TEST", + type: PromotionType.STANDARD, + application_method: { + type: ApplicationMethodType.PERCENTAGE, + target_type: ApplicationMethodTargetType.ORDER, + value: "1000", + }, + }) + .catch((e) => e) + + expect(error.message).toContain( + "Application Method value should be a percentage number between 0 and 100" + ) + }) + it("should throw an error when both campaign and campaign_id are provided", async () => { const startsAt = new Date("01/01/2023") const endsAt = new Date("01/01/2023") @@ -655,7 +674,7 @@ describe("Promotion Service", () => { is_automatic: true, code: "TEST", type: PromotionType.BUYGET, - }, + } as any, ]) expect(updatedPromotion).toEqual( @@ -871,7 +890,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should return promotion based on config select param", async () => { @@ -899,12 +918,12 @@ describe("Promotion Service", () => { value: "200", target_type: "items", }, - }, + } as any, { id: "promotion-id-2", code: "PROMOTION_2", type: PromotionType.STANDARD, - }, + } as any, ]) }) @@ -919,7 +938,7 @@ describe("Promotion Service", () => { campaign: null, is_automatic: false, type: "standard", - application_method: expect.any(String), + application_method: expect.any(Object), created_at: expect.any(Date), updated_at: expect.any(Date), deleted_at: null, @@ -1062,7 +1081,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1137,7 +1156,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create target rules for a promotion", async () => { @@ -1227,7 +1246,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create buy rules for a buyget promotion", async () => { @@ -1315,7 +1334,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1386,7 +1405,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully create rules for a promotion", async () => { @@ -1470,7 +1489,7 @@ describe("Promotion Service", () => { error = e } - expect(error.message).toEqual('"promotionId" must be defined') + expect(error.message).toEqual("promotion - id must be defined") }) it("should successfully remove rules for a promotion", async () => { diff --git a/packages/promotion/src/repositories/campaign.ts b/packages/promotion/src/repositories/campaign.ts index 231dcb8563351..afec080397274 100644 --- a/packages/promotion/src/repositories/campaign.ts +++ b/packages/promotion/src/repositories/campaign.ts @@ -4,13 +4,9 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { Campaign, Promotion } from "@models" import { CreateCampaignDTO, UpdateCampaignDTO } from "@types" -export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< - Campaign, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - } ->(Campaign) { +export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory( + Campaign +) { async create( data: CreateCampaignDTO[], context: Context = {} @@ -64,7 +60,7 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< } async update( - data: UpdateCampaignDTO[], + data: { entity: Campaign; update: UpdateCampaignDTO }[], context: Context = {} ): Promise { const manager = this.getActiveManager(context) @@ -72,7 +68,7 @@ export class CampaignRepository extends DALUtils.mikroOrmBaseRepositoryFactory< const campaignIds: string[] = [] const campaignPromotionIdsMap = new Map() - data.forEach((campaignData) => { + data.forEach(({ update: campaignData }) => { const campaignPromotionIds = campaignData.promotions?.map((p) => p.id) campaignIds.push(campaignData.id) diff --git a/packages/promotion/src/services/application-method.ts b/packages/promotion/src/services/application-method.ts deleted file mode 100644 index 2f0e4d38be68a..0000000000000 --- a/packages/promotion/src/services/application-method.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ApplicationMethod } from "@models" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CreateApplicationMethodDTO, UpdateApplicationMethodDTO } from "@types" - -type InjectedDependencies = { - applicationMethodRepository: DAL.RepositoryService -} - -export default class ApplicationMethodService< - TEntity extends ApplicationMethod = ApplicationMethod -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateApplicationMethodDTO - update: UpdateApplicationMethodDTO - }, - { - list: PromotionTypes.FilterableApplicationMethodProps - listAndCount: PromotionTypes.FilterableApplicationMethodProps - } ->(ApplicationMethod) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/campaign-budget.ts b/packages/promotion/src/services/campaign-budget.ts deleted file mode 100644 index 269b337787e57..0000000000000 --- a/packages/promotion/src/services/campaign-budget.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CampaignBudget } from "@models" -import { CreateCampaignBudgetDTO, UpdateCampaignBudgetDTO } from "../types" - -type InjectedDependencies = { - campaignBudgetRepository: DAL.RepositoryService -} - -export default class CampaignBudgetService< - TEntity extends CampaignBudget = CampaignBudget -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCampaignBudgetDTO - update: UpdateCampaignBudgetDTO - }, - { - list: PromotionTypes.FilterableCampaignBudgetProps - listAndCount: PromotionTypes.FilterableCampaignBudgetProps - } ->(CampaignBudget) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/campaign.ts b/packages/promotion/src/services/campaign.ts deleted file mode 100644 index 1f7b83adae2d1..0000000000000 --- a/packages/promotion/src/services/campaign.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Campaign } from "@models" -import { CreateCampaignDTO, UpdateCampaignDTO } from "../types" - -type InjectedDependencies = { - campaignRepository: DAL.RepositoryService -} - -export default class CampaignService< - TEntity extends Campaign = Campaign -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - }, - { - list: PromotionTypes.FilterableCampaignProps - listAndCount: PromotionTypes.FilterableCampaignProps - } ->(Campaign) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/index.ts b/packages/promotion/src/services/index.ts index dcd93f46b8fad..c127ea20f4bcf 100644 --- a/packages/promotion/src/services/index.ts +++ b/packages/promotion/src/services/index.ts @@ -1,7 +1 @@ -export { default as ApplicationMethodService } from "./application-method" -export { default as CampaignService } from "./campaign" -export { default as CampaignBudgetService } from "./campaign-budget" -export { default as PromotionService } from "./promotion" export { default as PromotionModuleService } from "./promotion-module" -export { default as PromotionRuleService } from "./promotion-rule" -export { default as PromotionRuleValueService } from "./promotion-rule-value" diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts index 158e8201c6847..2fc707942a92e 100644 --- a/packages/promotion/src/services/promotion-module.ts +++ b/packages/promotion/src/services/promotion-module.ts @@ -1,23 +1,21 @@ import { Context, DAL, - FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, PromotionTypes, - RestoreReturn, - SoftDeleteReturn, } from "@medusajs/types" import { ApplicationMethodTargetType, CampaignBudgetType, InjectManager, InjectTransactionManager, + isString, MedusaContext, MedusaError, + ModulesSdkUtils, PromotionType, - isString, - mapObjectTo, } from "@medusajs/utils" import { ApplicationMethod, @@ -27,14 +25,6 @@ import { PromotionRule, PromotionRuleValue, } from "@models" -import { - ApplicationMethodService, - CampaignBudgetService, - CampaignService, - PromotionRuleService, - PromotionRuleValueService, - PromotionService, -} from "@services" import { ApplicationMethodRuleTypes, CreateApplicationMethodDTO, @@ -48,43 +38,60 @@ import { UpdatePromotionDTO, } from "@types" import { - ComputeActionUtils, allowedAllocationForQuantity, areRulesValidForContext, + ComputeActionUtils, validateApplicationMethodAttributes, validatePromotionRuleAttributes, } from "@utils" -import { - LinkableKeys, - entityNameToLinkableKeysMap, - joinerConfig, -} from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - promotionService: PromotionService - applicationMethodService: ApplicationMethodService - promotionRuleService: PromotionRuleService - promotionRuleValueService: PromotionRuleValueService - campaignService: CampaignService - campaignBudgetService: CampaignBudgetService + promotionService: ModulesSdkTypes.InternalModuleService + applicationMethodService: ModulesSdkTypes.InternalModuleService + promotionRuleService: ModulesSdkTypes.InternalModuleService + promotionRuleValueService: ModulesSdkTypes.InternalModuleService + campaignService: ModulesSdkTypes.InternalModuleService + campaignBudgetService: ModulesSdkTypes.InternalModuleService } +const generateMethodForModels = [ + ApplicationMethod, + Campaign, + CampaignBudget, + PromotionRule, + PromotionRuleValue, +] + export default class PromotionModuleService< - TPromotion extends Promotion = Promotion, - TPromotionRule extends PromotionRule = PromotionRule, - TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue, - TCampaign extends Campaign = Campaign, - TCampaignBudget extends CampaignBudget = CampaignBudget -> implements PromotionTypes.IPromotionModuleService + TApplicationMethod extends ApplicationMethod = ApplicationMethod, + TPromotion extends Promotion = Promotion, + TPromotionRule extends PromotionRule = PromotionRule, + TPromotionRuleValue extends PromotionRuleValue = PromotionRuleValue, + TCampaign extends Campaign = Campaign, + TCampaignBudget extends CampaignBudget = CampaignBudget + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + PromotionTypes.PromotionDTO, + { + ApplicationMethod: { dto: PromotionTypes.ApplicationMethodDTO } + Campaign: { dto: PromotionTypes.CampaignDTO } + CampaignBudget: { dto: PromotionTypes.CampaignBudgetDTO } + PromotionRule: { dto: PromotionTypes.PromotionRuleDTO } + PromotionRuleValue: { dto: PromotionTypes.PromotionRuleValueDTO } + } + >(Promotion, generateMethodForModels, entityNameToLinkableKeysMap) + implements PromotionTypes.IPromotionModuleService { protected baseRepository_: DAL.RepositoryService - protected promotionService_: PromotionService - protected applicationMethodService_: ApplicationMethodService - protected promotionRuleService_: PromotionRuleService - protected promotionRuleValueService_: PromotionRuleValueService - protected campaignService_: CampaignService - protected campaignBudgetService_: CampaignBudgetService + protected promotionService_: ModulesSdkTypes.InternalModuleService + protected applicationMethodService_: ModulesSdkTypes.InternalModuleService + protected promotionRuleService_: ModulesSdkTypes.InternalModuleService + protected promotionRuleValueService_: ModulesSdkTypes.InternalModuleService + protected campaignService_: ModulesSdkTypes.InternalModuleService + protected campaignBudgetService_: ModulesSdkTypes.InternalModuleService constructor( { @@ -98,6 +105,9 @@ export default class PromotionModuleService< }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) + this.baseRepository_ = baseRepository this.promotionService_ = promotionService this.applicationMethodService_ = applicationMethodService @@ -399,63 +409,6 @@ export default class PromotionModuleService< return computedActions } - @InjectManager("baseRepository_") - async retrieve( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const promotion = await this.promotionService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - promotion, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async list( - filters: PromotionTypes.FilterablePromotionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const promotions = await this.promotionService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - promotions, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: PromotionTypes.FilterablePromotionProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PromotionTypes.PromotionDTO[], number]> { - const [promotions, count] = await this.promotionService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - promotions, - { populate: true } - ), - count, - ] - } - async create( data: PromotionTypes.CreatePromotionDTO, sharedContext?: Context @@ -921,88 +874,6 @@ export default class PromotionModuleService< } } - @InjectTransactionManager("baseRepository_") - async delete( - ids: string[] | string, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - - await this.promotionService_.delete(idsToDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - let [_, cascadedEntitiesMap] = await this.softDelete_( - idsToDelete, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - promotionIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TPromotion[], Record]> { - return await this.promotionService_.softDelete(promotionIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToRestore = Array.isArray(ids) ? ids : [ids] - const [_, cascadedEntitiesMap] = await this.restore_( - idsToRestore, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TPromotion[], Record]> { - return await this.promotionService_.restore(ids, sharedContext) - } - @InjectManager("baseRepository_") async removePromotionRules( promotionId: string, @@ -1134,63 +1005,6 @@ export default class PromotionModuleService< ) } - @InjectManager("baseRepository_") - async retrieveCampaign( - id: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const campaign = await this.campaignService_.retrieve( - id, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - campaign, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listCampaigns( - filters: PromotionTypes.FilterableCampaignProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const campaigns = await this.campaignService_.list( - filters, - config, - sharedContext - ) - - return await this.baseRepository_.serialize( - campaigns, - { populate: true } - ) - } - - @InjectManager("baseRepository_") - async listAndCountCampaigns( - filters: PromotionTypes.FilterableCampaignProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[PromotionTypes.CampaignDTO[], number]> { - const [campaigns, count] = await this.campaignService_.listAndCount( - filters, - config, - sharedContext - ) - - return [ - await this.baseRepository_.serialize( - campaigns, - { populate: true } - ), - count, - ] - } - async createCampaigns( data: PromotionTypes.CreateCampaignDTO, sharedContext?: Context @@ -1361,82 +1175,4 @@ export default class PromotionModuleService< return updatedCampaigns } - - @InjectTransactionManager("baseRepository_") - async deleteCampaigns( - ids: string | string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - - await this.campaignService_.delete(idsToDelete, sharedContext) - } - - @InjectManager("baseRepository_") - async softDeleteCampaigns( - ids: string | string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToDelete = Array.isArray(ids) ? ids : [ids] - let [_, cascadedEntitiesMap] = await this.softDeleteCampaigns_( - idsToDelete, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - protected async softDeleteCampaigns_( - campaignIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TCampaign[], Record]> { - return await this.campaignService_.softDelete(campaignIds, sharedContext) - } - - @InjectManager("baseRepository_") - async restoreCampaigns< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - ids: string | string[], - { returnLinkableKeys }: RestoreReturn = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise, string[]> | void> { - const idsToRestore = Array.isArray(ids) ? ids : [ids] - const [_, cascadedEntitiesMap] = await this.restoreCampaigns_( - idsToRestore, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restoreCampaigns_( - ids: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TCampaign[], Record]> { - return await this.campaignService_.restore(ids, sharedContext) - } } diff --git a/packages/promotion/src/services/promotion-rule-value.ts b/packages/promotion/src/services/promotion-rule-value.ts deleted file mode 100644 index 90a0487d94685..0000000000000 --- a/packages/promotion/src/services/promotion-rule-value.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PromotionRuleValue } from "@models" -import { - CreatePromotionRuleValueDTO, - UpdatePromotionRuleValueDTO, -} from "../types" - -type InjectedDependencies = { - promotionRuleValueRepository: DAL.RepositoryService -} - -export default class PromotionRuleValueService< - TEntity extends PromotionRuleValue = PromotionRuleValue -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionRuleValueDTO - update: UpdatePromotionRuleValueDTO - }, - { - list: PromotionTypes.FilterablePromotionRuleValueProps - listAndCount: PromotionTypes.FilterablePromotionRuleValueProps - } ->(PromotionRuleValue) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/promotion-rule.ts b/packages/promotion/src/services/promotion-rule.ts deleted file mode 100644 index a8654617c7c5d..0000000000000 --- a/packages/promotion/src/services/promotion-rule.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { PromotionRule } from "@models" -import { CreatePromotionRuleDTO, UpdatePromotionRuleDTO } from "../types" - -type InjectedDependencies = { - promotionRuleRepository: DAL.RepositoryService -} - -export default class PromotionRuleService< - TEntity extends PromotionRule = PromotionRule -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionRuleDTO - update: UpdatePromotionRuleDTO - }, - { - list: PromotionTypes.FilterablePromotionRuleProps - listAndCount: PromotionTypes.FilterablePromotionRuleProps - } ->(PromotionRule) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/services/promotion.ts b/packages/promotion/src/services/promotion.ts deleted file mode 100644 index 74c8e71ba3274..0000000000000 --- a/packages/promotion/src/services/promotion.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DAL, PromotionTypes } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { Promotion } from "@models" -import { CreatePromotionDTO, UpdatePromotionDTO } from "../types" - -type InjectedDependencies = { - promotionRepository: DAL.RepositoryService -} - -export default class PromotionService< - TEntity extends Promotion = Promotion -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreatePromotionDTO - update: UpdatePromotionDTO - }, - { - list: PromotionTypes.FilterablePromotionProps - listAndCount: PromotionTypes.FilterablePromotionProps - } ->(Promotion) { - constructor(...args: any[]) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/promotion/src/types/index.ts b/packages/promotion/src/types/index.ts index 80891678e3717..145c717bae997 100644 --- a/packages/promotion/src/types/index.ts +++ b/packages/promotion/src/types/index.ts @@ -10,4 +10,3 @@ export * from "./campaign-budget" export * from "./promotion" export * from "./promotion-rule" export * from "./promotion-rule-value" -export * from "./repositories" diff --git a/packages/promotion/src/types/repositories.ts b/packages/promotion/src/types/repositories.ts deleted file mode 100644 index bfa06a3073535..0000000000000 --- a/packages/promotion/src/types/repositories.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - ApplicationMethod, - Campaign, - CampaignBudget, - Promotion, - PromotionRule, - PromotionRuleValue, -} from "@models" -import { DAL } from "@medusajs/types" -import { - CreateApplicationMethodDTO, - UpdateApplicationMethodDTO, -} from "./application-method" -import { CreateCampaignDTO, UpdateCampaignDTO } from "./campaign" -import { - CreateCampaignBudgetDTO, - UpdateCampaignBudgetDTO, -} from "./campaign-budget" -import { CreatePromotionDTO, UpdatePromotionDTO } from "./promotion" -import { - CreatePromotionRuleDTO, - UpdatePromotionRuleDTO, -} from "./promotion-rule" -import { - CreatePromotionRuleValueDTO, - UpdatePromotionRuleValueDTO, -} from "./promotion-rule-value" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IApplicationMethodRepository< - TEntity extends ApplicationMethod = ApplicationMethod -> extends DAL.RepositoryService< - TEntity, - { - create: CreateApplicationMethodDTO - update: UpdateApplicationMethodDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICampaignRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreateCampaignDTO - update: UpdateCampaignDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ICampaignBudgetRepository< - TEntity extends CampaignBudget = CampaignBudget -> extends DAL.RepositoryService< - TEntity, - { - create: CreateCampaignBudgetDTO - update: UpdateCampaignBudgetDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRepository - extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionDTO - Update: UpdatePromotionDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRuleRepository< - TEntity extends PromotionRule = PromotionRule -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionRuleDTO - update: UpdatePromotionRuleDTO - } - > {} - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IPromotionRuleValueRepository< - TEntity extends PromotionRuleValue = PromotionRuleValue -> extends DAL.RepositoryService< - TEntity, - { - create: CreatePromotionRuleValueDTO - update: UpdatePromotionRuleValueDTO - } - > {} diff --git a/packages/promotion/src/utils/compute-actions/buy-get.ts b/packages/promotion/src/utils/compute-actions/buy-get.ts index cc6eaa44e114f..d3bcc3f68d249 100644 --- a/packages/promotion/src/utils/compute-actions/buy-get.ts +++ b/packages/promotion/src/utils/compute-actions/buy-get.ts @@ -46,7 +46,10 @@ export function getComputedActionsForBuyGet( const validItemsForTargetRules = itemsContext .filter((item) => areRulesValidForContext(targetRules, item)) .sort((a, b) => { - return b.unit_price - a.unit_price + const aPrice = a.subtotal / a.quantity + const bPrice = b.subtotal / b.quantity + + return bPrice - aPrice }) let remainingQtyToApply = applyToQuantity @@ -54,7 +57,7 @@ export function getComputedActionsForBuyGet( for (const method of validItemsForTargetRules) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 const multiplier = Math.min(method.quantity, remainingQtyToApply) - const amount = method.unit_price * multiplier + const amount = (method.subtotal / method.quantity) * multiplier const newRemainingQtyToApply = remainingQtyToApply - multiplier if (newRemainingQtyToApply < 0 || amount <= 0) { diff --git a/packages/promotion/src/utils/compute-actions/items.ts b/packages/promotion/src/utils/compute-actions/items.ts index afbf400b564ee..555a27548f612 100644 --- a/packages/promotion/src/utils/compute-actions/items.ts +++ b/packages/promotion/src/utils/compute-actions/items.ts @@ -5,6 +5,7 @@ import { import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -67,10 +68,15 @@ export function applyPromotionToItems( method.quantity, applicationMethod?.max_quantity! ) - const promotionValue = - parseFloat(applicationMethod!.value!) * quantityMultiplier - const applicableTotal = - method.unit_price * quantityMultiplier - appliedPromoValue + const totalItemValue = + (method.subtotal / method.quantity) * quantityMultiplier + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = totalItemValue - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -106,18 +112,32 @@ export function applyPromotionToItems( ) { const totalApplicableValue = items!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price * method.quantity - appliedPromoValue + return ( + acc + + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + ) }, 0) for (const method of items!) { - const promotionValue = parseFloat(applicationMethod!.value!) const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 + const promotionValue = parseFloat(applicationMethod!.value!) const applicableTotal = - method.unit_price * method.quantity - appliedPromoValue + (method.subtotal / method.quantity) * method.quantity - + appliedPromoValue + + if (applicableTotal <= 0) { + continue + } // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { @@ -135,6 +155,8 @@ export function applyPromotionToItems( continue } + methodIdPromoValueMap.set(method.id, appliedPromoValue + amount) + computedActions.push({ action: ComputedActions.ADD_ITEM_ADJUSTMENT, item_id: method.id, diff --git a/packages/promotion/src/utils/compute-actions/shipping-methods.ts b/packages/promotion/src/utils/compute-actions/shipping-methods.ts index 440021f482bdf..6e96db1fd2aec 100644 --- a/packages/promotion/src/utils/compute-actions/shipping-methods.ts +++ b/packages/promotion/src/utils/compute-actions/shipping-methods.ts @@ -2,6 +2,7 @@ import { PromotionTypes } from "@medusajs/types" import { ApplicationMethodAllocation, ApplicationMethodTargetType, + ApplicationMethodType, ComputedActions, MedusaError, } from "@medusajs/utils" @@ -55,8 +56,13 @@ export function applyPromotionToShippingMethods( if (allocation === ApplicationMethodAllocation.EACH) { for (const method of shippingMethods!) { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price - appliedPromoValue + let promotionValue = parseFloat(applicationMethod!.value!) + const applicableTotal = method.subtotal - appliedPromoValue + + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + promotionValue = (promotionValue / 100) * applicableTotal + } + const amount = Math.min(promotionValue, applicableTotal) if (amount <= 0) { @@ -89,7 +95,7 @@ export function applyPromotionToShippingMethods( const totalApplicableValue = shippingMethods!.reduce((acc, method) => { const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 - return acc + method.unit_price - appliedPromoValue + return acc + method.subtotal - appliedPromoValue }, 0) if (totalApplicableValue <= 0) { @@ -98,14 +104,19 @@ export function applyPromotionToShippingMethods( for (const method of shippingMethods!) { const promotionValue = parseFloat(applicationMethod!.value!) - const applicableTotal = method.unit_price + const applicableTotal = method.subtotal const appliedPromoValue = methodIdPromoValueMap.get(method.id) || 0 // TODO: should we worry about precision here? - const applicablePromotionValue = + let applicablePromotionValue = (applicableTotal / totalApplicableValue) * promotionValue - appliedPromoValue + if (applicationMethod?.type === ApplicationMethodType.PERCENTAGE) { + applicablePromotionValue = + (promotionValue / 100) * (applicableTotal - appliedPromoValue) + } + const amount = Math.min(applicablePromotionValue, applicableTotal) if (amount <= 0) { diff --git a/packages/promotion/src/utils/validations/application-method.ts b/packages/promotion/src/utils/validations/application-method.ts index 99c6296349cc4..7d022f0e9d519 100644 --- a/packages/promotion/src/utils/validations/application-method.ts +++ b/packages/promotion/src/utils/validations/application-method.ts @@ -37,11 +37,23 @@ export function validateApplicationMethodAttributes( const applyToQuantity = data.apply_to_quantity || applicationMethod?.apply_to_quantity const targetType = data.target_type || applicationMethod?.target_type + const type = data.type || applicationMethod?.type const applicationMethodType = data.type || applicationMethod?.type + const value = parseFloat(data.value! || applicationMethod?.value!) const maxQuantity = data.max_quantity || applicationMethod.max_quantity const allocation = data.allocation || applicationMethod.allocation const allTargetTypes: string[] = Object.values(ApplicationMethodTargetType) + if ( + type === ApplicationMethodType.PERCENTAGE && + (value <= 0 || value > 100) + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Application Method value should be a percentage number between 0 and 100` + ) + } + if (promotion?.type === PromotionType.BUYGET) { if (!isPresent(applyToQuantity)) { throw new MedusaError( diff --git a/packages/sales-channel/src/services/__fixtures__/sales-channel.ts b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts index c4a716dde4ad6..4549d004b4080 100644 --- a/packages/sales-channel/src/services/__fixtures__/sales-channel.ts +++ b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts @@ -1,10 +1,6 @@ -import { SalesChannelService, SalesChannelModuleService } from "@services" -import { asClass, asValue, createContainer } from "awilix" +import { asValue } from "awilix" -export const mockContainer = createContainer() - -mockContainer.register({ - transaction: asValue(async (task) => await task()), +export const salesChannelRepositoryMock = { salesChannelRepository: asValue({ find: jest.fn().mockImplementation(async ({ where: { code } }) => { return [{}] @@ -12,6 +8,4 @@ mockContainer.register({ findAndCount: jest.fn().mockResolvedValue([[], 0]), getFreshManager: jest.fn().mockResolvedValue({}), }), - salesChannelService: asClass(SalesChannelService), - salesChannelModuleService: asClass(SalesChannelModuleService), -}) +} diff --git a/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts index d8dab120bdf96..61e5bb1a1f4ff 100644 --- a/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts +++ b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts @@ -1,15 +1,25 @@ -import { mockContainer } from "../__fixtures__/sales-channel" +import { createMedusaContainer } from "@medusajs/utils" +import { asValue } from "awilix" +import ContainerLoader from "../../loaders/container" +import { salesChannelRepositoryMock } from "../__fixtures__/sales-channel" describe("Sales channel service", function () { - beforeEach(function () { + let container + + beforeEach(async function () { jest.clearAllMocks() + + container = createMedusaContainer() + container.register("manager", asValue({})) + + await ContainerLoader({ container }) + + container.register(salesChannelRepositoryMock) }) it("should list sales channels with filters and relations", async function () { - const salesChannelRepository = mockContainer.resolve( - "salesChannelRepository" - ) - const salesChannelService = mockContainer.resolve("salesChannelService") + const salesChannelRepository = container.resolve("salesChannelRepository") + const salesChannelService = container.resolve("salesChannelService") const config = { select: ["id", "name"], diff --git a/packages/sales-channel/src/services/index.ts b/packages/sales-channel/src/services/index.ts index 117f8a36c6b86..d00ec9ecfc719 100644 --- a/packages/sales-channel/src/services/index.ts +++ b/packages/sales-channel/src/services/index.ts @@ -1,2 +1 @@ -export { default as SalesChannelService } from "./sales-channel" export { default as SalesChannelModuleService } from "./sales-channel-module" diff --git a/packages/sales-channel/src/services/sales-channel-module.ts b/packages/sales-channel/src/services/sales-channel-module.ts index 270629f7c67a4..b9d02de4eaa58 100644 --- a/packages/sales-channel/src/services/sales-channel-module.ts +++ b/packages/sales-channel/src/services/sales-channel-module.ts @@ -1,48 +1,48 @@ import { Context, + CreateSalesChannelDTO, DAL, - FilterableSalesChannelProps, - FindConfig, InternalModuleDeclaration, ISalesChannelModuleService, ModuleJoinerConfig, - RestoreReturn, + ModulesSdkTypes, SalesChannelDTO, - SoftDeleteReturn, + UpdateSalesChannelDTO, } from "@medusajs/types" import { - InjectManager, InjectTransactionManager, - mapObjectTo, MedusaContext, + ModulesSdkUtils, } from "@medusajs/utils" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" import { SalesChannel } from "@models" -import SalesChannelService from "./sales-channel" -import { - joinerConfig, - entityNameToLinkableKeysMap, - LinkableKeys, -} from "../joiner-config" +import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - salesChannelService: SalesChannelService + salesChannelService: ModulesSdkTypes.InternalModuleService } export default class SalesChannelModuleService< - TEntity extends SalesChannel = SalesChannel -> implements ISalesChannelModuleService + TEntity extends SalesChannel = SalesChannel + > + extends ModulesSdkUtils.abstractModuleServiceFactory< + InjectedDependencies, + SalesChannelDTO, + {} + >(SalesChannel, [], entityNameToLinkableKeysMap) + implements ISalesChannelModuleService { protected baseRepository_: DAL.RepositoryService - protected readonly salesChannelService_: SalesChannelService + protected readonly salesChannelService_: ModulesSdkTypes.InternalModuleService constructor( { baseRepository, salesChannelService }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { + // @ts-ignore + super(...arguments) this.baseRepository_ = baseRepository this.salesChannelService_ = salesChannelService } @@ -78,95 +78,6 @@ export default class SalesChannelModuleService< ) } - async delete(ids: string[], sharedContext?: Context): Promise - - async delete(id: string, sharedContext?: Context): Promise - - @InjectTransactionManager("baseRepository_") - async delete( - ids: string | string[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannelIds = Array.isArray(ids) ? ids : [ids] - await this.salesChannelService_.delete(salesChannelIds, sharedContext) - } - - @InjectTransactionManager("baseRepository_") - protected async softDelete_( - salesChannelIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this.salesChannelService_.softDelete( - salesChannelIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async softDelete< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - salesChannelIds: string[], - { returnLinkableKeys }: SoftDeleteReturn = {}, - sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.softDelete_( - salesChannelIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - - @InjectTransactionManager("baseRepository_") - async restore_( - salesChannelIds: string[], - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this.salesChannelService_.restore( - salesChannelIds, - sharedContext - ) - } - - @InjectManager("baseRepository_") - async restore< - TReturnableLinkableKeys extends string = Lowercase< - keyof typeof LinkableKeys - > - >( - salesChannelIds: string[], - { returnLinkableKeys }: RestoreReturn = {}, - sharedContext: Context = {} - ): Promise, string[]> | void> { - const [_, cascadedEntitiesMap] = await this.restore_( - salesChannelIds, - sharedContext - ) - - let mappedCascadedEntitiesMap - if (returnLinkableKeys) { - mappedCascadedEntitiesMap = mapObjectTo< - Record, string[]> - >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { - pick: returnLinkableKeys, - }) - } - - return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 - } - async update( data: UpdateSalesChannelDTO[], sharedContext?: Context @@ -193,55 +104,4 @@ export default class SalesChannelModuleService< } ) } - - @InjectManager("baseRepository_") - async retrieve( - salesChannelId: string, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannel = await this.salesChannelService_.retrieve( - salesChannelId, - config - ) - - return await this.baseRepository_.serialize(salesChannel, { - populate: true, - }) - } - - @InjectManager("baseRepository_") - async list( - filters: {} = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const salesChannels = await this.salesChannelService_.list(filters, config) - - return await this.baseRepository_.serialize( - salesChannels, - { - populate: true, - } - ) - } - - @InjectManager("baseRepository_") - async listAndCount( - filters: FilterableSalesChannelProps = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[SalesChannelDTO[], number]> { - const [salesChannels, count] = await this.salesChannelService_.listAndCount( - filters, - config - ) - - return [ - await this.baseRepository_.serialize(salesChannels, { - populate: true, - }), - count, - ] - } } diff --git a/packages/sales-channel/src/services/sales-channel.ts b/packages/sales-channel/src/services/sales-channel.ts deleted file mode 100644 index 83e6ef90b4bcd..0000000000000 --- a/packages/sales-channel/src/services/sales-channel.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" - -import { SalesChannel } from "@models" - -type InjectedDependencies = { - salesChannelRepository: DAL.RepositoryService -} - -export default class SalesChannelService< - TEntity extends SalesChannel = SalesChannel -> extends ModulesSdkUtils.abstractServiceFactory< - InjectedDependencies, - { - create: CreateSalesChannelDTO - update: UpdateSalesChannelDTO - } ->(SalesChannel) { - constructor(container: InjectedDependencies) { - // @ts-ignore - super(...arguments) - } -} diff --git a/packages/sales-channel/src/types/repositories.ts b/packages/sales-channel/src/types/repositories.ts deleted file mode 100644 index e1a90fef2194c..0000000000000 --- a/packages/sales-channel/src/types/repositories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { DAL } from "@medusajs/types" - -// eslint-disable-next-line @typescript-eslint/no-empty-interface -import { SalesChannel } from "@models" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" - -export interface ISalesChannelRepository< - TEntity extends SalesChannel = SalesChannel -> extends DAL.RepositoryService< - TEntity, - { - create: CreateSalesChannelDTO - update: UpdateSalesChannelDTO - } - > {} diff --git a/packages/types/src/auth/common/auth-provider.ts b/packages/types/src/auth/common/auth-provider.ts index e3653642136f8..ddf833879748f 100644 --- a/packages/types/src/auth/common/auth-provider.ts +++ b/packages/types/src/auth/common/auth-provider.ts @@ -3,7 +3,7 @@ import { BaseFilterable } from "../../dal" export type AuthProviderDTO = { provider: string name: string - domain: ProviderDomain + scope?: string is_active: boolean config: Record | null } @@ -11,29 +11,23 @@ export type AuthProviderDTO = { export type CreateAuthProviderDTO = { provider: string name: string - domain?: ProviderDomain + scope?: string is_active?: boolean - config?: Record + config?: Record } export type UpdateAuthProviderDTO = { provider: string name?: string - domain?: ProviderDomain is_active?: boolean config?: Record } -export enum ProviderDomain { - ALL = "all", - STORE = "store", - ADMIN = "admin", -} - export interface FilterableAuthProviderProps extends BaseFilterable { + id?: string | string[] provider?: string[] is_active?: boolean - domain?: ProviderDomain[] + scope?: string[] name?: string[] } diff --git a/packages/types/src/auth/common/auth-user.ts b/packages/types/src/auth/common/auth-user.ts index 11357a10ebe8a..1946e360830a2 100644 --- a/packages/types/src/auth/common/auth-user.ts +++ b/packages/types/src/auth/common/auth-user.ts @@ -1,10 +1,11 @@ -import { BaseFilterable } from "../../dal" import { AuthProviderDTO } from "./auth-provider" +import { BaseFilterable } from "../../dal" export type AuthUserDTO = { id: string provider_id: string entity_id: string + scope: string provider: AuthProviderDTO provider_metadata?: Record user_metadata: Record @@ -12,8 +13,9 @@ export type AuthUserDTO = { } export type CreateAuthUserDTO = { - provider_id: string + provider: string entity_id: string + scope: string provider_metadata?: Record user_metadata?: Record app_metadata?: Record diff --git a/packages/types/src/auth/common/provider.ts b/packages/types/src/auth/common/provider.ts index 03dfb74b2e0f5..a13f90052d41d 100644 --- a/packages/types/src/auth/common/provider.ts +++ b/packages/types/src/auth/common/provider.ts @@ -10,13 +10,13 @@ export type AuthModuleProviderConfig = { scopes: Record } -export type AuthProviderScope = { domain?: string } & Record +export type AuthProviderScope = Record export type AuthenticationInput = { - connection: { encrypted: boolean } url: string headers: Record query: Record body: Record - scope: string + authScope: string + protocol: string } diff --git a/packages/types/src/auth/service.ts b/packages/types/src/auth/service.ts index 88b72362db24b..1dd4c4ecc2cc2 100644 --- a/packages/types/src/auth/service.ts +++ b/packages/types/src/auth/service.ts @@ -68,7 +68,7 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise - deleteAuthProvider(ids: string[], sharedContext?: Context): Promise + deleteAuthProviders(ids: string[], sharedContext?: Context): Promise retrieveAuthUser( id: string, @@ -118,5 +118,5 @@ export interface IAuthModuleService extends IModuleService { sharedContext?: Context ): Promise - deleteAuthUser(ids: string[], sharedContext?: Context): Promise + deleteAuthUsers(ids: string[], sharedContext?: Context): Promise } diff --git a/packages/types/src/common/common.ts b/packages/types/src/common/common.ts index 99c3dd9367f26..d97ebd31fb8bc 100644 --- a/packages/types/src/common/common.ts +++ b/packages/types/src/common/common.ts @@ -4,11 +4,11 @@ import { FindOperator, FindOptionsSelect, FindOptionsWhere, - OrderByCondition -} from "typeorm"; + OrderByCondition, +} from "typeorm" -import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder"; -import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations"; +import { FindOptionsOrder } from "typeorm/find-options/FindOptionsOrder" +import { FindOptionsRelations } from "typeorm/find-options/FindOptionsRelations" /** * Utility type used to remove some optional attributes (coming from K) from a type T @@ -230,3 +230,17 @@ export interface FindPaginationParams { offset?: number limit?: number } + +export type Pluralize = Singular extends `${infer R}y` + ? `${R}ies` + : Singular extends `${infer R}es` + ? `${Singular}` + : Singular extends + | `${infer R}ss` + | `${infer R}sh` + | `${infer R}ch` + | `${infer R}x` + | `${infer R}z` + | `${infer R}o` + ? `${Singular}es` + : `${Singular}s` diff --git a/packages/types/src/customer/service.ts b/packages/types/src/customer/service.ts index 8143bd213a22e..b2c348ffe03e5 100644 --- a/packages/types/src/customer/service.ts +++ b/packages/types/src/customer/service.ts @@ -3,15 +3,15 @@ import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { + CustomerAddressDTO, CustomerDTO, - CustomerGroupDTO, CustomerGroupCustomerDTO, + CustomerGroupDTO, + FilterableCustomerAddressProps, FilterableCustomerGroupCustomerProps, - FilterableCustomerProps, FilterableCustomerGroupProps, + FilterableCustomerProps, GroupCustomerPair, - FilterableCustomerAddressProps, - CustomerAddressDTO, } from "./common" import { CreateCustomerAddressDTO, @@ -58,11 +58,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise + // TODO should be pluralized createCustomerGroup( data: CreateCustomerGroupDTO[], sharedContext?: Context ): Promise + // TODO should be pluralized createCustomerGroup( data: CreateCustomerGroupDTO, sharedContext?: Context @@ -74,28 +76,30 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise - updateCustomerGroup( + updateCustomerGroups( groupId: string, data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - updateCustomerGroup( + + updateCustomerGroups( groupIds: string[], data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - updateCustomerGroup( + + updateCustomerGroups( selector: FilterableCustomerGroupProps, data: CustomerGroupUpdatableFields, sharedContext?: Context ): Promise - deleteCustomerGroup(groupId: string, sharedContext?: Context): Promise - deleteCustomerGroup( + deleteCustomerGroups(groupId: string, sharedContext?: Context): Promise + deleteCustomerGroups( groupIds: string[], sharedContext?: Context ): Promise - deleteCustomerGroup( + deleteCustomerGroups( selector: FilterableCustomerGroupProps, sharedContext?: Context ): Promise @@ -110,10 +114,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise<{ id: string }[]> + // TODO should be pluralized removeCustomerFromGroup( groupCustomerPair: GroupCustomerPair, sharedContext?: Context ): Promise + + // TODO should be pluralized removeCustomerFromGroup( groupCustomerPairs: GroupCustomerPair[], sharedContext?: Context @@ -128,25 +135,26 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise - updateAddress( + updateAddresses( addressId: string, data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - updateAddress( + updateAddresses( addressIds: string[], data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - updateAddress( + + updateAddresses( selector: FilterableCustomerAddressProps, data: UpdateCustomerAddressDTO, sharedContext?: Context ): Promise - deleteAddress(addressId: string, sharedContext?: Context): Promise - deleteAddress(addressIds: string[], sharedContext?: Context): Promise - deleteAddress( + deleteAddresses(addressId: string, sharedContext?: Context): Promise + deleteAddresses(addressIds: string[], sharedContext?: Context): Promise + deleteAddresses( selector: FilterableCustomerAddressProps, sharedContext?: Context ): Promise @@ -163,7 +171,7 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise<[CustomerAddressDTO[], number]> - listCustomerGroupRelations( + listCustomerGroupCustomers( filters?: FilterableCustomerGroupCustomerProps, config?: FindConfig, sharedContext?: Context @@ -205,13 +213,13 @@ export interface ICustomerModuleService extends IModuleService { sharedContext?: Context ): Promise | void> - softDeleteCustomerGroup( + softDeleteCustomerGroups( groupIds: string[], config?: SoftDeleteReturn, sharedContext?: Context ): Promise | void> - restoreCustomerGroup( + restoreCustomerGroups( groupIds: string[], config?: RestoreReturn, sharedContext?: Context diff --git a/packages/types/src/dal/repository-service.ts b/packages/types/src/dal/repository-service.ts index cc18daff1aca3..a2f5339e8b60c 100644 --- a/packages/types/src/dal/repository-service.ts +++ b/packages/types/src/dal/repository-service.ts @@ -1,6 +1,11 @@ import { RepositoryTransformOptions } from "../common" import { Context } from "../shared-context" -import { FindOptions } from "./index" +import { + BaseFilterable, + FilterQuery as InternalFilterQuery, + FilterQuery, + FindOptions, +} from "./index" /** * Data access layer (DAL) interface to implements for any repository service. @@ -29,12 +34,7 @@ interface BaseRepositoryService { type DtoBasedMutationMethods = "create" | "update" -export interface RepositoryService< - T = any, - TDTOs extends { [K in DtoBasedMutationMethods]?: any } = { - [K in DtoBasedMutationMethods]?: any - } -> extends BaseRepositoryService { +export interface RepositoryService extends BaseRepositoryService { find(options?: FindOptions, context?: Context): Promise findAndCount( @@ -42,34 +42,34 @@ export interface RepositoryService< context?: Context ): Promise<[T[], number]> - create(data: TDTOs["create"][], context?: Context): Promise + create(data: any[], context?: Context): Promise - update(data: TDTOs["update"][], context?: Context): Promise + update(data: { entity; update }[], context?: Context): Promise - delete(idsOrPKs: string[] | object[], context?: Context): Promise + delete( + idsOrPKs: FilterQuery & BaseFilterable>, + context?: Context + ): Promise /** * Soft delete entities and cascade to related entities if configured. * - * @param ids + * @param idsOrFilter * @param context * * @returns [T[], Record] the second value being the map of the entity names and ids that were soft deleted */ softDelete( - ids: string[], + idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> restore( - ids: string[], + idsOrFilter: string[] | InternalFilterQuery, context?: Context ): Promise<[T[], Record]> - upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - context?: Context - ): Promise + upsert(data: any[], context?: Context): Promise } export interface TreeRepositoryService diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index c69a3e90a1681..8780aa80ec829 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -10,6 +10,7 @@ import { Logger } from "../logger" export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" +export * from "./internal-module-service" export type LogLevel = | "query" diff --git a/packages/types/src/modules-sdk/internal-module-service.ts b/packages/types/src/modules-sdk/internal-module-service.ts new file mode 100644 index 0000000000000..10c1622511264 --- /dev/null +++ b/packages/types/src/modules-sdk/internal-module-service.ts @@ -0,0 +1,81 @@ +import { FindConfig } from "../common" +import { Context } from "../shared-context" +import { + BaseFilterable, + FilterQuery as InternalFilterQuery, + FilterQuery, +} from "../dal" + +export interface InternalModuleService< + TEntity extends {}, + TContainer extends object = object +> { + get __container__(): TContainer + + retrieve( + idOrObject: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + retrieve( + idOrObject: object, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: FilterQuery | BaseFilterable>, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: FilterQuery | BaseFilterable>, + config?: FindConfig, + sharedContext?: Context + ): Promise<[TEntity[], number]> + + create(data: any[], sharedContext?: Context): Promise + create(data: any, sharedContext?: Context): Promise + + update(data: any[], sharedContext?: Context): Promise + update(data: any, sharedContext?: Context): Promise + update( + selectorAndData: { + selector: FilterQuery | BaseFilterable> + data: any + }, + sharedContext?: Context + ): Promise + update( + selectorAndData: { + selector: FilterQuery | BaseFilterable> + data: any + }[], + sharedContext?: Context + ): Promise + + delete(idOrSelector: string, sharedContext?: Context): Promise + delete(idOrSelector: string[], sharedContext?: Context): Promise + delete(idOrSelector: object, sharedContext?: Context): Promise + delete(idOrSelector: object[], sharedContext?: Context): Promise + delete( + idOrSelector: { + selector: FilterQuery | BaseFilterable> + }, + sharedContext?: Context + ): Promise + + softDelete( + idsOrFilter: string[] | InternalFilterQuery, + sharedContext?: Context + ): Promise<[TEntity[], Record]> + + restore( + idsOrFilter: string[] | InternalFilterQuery, + sharedContext?: Context + ): Promise<[TEntity[], Record]> + + upsert(data: any[], sharedContext?: Context): Promise + upsert(data: any, sharedContext?: Context): Promise +} diff --git a/packages/types/src/modules-sdk/module-service.ts b/packages/types/src/modules-sdk/module-service.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/types/src/payment/service.ts b/packages/types/src/payment/service.ts index 94951f55455c0..ed5c12d60151f 100644 --- a/packages/types/src/payment/service.ts +++ b/packages/types/src/payment/service.ts @@ -54,11 +54,11 @@ export interface IPaymentModuleService extends IModuleService { sharedContext?: Context ): Promise - deletePaymentCollection( + deletePaymentCollections( paymentCollectionId: string[], sharedContext?: Context ): Promise - deletePaymentCollection( + deletePaymentCollections( paymentCollectionId: string, sharedContext?: Context ): Promise diff --git a/packages/types/src/pricing/service.ts b/packages/types/src/pricing/service.ts index bf433aaa890e5..0a88f48e95c6c 100644 --- a/packages/types/src/pricing/service.ts +++ b/packages/types/src/pricing/service.ts @@ -47,7 +47,7 @@ import { import { FindConfig } from "../common" import { ModuleJoinerConfig } from "../modules-sdk" import { Context } from "../shared-context" -import { RestoreReturn } from "../dal" +import { RestoreReturn, SoftDeleteReturn } from "../dal" export interface IPricingModuleService { /** @@ -1279,6 +1279,7 @@ export interface IPricingModuleService { * This method soft deletes money amounts by their IDs. * * @param {string[]} ids - The IDs of the money amounts to delete. + * @param {SoftDeleteReturn} config * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} Resolves when the money amounts are successfully deleted. * @@ -1295,7 +1296,11 @@ export interface IPricingModuleService { * ) * } */ - softDeleteMoneyAmounts(ids: string[], sharedContext?: Context): Promise + softDeleteMoneyAmounts( + ids: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> /** * This method restores soft deleted money amounts by their IDs. @@ -1305,10 +1310,10 @@ export interface IPricingModuleService { * Configurations determining which relations to restore along with each of the money amounts. You can pass to its `returnLinkableKeys` * property any of the money amount's relation attribute names, such as `price_set_money_amount`. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored, such as the ID of associated price set money amounts. - * The object's keys are the ID attribute names of the money amount entity's relations, such as `price_set_money_amount_id`, - * and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, + * @returns {Promise | void>} + * An object that includes the IDs of related records that were restored, such as the ID of associated price set money amounts. + * The object's keys are the ID attribute names of the money amount entity's relations, such as `price_set_money_amount_id`, + * and its value is an array of strings, each being the ID of the record associated with the money amount through this relation, * such as the IDs of associated price set money amounts. * * @example @@ -1324,7 +1329,7 @@ export interface IPricingModuleService { * ) * } */ - restoreDeletedMoneyAmounts( + restoreMoneyAmounts( ids: string[], config?: RestoreReturn, sharedContext?: Context diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index 86c4593e16f23..9477fd31ef756 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -1493,24 +1493,24 @@ export interface IProductModuleService { /** * This method is used to update a product's variants. - * + * * @param {UpdateProductVariantDTO[]} data - The product variants to update. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The updated product variants's details. - * + * * @example * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * import { + * import { * UpdateProductVariantDTO * } from "@medusajs/product/dist/types/services/product-variant" - * + * * async function updateProductVariants (items: UpdateProductVariantDTO[]) { * const productModule = await initializeProductModule() - * + * * const productVariants = await productModule.updateVariants(items) - * + * * // do something with the product variants or return them * } */ @@ -1521,24 +1521,24 @@ export interface IProductModuleService { /** * This method is used to create variants for a product. - * + * * @param {CreateProductVariantDTO[]} data - The product variants to create. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise} The created product variants' details. - * + * * @example * import { * initialize as initializeProductModule, * } from "@medusajs/product" - * + * * async function createProductVariants (items: { * product_id: string, * title: string * }[]) { * const productModule = await initializeProductModule() - * + * * const productVariants = await productModule.createVariants(items) - * + * * // do something with the product variants or return them * } */ @@ -2513,16 +2513,16 @@ export interface IProductModuleService { ): Promise | void> /** - * This method is used to restore product varaints that were soft deleted. Product variants are soft deleted when they're not + * This method is used to restore product varaints that were soft deleted. Product variants are soft deleted when they're not * provided in a product's details passed to the {@link update} method. - * + * * @param {string[]} variantIds - The IDs of the variants to restore. - * @param {RestoreReturn} config - + * @param {RestoreReturn} config - * Configurations determining which relations to restore along with each of the product variants. You can pass to its `returnLinkableKeys` * property any of the product variant's relation attribute names. * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. * @returns {Promise | void>} - * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product variant entity's relations + * An object that includes the IDs of related records that were restored. The object's keys are the ID attribute names of the product variant entity's relations * and its value is an array of strings, each being the ID of the record associated with the product variant through this relation. * * If there are no related records that were restored, the promise resolved to `void`. diff --git a/packages/types/src/promotion/common/compute-actions.ts b/packages/types/src/promotion/common/compute-actions.ts index 267da4b5c38d9..1384daa93fe5f 100644 --- a/packages/types/src/promotion/common/compute-actions.ts +++ b/packages/types/src/promotion/common/compute-actions.ts @@ -51,13 +51,13 @@ export interface ComputeActionAdjustmentLine extends Record { export interface ComputeActionItemLine extends Record { id: string quantity: number - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] } export interface ComputeActionShippingLine extends Record { id: string - unit_price: number + subtotal: number adjustments?: ComputeActionAdjustmentLine[] } diff --git a/packages/utils/src/common/__tests__/pluralize.spec.ts b/packages/utils/src/common/__tests__/pluralize.spec.ts new file mode 100644 index 0000000000000..8f3301bc05363 --- /dev/null +++ b/packages/utils/src/common/__tests__/pluralize.spec.ts @@ -0,0 +1,33 @@ +import { pluralize } from "../plurailze" + +describe("pluralize", function () { + it("should pluralize any words", function () { + const words = [ + "apple", + "box", + "day", + "country", + "baby", + "knife", + "hero", + "potato", + "address", + ] + + const expectedOutput = [ + "apples", + "boxes", + "days", + "countries", + "babies", + "knives", + "heroes", + "potatoes", + "addresses", + ] + + words.forEach((word, index) => { + expect(pluralize(word)).toBe(expectedOutput[index]) + }) + }) +}) diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 7dc7fd9831041..4f47fcc1ea4d0 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -26,6 +26,7 @@ export * from "./object-from-string-path" export * from "./object-to-string-path" export * from "./optional-numeric-serializer" export * from "./pick-value-from-object" +export * from "./plurailze" export * from "./promise-all" export * from "./remote-query-object-from-string" export * from "./remote-query-object-to-string" diff --git a/packages/utils/src/common/plurailze.ts b/packages/utils/src/common/plurailze.ts new file mode 100644 index 0000000000000..f35fb2d89bd88 --- /dev/null +++ b/packages/utils/src/common/plurailze.ts @@ -0,0 +1,27 @@ +/** + * Some library provide pluralize function with language specific rules. + * This is a simple implementation of pluralize function. + * @param word + */ +export function pluralize(word: string): string { + // Add basic rules for forming plurals + if ( + //word.endsWith("s") || + word.endsWith("sh") || + word.endsWith("ss") || + word.endsWith("ch") || + word.endsWith("x") || + word.endsWith("o") || + word.endsWith("z") + ) { + return word + "es" + } else if (word.endsWith("y") && !"aeiou".includes(word[word.length - 2])) { + return word.slice(0, -1) + "ies" + } else if (word.endsWith("es")) { + return word + } else if (word.endsWith("fe")) { + return word.slice(0, -2) + "ves" + } else { + return word + "s" + } +} diff --git a/packages/utils/src/dal/index.ts b/packages/utils/src/dal/index.ts index 5085c6d0a3754..2d87ea47cfaed 100644 --- a/packages/utils/src/dal/index.ts +++ b/packages/utils/src/dal/index.ts @@ -3,5 +3,4 @@ export * from "./mikro-orm/mikro-orm-repository" export * from "./mikro-orm/mikro-orm-soft-deletable-filter" export * from "./mikro-orm/utils" export * from "./repositories" -export * from "./repository" export * from "./utils" 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 ea4d185af60ff..745b0477954a4 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -1,6 +1,8 @@ import { + BaseFilterable, Context, DAL, + FilterQuery, FilterQuery as InternalFilterQuery, RepositoryService, RepositoryTransformOptions, @@ -17,11 +19,11 @@ import { EntityName, FilterQuery as MikroFilterQuery, } from "@mikro-orm/core/typings" -import { MedusaError, isString } from "../../common" +import { isString } from "../../common" import { + buildQuery, InjectTransactionManager, MedusaContext, - buildQuery, } from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, @@ -42,10 +44,10 @@ export class MikroOrmBase { : this.manager_) as unknown as TManager } - getActiveManager( - @MedusaContext() - { transactionManager, manager }: Context = {} - ): TManager { + getActiveManager({ + transactionManager, + manager, + }: Context = {}): TManager { return (transactionManager ?? manager ?? this.manager_) as TManager } @@ -77,9 +79,10 @@ export class MikroOrmBase { * related ones. */ -export class MikroOrmBaseRepository< - T extends object = object -> extends MikroOrmBase { +export class MikroOrmBaseRepository + extends MikroOrmBase + implements RepositoryService +{ constructor(...args: any[]) { // @ts-ignore super(...arguments) @@ -89,11 +92,14 @@ export class MikroOrmBaseRepository< throw new Error("Method not implemented.") } - update(data: unknown[], context?: Context): Promise { + update(data: { entity; update }[], context?: Context): Promise { throw new Error("Method not implemented.") } - delete(ids: string[] | object[], context?: Context): Promise { + delete( + idsOrPKs: FilterQuery & BaseFilterable>, + context?: Context + ): Promise { throw new Error("Method not implemented.") } @@ -115,10 +121,10 @@ export class MikroOrmBaseRepository< @InjectTransactionManager() async softDelete( idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() - { transactionManager: manager }: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise<[T[], Record]> { const isArray = Array.isArray(idsOrFilter) + // TODO handle composite keys const filter = isArray || isString(idsOrFilter) ? { @@ -128,9 +134,10 @@ export class MikroOrmBaseRepository< } : idsOrFilter - const entities = await this.find({ where: filter as any }) + const entities = await this.find({ where: filter as any }, sharedContext) const date = new Date() + const manager = this.getActiveManager(sharedContext) await mikroOrmUpdateDeletedAtRecursively( manager, entities as any[], @@ -147,9 +154,9 @@ export class MikroOrmBaseRepository< @InjectTransactionManager() async restore( idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() - { transactionManager: manager }: Context = {} + @MedusaContext() sharedContext: Context = {} ): Promise<[T[], Record]> { + // TODO handle composite keys const isArray = Array.isArray(idsOrFilter) const filter = isArray || isString(idsOrFilter) @@ -164,8 +171,9 @@ export class MikroOrmBaseRepository< withDeleted: true, }) - const entities = await this.find(query) + const entities = await this.find(query, sharedContext) + const manager = this.getActiveManager(sharedContext) await mikroOrmUpdateDeletedAtRecursively(manager, entities as any[], null) const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({ @@ -228,18 +236,12 @@ export class MikroOrmBaseTreeRepository< } } -type DtoBasedMutationMethods = "create" | "update" - -export function mikroOrmBaseRepositoryFactory< - T extends object = object, - TDTOs extends { [K in DtoBasedMutationMethods]?: any } = { - [K in DtoBasedMutationMethods]?: any - } ->(entity: EntityClass | EntitySchema) { - class MikroOrmAbstractBaseRepository_ - extends MikroOrmBaseRepository - implements RepositoryService - { +export function mikroOrmBaseRepositoryFactory( + entity: any +): { + new ({ manager }: { manager: any }): MikroOrmBaseRepository +} { + class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository { // @ts-ignore constructor(...args: any[]) { // @ts-ignore @@ -257,7 +259,7 @@ export function mikroOrmBaseRepositoryFactory< ) } - async create(data: TDTOs["create"][], context?: Context): Promise { + async create(data: any[], context?: Context): Promise { const manager = this.getActiveManager(context) const entities = data.map((data_) => { @@ -272,76 +274,13 @@ export function mikroOrmBaseRepositoryFactory< return entities } - async update(data: TDTOs["update"][], context?: Context): Promise { - // TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts + async update(data: { entity; update }[], context?: Context): Promise { const manager = this.getActiveManager(context) - - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) - - let primaryKeysCriteria: { [key: string]: any }[] = [] - if (primaryKeys.length === 1) { - primaryKeysCriteria.push({ - [primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]), - }) - } else { - primaryKeysCriteria = data.map((d) => ({ - $and: primaryKeys.map((key) => ({ [key]: d[key] })), - })) - } - - const allEntities = await Promise.all( - primaryKeysCriteria.map( - async (criteria) => - await this.find({ where: criteria } as DAL.FindOptions, context) - ) - ) - - const existingEntities = allEntities.flat() - - const existingEntitiesMap = new Map() - existingEntities.forEach((entity) => { - if (entity) { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - entity - ) - existingEntitiesMap.set(key, entity) - } - }) - - const missingEntities = data.filter((data_) => { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - data_ - ) - return !existingEntitiesMap.has(key) - }) - - if (missingEntities.length) { - const entityName = (entity as EntityClass).name ?? entity - const missingEntitiesKeys = data.map((data_) => - primaryKeys.map((key) => data_[key]).join(", ") - ) - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${entityName} with ${primaryKeys.join( - ", " - )} "${missingEntitiesKeys.join(", ")}" not found` - ) - } - const entities = data.map((data_) => { - const key = - MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue( - primaryKeys, - data_ - ) - const existingEntity = existingEntitiesMap.get(key)! - - return manager.assign(existingEntity, data_ as RequiredEntityData) + return manager.assign( + data_.entity, + data_.update as RequiredEntityData + ) }) manager.persist(entities) @@ -350,40 +289,11 @@ export function mikroOrmBaseRepositoryFactory< } async delete( - primaryKeyValues: string[] | object[], + filters: FilterQuery & BaseFilterable>, context?: Context ): Promise { const manager = this.getActiveManager(context) - - const primaryKeys = - MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity) - - let deletionCriteria - if (primaryKeys.length > 1) { - deletionCriteria = { - $or: primaryKeyValues.map((compositeKeyValue) => { - const keys = Object.keys(compositeKeyValue) - if (!primaryKeys.every((k) => keys.includes(k))) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Composite key must contain all primary key fields: ${primaryKeys.join( - ", " - )}. Found: ${keys}` - ) - } - - const criteria: { [key: string]: any } = {} - for (const key of primaryKeys) { - criteria[key] = compositeKeyValue[key] - } - return criteria - }), - } - } else { - deletionCriteria = { [primaryKeys[0]]: { $in: primaryKeyValues } } - } - - await manager.nativeDelete(entity as EntityName, deletionCriteria) + await manager.nativeDelete(entity as EntityName, filters as any) } async find(options?: DAL.FindOptions, context?: Context): Promise { @@ -423,11 +333,7 @@ export function mikroOrmBaseRepositoryFactory< ) } - async upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - context: Context = {} - ): Promise { - // TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts + async upsert(data: any[], context: Context = {}): Promise { const manager = this.getActiveManager(context) const primaryKeys = @@ -435,21 +341,34 @@ export function mikroOrmBaseRepositoryFactory< let primaryKeysCriteria: { [key: string]: any }[] = [] if (primaryKeys.length === 1) { - primaryKeysCriteria.push({ - [primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]), - }) + const primaryKeyValues = data + .map((d) => d[primaryKeys[0]]) + .filter(Boolean) + + if (primaryKeyValues.length) { + primaryKeysCriteria.push({ + [primaryKeys[0]]: primaryKeyValues, + }) + } } else { primaryKeysCriteria = data.map((d) => ({ $and: primaryKeys.map((key) => ({ [key]: d[key] })), })) } - const allEntities = await Promise.all( - primaryKeysCriteria.map( - async (criteria) => - await this.find({ where: criteria } as DAL.FindOptions, context) + let allEntities: T[][] = [] + + if (primaryKeysCriteria.length) { + allEntities = await Promise.all( + primaryKeysCriteria.map( + async (criteria) => + await this.find( + { where: criteria } as DAL.FindOptions, + context + ) + ) ) - ) + } const existingEntities = allEntities.flat() @@ -482,7 +401,7 @@ export function mikroOrmBaseRepositoryFactory< const updatedType = manager.assign(existingEntity, data_) updatedEntities.push(updatedType) } else { - const newEntity = manager.create(entity, data_) + const newEntity = manager.create(entity, data_) createdEntities.push(newEntity) } }) @@ -497,9 +416,12 @@ export function mikroOrmBaseRepositoryFactory< upsertedEntities.push(...updatedEntities) } + // TODO return the all, created, updated entities return upsertedEntities } } - return MikroOrmAbstractBaseRepository_ + return MikroOrmAbstractBaseRepository_ as unknown as { + new ({ manager }: { manager: any }): MikroOrmBaseRepository + } } diff --git a/packages/utils/src/dal/mikro-orm/utils.ts b/packages/utils/src/dal/mikro-orm/utils.ts index fc5d1f55d78a3..43a5a5efa218b 100644 --- a/packages/utils/src/dal/mikro-orm/utils.ts +++ b/packages/utils/src/dal/mikro-orm/utils.ts @@ -1,3 +1,5 @@ +import { buildQuery } from "../../modules-sdk" + export const mikroOrmUpdateDeletedAtRecursively = async < T extends object = any >( @@ -27,12 +29,35 @@ export const mikroOrmUpdateDeletedAtRecursively = async < continue } + const retrieveEntity = async () => { + const query = buildQuery( + { + id: entity.id, + }, + { + relations: [relation.name], + withDeleted: true, + } + ) + return await manager.findOne( + entity.constructor.name, + query.where, + query.options + ) + } + + if (!entityRelation) { + // Fixes the case of many to many through pivot table + entityRelation = await retrieveEntity() + } + const isCollection = "toArray" in entityRelation let relationEntities: any[] = [] if (isCollection) { if (!entityRelation.isInitialized()) { - entityRelation = await entityRelation.init({ populate: true }) + entityRelation = await retrieveEntity() + entityRelation = entityRelation[relation.name] } relationEntities = entityRelation.getItems() } else { @@ -53,6 +78,10 @@ export const mikroOrmSerializer = async ( ): Promise => { options ??= {} const { serialize } = await import("@mikro-orm/core") - const result = serialize(data, options) + const result = serialize(data, { + forceObject: true, + populate: true, + ...options, + }) return result as unknown as Promise } diff --git a/packages/utils/src/dal/repository.ts b/packages/utils/src/dal/repository.ts deleted file mode 100644 index f2f3da05a30b1..0000000000000 --- a/packages/utils/src/dal/repository.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types" -import { MedusaContext } from "../modules-sdk" -import { transactionWrapper } from "./utils" - -class AbstractBase { - protected readonly manager_: any - - protected constructor({ manager }) { - this.manager_ = manager - } - - getActiveManager( - @MedusaContext() - { transactionManager, manager }: Context = {} - ): TManager { - return (transactionManager ?? manager ?? this.manager_) as TManager - } - - async transaction( - task: (transactionManager: TManager) => Promise, - { - transaction, - isolationLevel, - enableNestedTransactions = false, - }: { - isolationLevel?: string - enableNestedTransactions?: boolean - transaction?: TManager - } = {} - ): Promise { - // @ts-ignore - return await transactionWrapper.apply(this, arguments) - } -} - -export abstract class AbstractBaseRepository - extends AbstractBase - implements DAL.RepositoryService -{ - abstract find(options?: DAL.FindOptions, context?: Context) - - abstract findAndCount( - options?: DAL.FindOptions, - context?: Context - ): Promise<[T[], number]> - - abstract create(data: unknown[], context?: Context): Promise - - abstract update(data: unknown[], context?: Context): Promise - - abstract delete(ids: string[], context?: Context): Promise - - abstract upsert(data: unknown[], context?: Context): Promise - - abstract softDelete( - ids: string[], - context?: Context - ): Promise<[T[], Record]> - - abstract restore( - ids: string[], - context?: Context - ): Promise<[T[], Record]> - - abstract getFreshManager(): TManager - - abstract serialize( - data: any, - options?: any - ): Promise -} - -export abstract class AbstractTreeRepositoryBase - extends AbstractBase - implements DAL.TreeRepositoryService -{ - protected constructor({ manager }) { - // @ts-ignore - super(...arguments) - } - - abstract find( - options?: DAL.FindOptions, - transformOptions?: RepositoryTransformOptions, - context?: Context - ) - - abstract findAndCount( - options?: DAL.FindOptions, - transformOptions?: RepositoryTransformOptions, - context?: Context - ): Promise<[T[], number]> - - abstract create(data: unknown, context?: Context): Promise - - abstract delete(id: string, context?: Context): Promise - - abstract getFreshManager(): TManager - - abstract serialize( - data: any, - options?: any - ): Promise -} diff --git a/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts b/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts new file mode 100644 index 0000000000000..35935e84ed308 --- /dev/null +++ b/packages/utils/src/modules-sdk/__tests__/abstract-module-service-factory.spec.ts @@ -0,0 +1,201 @@ +import { abstractModuleServiceFactory } from "../abstract-module-service-factory" + +const baseRepoMock = { + serialize: jest.fn().mockImplementation((item) => item), + transaction: (task) => task("transactionManager"), + getFreshManager: jest.fn().mockReturnThis(), +} + +const defaultContext = { __type: "MedusaContext", manager: baseRepoMock } +const defaultTransactionContext = { + __type: "MedusaContext", + transactionManager: "transactionManager", +} + +describe("Abstract Module Service Factory", () => { + const containerMock = { + baseRepository: baseRepoMock, + mainModelMockRepository: baseRepoMock, + otherModelMock1Repository: baseRepoMock, + otherModelMock2Repository: baseRepoMock, + mainModelMockService: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + otherModelMock1Service: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + otherModelMock2Service: { + retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }), + list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]), + delete: jest.fn().mockResolvedValue(undefined), + softDelete: jest.fn().mockResolvedValue([[], {}]), + restore: jest.fn().mockResolvedValue([[], {}]), + }, + } + + const mainModelMock = class MainModelMock {} + const otherModelMock1 = class OtherModelMock1 {} + const otherModelMock2 = class OtherModelMock2 {} + + const abstractModuleService = abstractModuleServiceFactory< + any, + any, + { + OtherModelMock1: { + dto: any + singular: "OtherModelMock1" + plural: "OtherModelMock1s" + } + OtherModelMock2: { + dto: any + singular: "OtherModelMock2" + plural: "OtherModelMock2s" + } + } + >( + mainModelMock, + [ + { + model: otherModelMock1, + plural: "otherModelMock1s", + singular: "otherModelMock1", + }, + { + model: otherModelMock2, + plural: "otherModelMock2s", + singular: "otherModelMock2", + }, + ] + // Add more parameters as needed + ) + + describe("Main Model Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new abstractModuleService(containerMock) + }) + + test("should have retrieve method", async () => { + const result = await instance.retrieve("1") + expect(result).toEqual({ id: "1", name: "Item" }) + expect(containerMock.mainModelMockService.retrieve).toHaveBeenCalledWith( + "1", + undefined, + defaultContext + ) + }) + + test("should have list method", async () => { + const result = await instance.list() + expect(result).toEqual([{ id: "1", name: "Item" }]) + expect(containerMock.mainModelMockService.list).toHaveBeenCalledWith( + {}, + {}, + defaultContext + ) + }) + + test("should have delete method", async () => { + await instance.delete("1") + expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have softDelete method", async () => { + const result = await instance.softDelete("1") + expect(result).toEqual(undefined) + expect( + containerMock.mainModelMockService.softDelete + ).toHaveBeenCalledWith(["1"], defaultTransactionContext) + }) + + test("should have restore method", async () => { + const result = await instance.restore("1") + expect(result).toEqual(undefined) + expect(containerMock.mainModelMockService.restore).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have delete method with selector", async () => { + await instance.delete({ selector: { id: "1" } }) + expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith( + [{ selector: { id: "1" } }], + defaultTransactionContext + ) + }) + }) + + describe("Other Models Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new abstractModuleService(containerMock) + }) + + test("should have retrieve method for other models", async () => { + const result = await instance.retrieveOtherModelMock1("1") + expect(result).toEqual({ id: "1", name: "Item" }) + expect( + containerMock.otherModelMock1Service.retrieve + ).toHaveBeenCalledWith("1", undefined, defaultContext) + }) + + test("should have list method for other models", async () => { + const result = await instance.listOtherModelMock1s() + expect(result).toEqual([{ id: "1", name: "Item" }]) + expect(containerMock.otherModelMock1Service.list).toHaveBeenCalledWith( + {}, + {}, + defaultContext + ) + }) + + test("should have delete method for other models", async () => { + await instance.deleteOtherModelMock1s("1") + expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have softDelete method for other models", async () => { + const result = await instance.softDeleteOtherModelMock1s("1") + expect(result).toEqual(undefined) + expect( + containerMock.otherModelMock1Service.softDelete + ).toHaveBeenCalledWith(["1"], defaultTransactionContext) + }) + + test("should have restore method for other models", async () => { + const result = await instance.restoreOtherModelMock1s("1") + expect(result).toEqual(undefined) + expect(containerMock.otherModelMock1Service.restore).toHaveBeenCalledWith( + ["1"], + defaultTransactionContext + ) + }) + + test("should have delete method for other models with selector", async () => { + await instance.deleteOtherModelMock1s({ selector: { id: "1" } }) + expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith( + [{ selector: { id: "1" } }], + defaultTransactionContext + ) + }) + }) +}) diff --git a/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts b/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts new file mode 100644 index 0000000000000..2e4b9db5c409b --- /dev/null +++ b/packages/utils/src/modules-sdk/__tests__/internal-module-service-factory.spec.ts @@ -0,0 +1,240 @@ +import { internalModuleServiceFactory } from "../internal-module-service-factory" +import { lowerCaseFirst } from "../../common" + +const defaultContext = { __type: "MedusaContext" } + +class Model {} +describe("Internal Module Service Factory", () => { + const modelRepositoryName = `${lowerCaseFirst(Model.name)}Repository` + + const containerMock = { + [modelRepositoryName]: { + transaction: (task) => task(), + getFreshManager: jest.fn().mockReturnThis(), + find: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + softDelete: jest.fn(), + restore: jest.fn(), + upsert: jest.fn(), + }, + [`composite${Model.name}Repository`]: { + transaction: (task) => task(), + getFreshManager: jest.fn().mockReturnThis(), + find: jest.fn(), + findAndCount: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + softDelete: jest.fn(), + restore: jest.fn(), + upsert: jest.fn(), + }, + } + + const internalModuleService = internalModuleServiceFactory(Model) + + describe("Internal Module Service Methods", () => { + let instance + + beforeEach(() => { + jest.clearAllMocks() + instance = new internalModuleService(containerMock) + }) + + test("should throw model id undefined error on retrieve if id is not defined", async () => { + const err = await instance.retrieve().catch((e) => e) + expect(err.message).toBe("model - id must be defined") + }) + + test("should throw an error on retrieve if composite key values are not defined", async () => { + class CompositeModel { + id: string + name: string + + static meta = { primaryKeys: ["id", "name"] } + } + + const compositeInternalModuleService = + internalModuleServiceFactory(CompositeModel) + + const instance = new compositeInternalModuleService(containerMock) + + const err = await instance.retrieve().catch((e) => e) + expect(err.message).toBe("compositeModel - id, name must be defined") + }) + + test("should throw NOT_FOUND error on retrieve if entity not found", async () => { + containerMock[modelRepositoryName].find.mockResolvedValueOnce([]) + + const err = await instance.retrieve("1").catch((e) => e) + expect(err.message).toBe("Model with id: 1 was not found") + }) + + test("should retrieve entity successfully", async () => { + const entity = { id: "1", name: "Item" } + containerMock[modelRepositoryName].find.mockResolvedValueOnce([entity]) + + const result = await instance.retrieve("1") + expect(result).toEqual(entity) + }) + + test("should retrieve entity successfully with composite key", async () => { + class CompositeModel { + id: string + name: string + + static meta = { primaryKeys: ["id", "name"] } + } + + const compositeInternalModuleService = + internalModuleServiceFactory(CompositeModel) + + const instance = new compositeInternalModuleService(containerMock) + + const entity = { id: "1", name: "Item" } + containerMock[ + `${lowerCaseFirst(CompositeModel.name)}Repository` + ].find.mockResolvedValueOnce([entity]) + + const result = await instance.retrieve({ id: "1", name: "Item" }) + expect(result).toEqual(entity) + }) + + test("should list entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + containerMock[modelRepositoryName].find.mockResolvedValueOnce(entities) + + const result = await instance.list() + expect(result).toEqual(entities) + }) + + test("should list and count entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + const count = entities.length + containerMock[modelRepositoryName].findAndCount.mockResolvedValueOnce([ + entities, + count, + ]) + + const result = await instance.listAndCount() + expect(result).toEqual([entities, count]) + }) + + test("should create entity successfully", async () => { + const entity = { id: "1", name: "Item" } + + containerMock[modelRepositoryName].find.mockReturnValue([entity]) + + containerMock[modelRepositoryName].create.mockImplementation( + async (entity) => entity + ) + + const result = await instance.create(entity) + expect(result).toEqual(entity) + }) + + test("should create entities successfully", async () => { + const entities = [ + { id: "1", name: "Item" }, + { id: "2", name: "Item2" }, + ] + + containerMock[modelRepositoryName].find.mockResolvedValueOnce([entities]) + + containerMock[modelRepositoryName].create.mockResolvedValueOnce(entities) + + const result = await instance.create(entities) + expect(result).toEqual(entities) + }) + + test("should update entity successfully", async () => { + const updateData = { id: "1", name: "UpdatedItem" } + + containerMock[modelRepositoryName].find.mockResolvedValueOnce([ + updateData, + ]) + + containerMock[modelRepositoryName].update.mockResolvedValueOnce([ + updateData, + ]) + + const result = await instance.update(updateData) + expect(result).toEqual([updateData]) + }) + + test("should update entities successfully", async () => { + const updateData = { id: "1", name: "UpdatedItem" } + const entitiesToUpdate = [{ id: "1", name: "Item" }] + + containerMock[modelRepositoryName].find.mockResolvedValueOnce( + entitiesToUpdate + ) + + containerMock[modelRepositoryName].update.mockResolvedValueOnce([ + { entity: entitiesToUpdate[0], update: updateData }, + ]) + + const result = await instance.update({ selector: {}, data: updateData }) + expect(result).toEqual([ + { entity: entitiesToUpdate[0], update: updateData }, + ]) + }) + + test("should delete entity successfully", async () => { + await instance.delete("1") + expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( + { + $or: [ + { + id: "1", + }, + ], + }, + defaultContext + ) + }) + + test("should delete entities successfully", async () => { + const entitiesToDelete = [{ id: "1", name: "Item" }] + containerMock[modelRepositoryName].find.mockResolvedValueOnce( + entitiesToDelete + ) + + await instance.delete({ selector: {} }) + expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith( + { + $or: [ + { + id: "1", + }, + ], + }, + defaultContext + ) + }) + + test("should soft delete entity successfully", async () => { + await instance.softDelete("1") + expect( + containerMock[modelRepositoryName].softDelete + ).toHaveBeenCalledWith("1", defaultContext) + }) + + test("should restore entity successfully", async () => { + await instance.restore("1") + expect(containerMock[modelRepositoryName].restore).toHaveBeenCalledWith( + "1", + defaultContext + ) + }) + }) +}) diff --git a/packages/utils/src/modules-sdk/abstract-module-service-factory.ts b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts new file mode 100644 index 0000000000000..e6e23257dff8d --- /dev/null +++ b/packages/utils/src/modules-sdk/abstract-module-service-factory.ts @@ -0,0 +1,527 @@ +/** + * Utility factory and interfaces for module service public facing API + */ +import { + Constructor, + Context, + FindConfig, + IEventBusModuleService, + Pluralize, + RepositoryService, + RestoreReturn, + SoftDeleteReturn, +} from "@medusajs/types" +import { + isString, + kebabCase, + lowerCaseFirst, + mapObjectTo, + MapToConfig, + pluralize, + upperCaseFirst, +} from "../common" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "./decorators" + +type BaseMethods = + | "retrieve" + | "list" + | "listAndCount" + | "delete" + | "softDelete" + | "restore" + +const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[] +const writeMethods = ["delete", "softDelete", "restore"] as BaseMethods[] + +const methods: BaseMethods[] = [...readMethods, ...writeMethods] + +type ModelsConfigTemplate = { + [ModelName: string]: { singular?: string; plural?: string; dto: object } +} + +type ExtractSingularName< + T extends Record, + K = keyof T +> = T[K] extends { singular?: string } ? T[K]["singular"] : K + +type ExtractPluralName, K = keyof T> = T[K] extends { + plural?: string +} + ? T[K]["plural"] + : Pluralize + +type ModelConfiguration = + | Constructor + | { singular?: string; plural?: string; model: Constructor } + +export interface AbstractModuleServiceBase { + get __container__(): TContainer + + retrieve( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ): Promise<[TMainModelDTO[], number]> + + delete( + primaryKeyValues: string | object | string[] | object[], + sharedContext?: Context + ): Promise + + softDelete( + primaryKeyValues: string | object | string[] | object[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restore( + primaryKeyValues: string | object | string[] | object[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> +} + +/** + * Multiple issues on typescript around mapped types function are open, so + * when overriding a method from the base class that is mapped dynamically from the + * other models, we will have to ignore the error (2425) + * + * see: https://github.com/microsoft/TypeScript/issues/48125 + */ +export type AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate +> = AbstractModuleServiceBase & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `retrieve${ExtractSingularName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: ( + id: string, + config?: FindConfig, + sharedContext?: Context + ) => Promise +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `list${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: ( + filters?: any, + config?: FindConfig, + sharedContext?: Context + ) => Promise +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `listAndCount${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + (filters?: any, config?: FindConfig, sharedContext?: Context): Promise< + [TOtherModelNamesAndAssociatedDTO[K & string]["dto"][], number] + > + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `delete${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + sharedContext?: Context + ): Promise + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `softDelete${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + } +} & { + [K in keyof TOtherModelNamesAndAssociatedDTO as `restore${ExtractPluralName< + TOtherModelNamesAndAssociatedDTO, + K + > & + string}`]: { + ( + primaryKeyValues: string | object | string[] | object[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + } +} + +/** + * Factory function for creating an abstract module service + * + * @example + * + * const otherModels = new Set([ + * Currency, + * MoneyAmount, + * PriceList, + * PriceListRule, + * PriceListRuleValue, + * PriceRule, + * PriceSetMoneyAmount, + * PriceSetMoneyAmountRules, + * PriceSetRuleType, + * RuleType, + * ]) + * + * const AbstractModuleService = ModulesSdkUtils.abstractModuleServiceFactory< + * InjectedDependencies, + * PricingTypes.PriceSetDTO, + * // The configuration of each entity also accept singular/plural properties, if not provided then it is using english pluralization + * { + * Currency: { dto: PricingTypes.CurrencyDTO } + * MoneyAmount: { dto: PricingTypes.MoneyAmountDTO } + * PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO } + * PriceSetMoneyAmountRules: { + * dto: PricingTypes.PriceSetMoneyAmountRulesDTO + * } + * PriceRule: { dto: PricingTypes.PriceRuleDTO } + * RuleType: { dto: PricingTypes.RuleTypeDTO } + * PriceList: { dto: PricingTypes.PriceListDTO } + * PriceListRule: { dto: PricingTypes.PriceListRuleDTO } + * } + * >(PriceSet, [...otherModels], entityNameToLinkableKeysMap) + * + * @param mainModel + * @param otherModels + * @param entityNameToLinkableKeysMap + */ +export function abstractModuleServiceFactory< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate +>( + mainModel: Constructor, + otherModels: ModelConfiguration[], + entityNameToLinkableKeysMap: MapToConfig = {} +): { + new (container: TContainer): AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO + > +} { + const buildMethodNamesFromModel = ( + model: ModelConfiguration, + suffixed: boolean = true + ): Record => { + return methods.reduce((acc, method) => { + let modelName: string = "" + + if (method === "retrieve") { + modelName = + "singular" in model && model.singular + ? model.singular + : (model as Constructor).name + } else { + modelName = + "plural" in model && model.plural + ? model.plural + : pluralize((model as Constructor).name) + } + + const methodName = suffixed + ? `${method}${upperCaseFirst(modelName)}` + : method + + return { ...acc, [method]: methodName } + }, {}) + } + + const buildAndAssignMethodImpl = function ( + klassPrototype: any, + method: string, + methodName: string, + model: Constructor + ): void { + const serviceRegistrationName = `${lowerCaseFirst(model.name)}Service` + + const applyMethod = function (impl: Function, contextIndex) { + klassPrototype[methodName] = impl + + const descriptorMockRef = { + value: klassPrototype[methodName], + } + + MedusaContext()(klassPrototype, methodName, contextIndex) + + const ManagerDecorator = readMethods.includes(method as BaseMethods) + ? InjectManager + : InjectTransactionManager + + ManagerDecorator("baseRepository_")( + klassPrototype, + methodName, + descriptorMockRef + ) + + klassPrototype[methodName] = descriptorMockRef.value + } + + let methodImplementation: any = function () { + void 0 + } + + switch (method) { + case "retrieve": + methodImplementation = async function ( + this: AbstractModuleService_, + id: string, + config?: FindConfig, + sharedContext: Context = {} + ): Promise { + const entities = await this.__container__[ + serviceRegistrationName + ].retrieve(id, config, sharedContext) + + return await this.baseRepository_.serialize(entities, { + populate: true, + }) + } + + applyMethod(methodImplementation, 2) + + break + case "list": + methodImplementation = async function ( + this: AbstractModuleService_, + filters = {}, + config: FindConfig = {}, + sharedContext: Context = {} + ): Promise { + const entities = await this.__container__[ + serviceRegistrationName + ].list(filters, config, sharedContext) + + return await this.baseRepository_.serialize(entities, { + populate: true, + }) + } + + applyMethod(methodImplementation, 2) + + break + case "listAndCount": + methodImplementation = async function ( + this: AbstractModuleService_, + filters = {}, + config: FindConfig = {}, + sharedContext: Context = {} + ): Promise { + const [entities, count] = await this.__container__[ + serviceRegistrationName + ].listAndCount(filters, config, sharedContext) + + return [ + await this.baseRepository_.serialize(entities, { + populate: true, + }), + count, + ] + } + + applyMethod(methodImplementation, 2) + + break + case "delete": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + sharedContext: Context = {} + ): Promise { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + await this.__container__[serviceRegistrationName].delete( + primaryKeyValues_, + sharedContext + ) + + await this.eventBusModuleService_?.emit( + primaryKeyValues_.map((primaryKeyValue) => ({ + eventName: `${kebabCase(model.name)}.deleted`, + data: isString(primaryKeyValue) + ? { id: primaryKeyValue } + : primaryKeyValue, + })) + ) + } + + applyMethod(methodImplementation, 1) + + break + case "softDelete": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + config: SoftDeleteReturn = {}, + sharedContext: Context = {} + ): Promise | void> { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + + const [entities, cascadedEntitiesMap] = await this.__container__[ + serviceRegistrationName + ].softDelete(primaryKeyValues_, sharedContext) + + const softDeletedEntities = await this.baseRepository_.serialize( + entities, + { + populate: true, + } + ) + + await this.eventBusModuleService_?.emit( + softDeletedEntities.map(({ id }) => ({ + eventName: `${kebabCase(model.name)}.deleted`, + data: { id }, + })) + ) + + let mappedCascadedEntitiesMap + if (config.returnLinkableKeys) { + // Map internal table/column names to their respective external linkable keys + // eg: product.id = product_id, variant.id = variant_id + mappedCascadedEntitiesMap = mapObjectTo( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + applyMethod(methodImplementation, 2) + + break + case "restore": + methodImplementation = async function ( + this: AbstractModuleService_, + primaryKeyValues: string | object | string[] | object[], + config: RestoreReturn = {}, + sharedContext: Context = {} + ): Promise | void> { + const primaryKeyValues_ = Array.isArray(primaryKeyValues) + ? primaryKeyValues + : [primaryKeyValues] + + const [_, cascadedEntitiesMap] = await this.__container__[ + serviceRegistrationName + ].restore(primaryKeyValues_, sharedContext) + + let mappedCascadedEntitiesMap + if (config.returnLinkableKeys) { + // Map internal table/column names to their respective external linkable keys + // eg: product.id = product_id, variant.id = variant_id + mappedCascadedEntitiesMap = mapObjectTo( + cascadedEntitiesMap, + entityNameToLinkableKeysMap, + { + pick: config.returnLinkableKeys, + } + ) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + applyMethod(methodImplementation, 2) + + break + } + } + + class AbstractModuleService_ { + readonly __container__: Record + readonly baseRepository_: RepositoryService + readonly eventBusModuleService_: IEventBusModuleService; + + [key: string]: any + + constructor(container: Record) { + this.__container__ = container + this.baseRepository_ = container.baseRepository + + try { + this.eventBusModuleService_ = container.eventBusModuleService + } catch { + /* ignore */ + } + } + } + + const mainModelMethods = buildMethodNamesFromModel(mainModel, false) + + /** + * Build the main retrieve/list/listAndCount/delete/softDelete/restore methods for the main model + */ + + for (let [method, methodName] of Object.entries(mainModelMethods)) { + buildAndAssignMethodImpl( + AbstractModuleService_.prototype, + method, + methodName, + mainModel + ) + } + + /** + * Build the retrieve/list/listAndCount/delete/softDelete/restore methods for all the other models + */ + + const otherModelsMethods: [ModelConfiguration, Record][] = + otherModels.map((model) => [model, buildMethodNamesFromModel(model)]) + + for (let [model, modelsMethods] of otherModelsMethods) { + Object.entries(modelsMethods).forEach(([method, methodName]) => { + model = "model" in model ? model.model : model + buildAndAssignMethodImpl( + AbstractModuleService_.prototype, + method, + methodName, + model + ) + }) + } + + return AbstractModuleService_ as unknown as new ( + container: TContainer + ) => AbstractModuleService< + TContainer, + TMainModelDTO, + TOtherModelNamesAndAssociatedDTO + > +} diff --git a/packages/utils/src/modules-sdk/abstract-service-factory.ts b/packages/utils/src/modules-sdk/abstract-service-factory.ts deleted file mode 100644 index 64a6531bb3c8c..0000000000000 --- a/packages/utils/src/modules-sdk/abstract-service-factory.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { - Context, - FindConfig, - FilterQuery as InternalFilterQuery, -} from "@medusajs/types" -import { EntitySchema } from "@mikro-orm/core" -import { EntityClass } from "@mikro-orm/core/typings" -import { - MedusaError, - doNotForceTransaction, - isDefined, - isString, - lowerCaseFirst, - shouldForceTransaction, - upperCaseFirst, -} from "../common" -import { MedusaContext } from "../modules-sdk" -import { buildQuery } from "./build-query" -import { InjectManager, InjectTransactionManager } from "./decorators" - -/** - * Utility factory and interfaces for internal module services - */ - -type FilterableMethods = "list" | "listAndCount" -type Methods = "create" | "update" - -export interface AbstractService< - TEntity extends {}, - TContainer extends object = object, - TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any }, - TFilters extends { [K in FilterableMethods]?: any } = { - [K in FilterableMethods]?: any - } -> { - get __container__(): TContainer - - retrieve( - id: string, - config?: FindConfig, - sharedContext?: Context - ): Promise - list( - filters?: TFilters["list"], - config?: FindConfig, - sharedContext?: Context - ): Promise - listAndCount( - filters?: TFilters["listAndCount"], - config?: FindConfig, - sharedContext?: Context - ): Promise<[TEntity[], number]> - create(data: TDTOs["create"][], sharedContext?: Context): Promise - update(data: TDTOs["update"][], sharedContext?: Context): Promise - delete( - primaryKeyValues: string[] | object[], - sharedContext?: Context - ): Promise - softDelete( - idsOrFilter: string[] | InternalFilterQuery, - sharedContext?: Context - ): Promise<[TEntity[], Record]> - restore( - idsOrFilter: string[] | InternalFilterQuery, - sharedContext?: Context - ): Promise<[TEntity[], Record]> - upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - sharedContext?: Context - ): Promise -} - -export function abstractServiceFactory< - TContainer extends object = object, - TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any }, - TFilters extends { [K in FilterableMethods]?: any } = { - [K in FilterableMethods]?: any - } ->( - model: new (...args: any[]) => any -): { - new (container: TContainer): AbstractService< - TEntity, - TContainer, - TDTOs, - TFilters - > -} { - const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository` - const propertyRepositoryName = `__${injectedRepositoryName}__` - - class AbstractService_ - implements AbstractService - { - readonly __container__: TContainer; - [key: string]: any - - constructor(container: TContainer) { - this.__container__ = container - this[propertyRepositoryName] = container[injectedRepositoryName] - } - - static retrievePrimaryKeys(entity: EntityClass | EntitySchema) { - return ( - (entity as EntitySchema).meta?.primaryKeys ?? - (entity as EntityClass).prototype.__meta?.primaryKeys ?? ["id"] - ) - } - - @InjectManager(propertyRepositoryName) - async retrieve( - primaryKeyValues: string | string[] | object[], - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const primaryKeys = AbstractService_.retrievePrimaryKeys(model) - - if (!isDefined(primaryKeyValues)) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${ - primaryKeys.length === 1 - ? `"${ - lowerCaseFirst(model.name) + upperCaseFirst(primaryKeys[0]) - }"` - : `${lowerCaseFirst(model.name)} ${primaryKeys.join(", ")}` - } must be defined` - ) - } - - let primaryKeysCriteria = {} - if (primaryKeys.length === 1) { - primaryKeysCriteria[primaryKeys[0]] = primaryKeyValues - } else { - primaryKeysCriteria = (primaryKeyValues as string[] | object[]).map( - (primaryKeyValue) => ({ - $and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })), - }) - ) - } - - const queryOptions = buildQuery(primaryKeysCriteria, config) - - const entities = await this[propertyRepositoryName].find( - queryOptions, - sharedContext - ) - - if (!entities?.length) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `${model.name} with ${primaryKeys.join(", ")}: ${ - Array.isArray(primaryKeyValues) - ? primaryKeyValues.map((v) => - [isString(v) ? v : Object.values(v)].join(", ") - ) - : primaryKeyValues - } was not found` - ) - } - - return entities[0] - } - - @InjectManager(propertyRepositoryName) - async list( - filters: TFilters["list"] = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise { - const queryOptions = buildQuery(filters, config) - - return (await this[propertyRepositoryName].find( - queryOptions, - sharedContext - )) as TEntity[] - } - - @InjectManager(propertyRepositoryName) - async listAndCount( - filters: TFilters["listAndCount"] = {}, - config: FindConfig = {}, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], number]> { - const queryOptions = buildQuery(filters, config) - - return (await this[propertyRepositoryName].findAndCount( - queryOptions, - sharedContext - )) as [TEntity[], number] - } - - @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) - async create( - data: TDTOs["create"][], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await this[propertyRepositoryName].create( - data, - sharedContext - )) as TEntity[] - } - - @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) - async update( - data: TDTOs["update"][], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return (await this[propertyRepositoryName].update( - data, - sharedContext - )) as TEntity[] - } - - @InjectTransactionManager(doNotForceTransaction, propertyRepositoryName) - async delete( - primaryKeyValues: string[] | object[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - await this[propertyRepositoryName].delete(primaryKeyValues, sharedContext) - } - - @InjectTransactionManager(propertyRepositoryName) - async softDelete( - idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this[propertyRepositoryName].softDelete( - idsOrFilter, - sharedContext - ) - } - - @InjectTransactionManager(propertyRepositoryName) - async restore( - idsOrFilter: string[] | InternalFilterQuery, - @MedusaContext() sharedContext: Context = {} - ): Promise<[TEntity[], Record]> { - return await this[propertyRepositoryName].restore( - idsOrFilter, - sharedContext - ) - } - - @InjectTransactionManager(propertyRepositoryName) - async upsert( - data: (TDTOs["create"] | TDTOs["update"])[], - @MedusaContext() sharedContext: Context = {} - ): Promise { - return await this[propertyRepositoryName].upsert(data, sharedContext) - } - } - - return AbstractService_ as unknown as new ( - container: TContainer - ) => AbstractService -} diff --git a/packages/utils/src/modules-sdk/decorators/inject-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-manager.ts index 0b40acfd1dcbd..d83c1deee40fc 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-manager.ts @@ -1,4 +1,5 @@ import { Context } from "@medusajs/types" +import { MedusaContextType } from "./context-parameter" export function InjectManager(managerProperty?: string): MethodDecorator { return function ( @@ -37,8 +38,14 @@ export function InjectManager(managerProperty?: string): MethodDecorator { ? this : this[managerProperty] - copiedContext.manager ??= resourceWithManager.getFreshManager() - copiedContext.transactionManager ??= originalContext?.transactionManager + copiedContext.manager = + originalContext.manager ?? resourceWithManager.getFreshManager() + + if (originalContext?.transactionManager) { + copiedContext.transactionManager = originalContext?.transactionManager + } + + copiedContext.__type = MedusaContextType args[argIndex] = copiedContext diff --git a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts index 4387cfb982b5e..580304467e543 100644 --- a/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts +++ b/packages/utils/src/modules-sdk/decorators/inject-transaction-manager.ts @@ -60,9 +60,13 @@ export function InjectTransactionManager( }) } - copiedContext.transactionManager ??= transactionManager - copiedContext.manager ??= originalContext?.manager - copiedContext.__type ??= MedusaContextType + copiedContext.transactionManager = transactionManager + + if (originalContext?.manager) { + copiedContext.manager = originalContext?.manager + } + + copiedContext.__type = MedusaContextType args[argIndex] = copiedContext diff --git a/packages/utils/src/modules-sdk/index.ts b/packages/utils/src/modules-sdk/index.ts index f4c31ebb23f4c..8837a26b11e25 100644 --- a/packages/utils/src/modules-sdk/index.ts +++ b/packages/utils/src/modules-sdk/index.ts @@ -5,4 +5,5 @@ export * from "./loaders/mikro-orm-connection-loader" export * from "./loaders/container-loader-factory" export * from "./create-pg-connection" export * from "./migration-scripts" -export * from "./abstract-service-factory" +export * from "./internal-module-service-factory" +export * from "./abstract-module-service-factory" diff --git a/packages/utils/src/modules-sdk/internal-module-service-factory.ts b/packages/utils/src/modules-sdk/internal-module-service-factory.ts new file mode 100644 index 0000000000000..62b2039a2010f --- /dev/null +++ b/packages/utils/src/modules-sdk/internal-module-service-factory.ts @@ -0,0 +1,442 @@ +import { + BaseFilterable, + Context, + FilterQuery, + FilterQuery as InternalFilterQuery, + FindConfig, + ModulesSdkTypes, +} from "@medusajs/types" +import { EntitySchema } from "@mikro-orm/core" +import { EntityClass } from "@mikro-orm/core/typings" +import { + doNotForceTransaction, + isDefined, + isObject, + isString, + lowerCaseFirst, + MedusaError, + shouldForceTransaction, +} from "../common" +import { buildQuery } from "./build-query" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "./decorators" + +type SelectorAndData = { + selector: FilterQuery | BaseFilterable> + data: any +} + +export function internalModuleServiceFactory< + TContainer extends object = object +>( + model: any +): { + new ( + container: TContainer + ): ModulesSdkTypes.InternalModuleService +} { + const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository` + const propertyRepositoryName = `__${injectedRepositoryName}__` + + class AbstractService_ + implements ModulesSdkTypes.InternalModuleService + { + readonly __container__: TContainer; + [key: string]: any + + constructor(container: TContainer) { + this.__container__ = container + this[propertyRepositoryName] = container[injectedRepositoryName] + } + + static retrievePrimaryKeys(entity: EntityClass | EntitySchema) { + return ( + (entity as EntitySchema).meta?.primaryKeys ?? + (entity as EntityClass).prototype.__meta?.primaryKeys ?? ["id"] + ) + } + + static buildUniqueCompositeKeyValue(keys: string[], data: object) { + return keys.map((k) => data[k]).join("_") + } + + @InjectManager(propertyRepositoryName) + async retrieve( + idOrObject: string | object, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + + if ( + !isDefined(idOrObject) || + (isString(idOrObject) && primaryKeys.length > 1) || + ((!isString(idOrObject) || + (isObject(idOrObject) && !idOrObject[primaryKeys[0]])) && + primaryKeys.length === 1) + ) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${ + primaryKeys.length === 1 + ? `${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}` + : `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}` + } must be defined` + ) + } + + let primaryKeysCriteria = {} + if (primaryKeys.length === 1) { + primaryKeysCriteria[primaryKeys[0]] = idOrObject + } else { + const idOrObject_ = Array.isArray(idOrObject) + ? idOrObject + : [idOrObject] + primaryKeysCriteria = idOrObject_.map((primaryKeyValue) => ({ + $and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })), + })) + } + + const queryOptions = buildQuery(primaryKeysCriteria, config) + + const entities = await this[propertyRepositoryName].find( + queryOptions, + sharedContext + ) + + if (!entities?.length) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${model.name} with ${primaryKeys.join(", ")}: ${ + Array.isArray(idOrObject) + ? idOrObject.map((v) => + [isString(v) ? v : Object.values(v)].join(", ") + ) + : idOrObject + } was not found` + ) + } + + return entities[0] + } + + @InjectManager(propertyRepositoryName) + async list( + filters: FilterQuery | BaseFilterable> = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const queryOptions = buildQuery(filters, config) + + return await this[propertyRepositoryName].find( + queryOptions, + sharedContext + ) + } + + @InjectManager(propertyRepositoryName) + async listAndCount( + filters: FilterQuery | BaseFilterable> = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], number]> { + const queryOptions = buildQuery(filters, config) + + return await this[propertyRepositoryName].findAndCount( + queryOptions, + sharedContext + ) + } + + create(data: any, sharedContext?: Context): Promise + create(data: any[], sharedContext?: Context): Promise + + @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) + async create( + data: any | any[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + if (!isDefined(data) || (Array.isArray(data) && data.length === 0)) { + return (Array.isArray(data) ? [] : void 0) as TEntity | TEntity[] + } + + const data_ = Array.isArray(data) ? data : [data] + const entities = await this[propertyRepositoryName].create( + data_, + sharedContext + ) + + return Array.isArray(data) ? entities : entities[0] + } + + update(data: any[], sharedContext?: Context): Promise + update(data: any, sharedContext?: Context): Promise + update( + selectorAndData: SelectorAndData, + sharedContext?: Context + ): Promise + update( + selectorAndData: SelectorAndData[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager(shouldForceTransaction, propertyRepositoryName) + async update( + input: any | any[] | SelectorAndData | SelectorAndData[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + if (!isDefined(input) || (Array.isArray(input) && input.length === 0)) { + return (Array.isArray(input) ? [] : void 0) as TEntity | TEntity[] + } + + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + const inputArray = Array.isArray(input) ? input : [input] + + const toUpdateData: { entity; update }[] = [] + + // Only used when we receive data and no selector + const keySelectorForDataOnly: any = { + $or: [], + } + const keySelectorDataMap = new Map() + + for (const input_ of inputArray) { + if (input_.selector) { + const entitiesToUpdate = await this.list( + input_.selector, + {}, + sharedContext + ) + // Create a pair of entity and data to update + entitiesToUpdate.forEach((entity) => { + toUpdateData.push({ + entity, + update: input_.data, + }) + }) + } else { + // in case we are manipulating the data, then extract the primary keys as a selector and the rest as the data to update + const selector = {} + + primaryKeys.forEach((key) => { + selector[key] = input_[key] + }) + + const uniqueCompositeKey = + AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, input_) + keySelectorDataMap.set(uniqueCompositeKey, input_) + + keySelectorForDataOnly.$or.push(selector) + } + } + + if (keySelectorForDataOnly.$or.length) { + const entitiesToUpdate = await this.list( + keySelectorForDataOnly, + {}, + sharedContext + ) + + // Create a pair of entity and data to update + entitiesToUpdate.forEach((entity) => { + const uniqueCompositeKey = + AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, entity) + toUpdateData.push({ + entity, + update: keySelectorDataMap.get(uniqueCompositeKey)!, + }) + }) + + // Only throw for missing entities when we dont have selectors involved as selector by design can return 0 entities + if (entitiesToUpdate.length !== keySelectorDataMap.size) { + const entityName = (model as EntityClass).name ?? model + + const compositeKeysValuesForFoundEntities = new Set( + entitiesToUpdate.map((entity) => { + return AbstractService_.buildUniqueCompositeKeyValue( + primaryKeys, + entity + ) + }) + ) + + const missingEntityValues: any[] = [] + + ;[...keySelectorDataMap.keys()].filter((key) => { + if (!compositeKeysValuesForFoundEntities.has(key)) { + const value = key.replace(/_/gi, " - ") + missingEntityValues.push(value) + } + }) + + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${entityName} with ${primaryKeys.join( + ", " + )} "${missingEntityValues.join(", ")}" not found` + ) + } + } + + return await this[propertyRepositoryName].update( + toUpdateData, + sharedContext + ) + } + + delete(idOrSelector: string, sharedContext?: Context): Promise + delete(idOrSelector: string[], sharedContext?: Context): Promise + delete(idOrSelector: object, sharedContext?: Context): Promise + delete(idOrSelector: object[], sharedContext?: Context): Promise + delete( + idOrSelector: { + selector: FilterQuery | BaseFilterable> + }, + sharedContext?: Context + ): Promise + + @InjectTransactionManager(doNotForceTransaction, propertyRepositoryName) + async delete( + idOrSelector: + | string + | string[] + | object + | object[] + | { + selector: FilterQuery | BaseFilterable> + }, + @MedusaContext() sharedContext: Context = {} + ): Promise { + if ( + !isDefined(idOrSelector) || + (Array.isArray(idOrSelector) && idOrSelector.length === 0) + ) { + return + } + + const primaryKeys = AbstractService_.retrievePrimaryKeys(model) + + if ( + (Array.isArray(idOrSelector) && idOrSelector.length === 0) || + ((isString(idOrSelector) || + (Array.isArray(idOrSelector) && isString(idOrSelector[0]))) && + primaryKeys.length > 1) + ) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `${ + primaryKeys.length === 1 + ? `"${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}"` + : `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}` + } must be defined` + ) + } + + const deleteCriteria: any = { + $or: [], + } + + if (isObject(idOrSelector) && "selector" in idOrSelector) { + const entitiesToDelete = await this.list( + idOrSelector.selector as FilterQuery, + { + select: primaryKeys, + }, + sharedContext + ) + + for (const entity of entitiesToDelete) { + const criteria = {} + primaryKeys.forEach((key) => { + criteria[key] = entity[key] + }) + deleteCriteria.$or.push(criteria) + } + } else { + const primaryKeysValues = Array.isArray(idOrSelector) + ? idOrSelector + : [idOrSelector] + + deleteCriteria.$or = primaryKeysValues.map((primaryKeyValue) => { + const criteria = {} + + if (isObject(primaryKeyValue)) { + Object.entries(primaryKeyValue).forEach(([key, value]) => { + criteria[key] = value + }) + } else { + criteria[primaryKeys[0]] = primaryKeyValue + } + + // TODO: Revisit + /*primaryKeys.forEach((key) => { + /!*if ( + isObject(primaryKeyValue) && + !isDefined(primaryKeyValue[key]) && + // primaryKeys.length > 1 + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Composite key must contain all primary key fields: ${primaryKeys.join( + ", " + )}. Found: ${Object.keys(primaryKeyValue)}` + ) + }*!/ + + criteria[key] = isObject(primaryKeyValue) + ? primaryKeyValue[key] + : primaryKeyValue + })*/ + return criteria + }) + } + + await this[propertyRepositoryName].delete(deleteCriteria, sharedContext) + } + + @InjectTransactionManager(propertyRepositoryName) + async softDelete( + idsOrFilter: string[] | InternalFilterQuery, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this[propertyRepositoryName].softDelete( + idsOrFilter, + sharedContext + ) + } + + @InjectTransactionManager(propertyRepositoryName) + async restore( + idsOrFilter: string[] | InternalFilterQuery, + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this[propertyRepositoryName].restore( + idsOrFilter, + sharedContext + ) + } + + upsert(data: any[], sharedContext?: Context): Promise + upsert(data: any, sharedContext?: Context): Promise + + @InjectTransactionManager(propertyRepositoryName) + async upsert( + data: any | any[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const data_ = Array.isArray(data) ? data : [data] + const entities = await this[propertyRepositoryName].upsert( + data_, + sharedContext + ) + return Array.isArray(data) ? entities : entities[0] + } + } + + return AbstractService_ as unknown as new ( + container: TContainer + ) => ModulesSdkTypes.InternalModuleService +} diff --git a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts index 3d48981505b6f..e89cca934a78f 100644 --- a/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts +++ b/packages/utils/src/modules-sdk/loaders/container-loader-factory.ts @@ -8,7 +8,7 @@ import { } from "@medusajs/types" import { lowerCaseFirst } from "../../common" import { asClass } from "awilix" -import { abstractServiceFactory } from "../abstract-service-factory" +import { internalModuleServiceFactory } from "../internal-module-service-factory" import { mikroOrmBaseRepositoryFactory } from "../../dal" type RepositoryLoaderOptions = { @@ -96,7 +96,7 @@ export function loadModuleServices({ const finalService = moduleServicesMap.get(mappedServiceName) if (!finalService) { - moduleServicesMap.set(mappedServiceName, abstractServiceFactory(Model)) + moduleServicesMap.set(mappedServiceName, internalModuleServiceFactory(Model)) } }) diff --git a/packages/workflow-engine-inmemory/src/services/index.ts b/packages/workflow-engine-inmemory/src/services/index.ts index 5a6d313d860b3..75bcf7eb47f97 100644 --- a/packages/workflow-engine-inmemory/src/services/index.ts +++ b/packages/workflow-engine-inmemory/src/services/index.ts @@ -1,3 +1,2 @@ -export * from "./workflow-execution" export * from "./workflow-orchestrator" export * from "./workflows-module" diff --git a/packages/workflow-engine-inmemory/src/services/workflow-execution.ts b/packages/workflow-engine-inmemory/src/services/workflow-execution.ts deleted file mode 100644 index 158557ec0bae8..0000000000000 --- a/packages/workflow-engine-inmemory/src/services/workflow-execution.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { WorkflowExecution } from "@models" - -type InjectedDependencies = { - workflowExecutionRepository: DAL.RepositoryService -} - -export class WorkflowExecutionService< - TEntity extends WorkflowExecution = WorkflowExecution -> extends ModulesSdkUtils.abstractServiceFactory( - WorkflowExecution -) { - protected workflowExecutionRepository_: DAL.RepositoryService - - constructor({ workflowExecutionRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.workflowExecutionRepository_ = workflowExecutionRepository - } -} diff --git a/packages/workflow-engine-inmemory/src/services/workflows-module.ts b/packages/workflow-engine-inmemory/src/services/workflows-module.ts index 31be5674d58a3..789fa5760092b 100644 --- a/packages/workflow-engine-inmemory/src/services/workflows-module.ts +++ b/packages/workflow-engine-inmemory/src/services/workflows-module.ts @@ -4,8 +4,8 @@ import { FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" -import {} from "@medusajs/types/src" import { InjectManager, InjectSharedContext, @@ -16,15 +16,12 @@ import type { UnwrapWorkflowInputDataType, WorkflowOrchestratorTypes, } from "@medusajs/workflows-sdk" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" +import { WorkflowOrchestratorService } from "@services" import { joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService workflowOrchestratorService: WorkflowOrchestratorService } @@ -32,7 +29,7 @@ export class WorkflowsModuleService implements WorkflowOrchestratorTypes.IWorkflowsModuleService { protected baseRepository_: DAL.RepositoryService - protected workflowExecutionService_: WorkflowExecutionService + protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService protected workflowOrchestratorService_: WorkflowOrchestratorService constructor( @@ -64,7 +61,7 @@ export class WorkflowsModuleService sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< WorkflowOrchestratorTypes.WorkflowExecutionDTO[] >(wfExecutions, { populate: true, diff --git a/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts b/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts index 7254f3b90dc2d..60c9771def6b7 100644 --- a/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts +++ b/packages/workflow-engine-inmemory/src/utils/workflow-orchestrator-storage.ts @@ -5,14 +5,12 @@ import { TransactionStep, } from "@medusajs/orchestration" import { TransactionState } from "@medusajs/utils" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" +import { WorkflowOrchestratorService } from "@services" +import { ModulesSdkTypes } from "@medusajs/types" // eslint-disable-next-line max-len export class InMemoryDistributedTransactionStorage extends DistributedTransactionStorage { - private workflowExecutionService_: WorkflowExecutionService + private workflowExecutionService_: ModulesSdkTypes.InternalModuleService private workflowOrchestratorService_: WorkflowOrchestratorService private storage: Map = new Map() @@ -22,7 +20,7 @@ export class InMemoryDistributedTransactionStorage extends DistributedTransactio constructor({ workflowExecutionService, }: { - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService }) { super() diff --git a/packages/workflow-engine-redis/src/services/index.ts b/packages/workflow-engine-redis/src/services/index.ts index 5a6d313d860b3..75bcf7eb47f97 100644 --- a/packages/workflow-engine-redis/src/services/index.ts +++ b/packages/workflow-engine-redis/src/services/index.ts @@ -1,3 +1,2 @@ -export * from "./workflow-execution" export * from "./workflow-orchestrator" export * from "./workflows-module" diff --git a/packages/workflow-engine-redis/src/services/workflow-execution.ts b/packages/workflow-engine-redis/src/services/workflow-execution.ts deleted file mode 100644 index 158557ec0bae8..0000000000000 --- a/packages/workflow-engine-redis/src/services/workflow-execution.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { DAL } from "@medusajs/types" -import { ModulesSdkUtils } from "@medusajs/utils" -import { WorkflowExecution } from "@models" - -type InjectedDependencies = { - workflowExecutionRepository: DAL.RepositoryService -} - -export class WorkflowExecutionService< - TEntity extends WorkflowExecution = WorkflowExecution -> extends ModulesSdkUtils.abstractServiceFactory( - WorkflowExecution -) { - protected workflowExecutionRepository_: DAL.RepositoryService - - constructor({ workflowExecutionRepository }: InjectedDependencies) { - // @ts-ignore - super(...arguments) - this.workflowExecutionRepository_ = workflowExecutionRepository - } -} diff --git a/packages/workflow-engine-redis/src/services/workflows-module.ts b/packages/workflow-engine-redis/src/services/workflows-module.ts index 31be5674d58a3..6667236626790 100644 --- a/packages/workflow-engine-redis/src/services/workflows-module.ts +++ b/packages/workflow-engine-redis/src/services/workflows-module.ts @@ -4,8 +4,8 @@ import { FindConfig, InternalModuleDeclaration, ModuleJoinerConfig, + ModulesSdkTypes, } from "@medusajs/types" -import {} from "@medusajs/types/src" import { InjectManager, InjectSharedContext, @@ -16,15 +16,12 @@ import type { UnwrapWorkflowInputDataType, WorkflowOrchestratorTypes, } from "@medusajs/workflows-sdk" -import { - WorkflowExecutionService, - WorkflowOrchestratorService, -} from "@services" -import { joinerConfig } from "../joiner-config" +import {WorkflowOrchestratorService} from "@services" +import {joinerConfig} from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService workflowOrchestratorService: WorkflowOrchestratorService } @@ -32,7 +29,7 @@ export class WorkflowsModuleService implements WorkflowOrchestratorTypes.IWorkflowsModuleService { protected baseRepository_: DAL.RepositoryService - protected workflowExecutionService_: WorkflowExecutionService + protected workflowExecutionService_: ModulesSdkTypes.InternalModuleService protected workflowOrchestratorService_: WorkflowOrchestratorService constructor( @@ -64,7 +61,7 @@ export class WorkflowsModuleService sharedContext ) - return this.baseRepository_.serialize< + return await this.baseRepository_.serialize< WorkflowOrchestratorTypes.WorkflowExecutionDTO[] >(wfExecutions, { populate: true, diff --git a/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts b/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts index 533181cf7f8fc..512c8e7cf6760 100644 --- a/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts +++ b/packages/workflow-engine-redis/src/utils/workflow-orchestrator-storage.ts @@ -5,8 +5,8 @@ import { TransactionStep, } from "@medusajs/orchestration" import { TransactionState } from "@medusajs/utils" +import { ModulesSdkTypes } from "@medusajs/types" import { - WorkflowExecutionService, WorkflowOrchestratorService, } from "@services" import { Queue, Worker } from "bullmq" @@ -21,7 +21,7 @@ enum JobType { // eslint-disable-next-line max-len export class RedisDistributedTransactionStorage extends DistributedTransactionStorage { private static TTL_AFTER_COMPLETED = 60 * 15 // 15 minutes - private workflowExecutionService_: WorkflowExecutionService + private workflowExecutionService_: ModulesSdkTypes.InternalModuleService private workflowOrchestratorService_: WorkflowOrchestratorService private redisClient: Redis @@ -34,7 +34,7 @@ export class RedisDistributedTransactionStorage extends DistributedTransactionSt redisWorkerConnection, redisQueueName, }: { - workflowExecutionService: WorkflowExecutionService + workflowExecutionService: ModulesSdkTypes.InternalModuleService, redisConnection: Redis redisWorkerConnection: Redis redisQueueName: string diff --git a/yarn.lock b/yarn.lock index 5775225d010e6..a0c934b17d200 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7902,7 +7902,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.3.1 jest: ^29.6.3 jsonwebtoken: ^9.0.2 knex: 2.4.2 @@ -8022,7 +8022,7 @@ __metadata: "@mikro-orm/postgresql": 5.9.7 awilix: ^8.0.0 cross-env: ^5.2.1 - dotenv: ^16.1.4 + dotenv: 16.3.1 jest: ^29.6.3 knex: 2.4.2 medusa-test-utils: ^1.1.40 @@ -8050,8 +8050,10 @@ __metadata: "@medusajs/ui-preset": "workspace:^" "@medusajs/vite-plugin-extension": "workspace:^" "@radix-ui/react-collapsible": 1.0.3 + "@radix-ui/react-hover-card": ^1.0.7 "@tanstack/react-query": 4.22.0 "@tanstack/react-table": 8.10.7 + "@types/node": ^20.11.15 "@types/react": 18.2.43 "@types/react-dom": 18.2.17 "@uiw/react-json-view": 2.0.0-alpha.10 @@ -8070,7 +8072,7 @@ __metadata: react-hook-form: 7.49.1 react-i18next: 13.5.0 react-router-dom: 6.20.1 - tailwindcss: 3.3.6 + tailwindcss: ^3.4.1 typescript: 5.2.2 vite: 5.0.10 zod: 3.22.4 @@ -8655,7 +8657,7 @@ __metadata: dependencies: "@medusajs/toolbox": ^0.0.1 "@tailwindcss/forms": ^0.5.3 - tailwindcss: ^3.3.2 + tailwindcss: ^3.4.1 tailwindcss-animate: ^1.0.6 tsup: ^7.1.0 typescript: ^5.1.6 @@ -8726,7 +8728,7 @@ __metadata: rimraf: ^5.0.1 storybook: ^7.0.23 tailwind-merge: ^1.13.2 - tailwindcss: ^3.3.2 + tailwindcss: ^3.4.1 tsc-alias: ^1.8.7 typescript: ^5.1.6 vite: ^4.3.9 @@ -10424,6 +10426,34 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-hover-card@npm:^1.0.7": + version: 1.0.7 + resolution: "@radix-ui/react-hover-card@npm:1.0.7" + dependencies: + "@babel/runtime": ^7.13.10 + "@radix-ui/primitive": 1.0.1 + "@radix-ui/react-compose-refs": 1.0.1 + "@radix-ui/react-context": 1.0.1 + "@radix-ui/react-dismissable-layer": 1.0.5 + "@radix-ui/react-popper": 1.1.3 + "@radix-ui/react-portal": 1.0.4 + "@radix-ui/react-presence": 1.0.1 + "@radix-ui/react-primitive": 1.0.3 + "@radix-ui/react-use-controllable-state": 1.0.1 + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: f29f3da5bd9a967b5a35e91ac2d1b223191c7a074550d9d9cc10a0c0baf62ba0705b32912a7d2ef1ea5c27dd5e130a9fda9cbe6c2a7f3c2037ed5dfed89aa8cc + languageName: node + linkType: hard + "@radix-ui/react-id@npm:1.0.0": version: 1.0.0 resolution: "@radix-ui/react-id@npm:1.0.0" @@ -17668,6 +17698,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.15": + version: 20.11.15 + resolution: "@types/node@npm:20.11.15" + dependencies: + undici-types: ~5.26.4 + checksum: 7dfab4208fedc02e9584c619551906f46ade7955bb929b1e32e354a50522eb532d6bfb2844fdaad2c8dca03be84a590674460c64cb101e1a33bb318e1ec448d4 + languageName: node + linkType: hard + "@types/node@npm:^8.5.7": version: 8.10.66 resolution: "@types/node@npm:8.10.66" @@ -47893,42 +47932,9 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:3.3.6": - version: 3.3.6 - resolution: "tailwindcss@npm:3.3.6" - dependencies: - "@alloc/quick-lru": ^5.2.0 - arg: ^5.0.2 - chokidar: ^3.5.3 - didyoumean: ^1.2.2 - dlv: ^1.1.3 - fast-glob: ^3.3.0 - glob-parent: ^6.0.2 - is-glob: ^4.0.3 - jiti: ^1.19.1 - lilconfig: ^2.1.0 - micromatch: ^4.0.5 - normalize-path: ^3.0.0 - object-hash: ^3.0.0 - picocolors: ^1.0.0 - postcss: ^8.4.23 - postcss-import: ^15.1.0 - postcss-js: ^4.0.1 - postcss-load-config: ^4.0.1 - postcss-nested: ^6.0.1 - postcss-selector-parser: ^6.0.11 - resolve: ^1.22.2 - sucrase: ^3.32.0 - bin: - tailwind: lib/cli.js - tailwindcss: lib/cli.js - checksum: 69caade773249cb963c33e81f85b7fc423dcb74b416727483f434f4e12874187f633970c9de864fa96736289abaf71189314a53589ada0be6c09ccb0e8b78391 - languageName: node - linkType: hard - -"tailwindcss@npm:^3.3.2": - version: 3.3.4 - resolution: "tailwindcss@npm:3.3.4" +"tailwindcss@npm:^3.3.6": + version: 3.4.0 + resolution: "tailwindcss@npm:3.4.0" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -47955,13 +47961,13 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: a1a0c8c1793b1b1b67503484fe924dc84f79e74c1ddc576095d616eaecc18bbd8fcdbf7c62e07a181673466f4913ebc20d92b93b87da730148b05f7c95e6c83e + checksum: 0a1cef7468e6d17c2857d0b3c4017af2cb37ed8ba27dfb14780c517b8a74f6786970227c400ac1325fc8bcfc09099d8e990fa7c60924bf945f3d0a912d63f546 languageName: node linkType: hard -"tailwindcss@npm:^3.3.6": - version: 3.4.0 - resolution: "tailwindcss@npm:3.4.0" +"tailwindcss@npm:^3.4.1": + version: 3.4.1 + resolution: "tailwindcss@npm:3.4.1" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -47988,7 +47994,7 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: 0a1cef7468e6d17c2857d0b3c4017af2cb37ed8ba27dfb14780c517b8a74f6786970227c400ac1325fc8bcfc09099d8e990fa7c60924bf945f3d0a912d63f546 + checksum: eec3d758f1cd4f51ab3b4c201927c3ecd18e55f8ac94256af60276aaf8d1df78f9dddb5e9fb1e057dfa7cea3c1356add4994cc3d42da9739df874e67047e656f languageName: node linkType: hard