chore(medusa): cleanup medusa package (#7206)

This commit is contained in:
Adrien de Peretti authored and GitHub committed 2024-05-07 12:48:49 +02:00
1 parent 8b61dccd0f
commit 71f4f46cb9
1452 files changed
+4737 -234780

No files matched your search

-88
View File
@@ -1,88 +0,0 @@
import compression from "compression"
import { Router } from "express"
import { compressionOptions, shouldCompressResponse } from "../utils/api"
import errorHandler from "./middlewares/error-handler"
import admin from "./routes/admin"
import store from "./routes/store"
// guaranteed to get dependencies
export default (container, config) => {
const app = Router()
const httpCompressionOptions = compressionOptions(config)
if (httpCompressionOptions.enabled) {
app.use(
compression({
filter: shouldCompressResponse,
...httpCompressionOptions,
})
)
}
admin(app, container, config)
store(app, container, config)
app.use(errorHandler())
return app
}
// Admin
export * from "./routes/admin/analytics-configs"
export * from "./routes/admin/auth"
export * from "./routes/admin/batch"
export * from "./routes/admin/collections"
export * from "./routes/admin/currencies"
export * from "./routes/admin/customer-groups"
export * from "./routes/admin/customers"
export * from "./routes/admin/discounts"
export * from "./routes/admin/draft-orders"
export * from "./routes/admin/gift-cards"
export * from "./routes/admin/inventory-items"
export * from "./routes/admin/invites"
export * from "./routes/admin/notes"
export * from "./routes/admin/notifications"
export * from "./routes/admin/order-edits"
export * from "./routes/admin/orders"
export * from "./routes/admin/payment-collections"
export * from "./routes/admin/payments"
export * from "./routes/admin/price-lists"
export * from "./routes/admin/product-categories"
export * from "./routes/admin/product-tags"
export * from "./routes/admin/product-types"
export * from "./routes/admin/products"
export * from "./routes/admin/publishable-api-keys"
export * from "./routes/admin/regions"
export * from "./routes/admin/reservations"
export * from "./routes/admin/return-reasons"
export * from "./routes/admin/returns"
export * from "./routes/admin/sales-channels"
export * from "./routes/admin/shipping-options"
export * from "./routes/admin/shipping-profiles"
export * from "./routes/admin/stock-locations"
export * from "./routes/admin/store"
export * from "./routes/admin/swaps"
export * from "./routes/admin/tax-rates"
export * from "./routes/admin/uploads"
export * from "./routes/admin/users"
export * from "./routes/admin/variants"
export * from "./routes/admin/workflows-executions"
// Store
export * from "./routes/store/auth"
export * from "./routes/store/carts"
export * from "./routes/store/collections"
export * from "./routes/store/customers"
export * from "./routes/store/gift-cards"
export * from "./routes/store/order-edits"
export * from "./routes/store/orders"
export * from "./routes/store/payment-collections"
export * from "./routes/store/product-categories"
export * from "./routes/store/product-tags"
export * from "./routes/store/product-types"
export * from "./routes/store/products"
export * from "./routes/store/regions"
export * from "./routes/store/return-reasons"
export * from "./routes/store/returns"
export * from "./routes/store/shipping-options"
export * from "./routes/store/swaps"
export * from "./routes/store/variants"
@@ -1,20 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import BatchJobService from "../../../services/batch-job"
export async function canAccessBatchJob(req, res, next) {
const id = req.params.id
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
const batch_job = req.batch_job ?? (await batchJobService.retrieve(id))
const userId = req.user.id ?? req.user.userId
if (batch_job.created_by !== userId) {
return next(
new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Cannot access a batch job that does not belong to the logged in user"
)
)
}
next()
}
@@ -1,14 +0,0 @@
import BatchJobService from "../../../services/batch-job"
export async function getRequestedBatchJob(req, res, next) {
const id = req.params.id
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
try {
req.batch_job = await batchJobService.retrieve(id)
} catch (error) {
return next(error)
}
next()
}
@@ -1,33 +0,0 @@
import { MedusaError } from "medusa-core-utils"
import { DiscountConditionService, DiscountService } from "../../../services"
export async function doesConditionBelongToDiscount(req, res, next) {
try {
const { discount_id, condition_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const discountService: DiscountService =
req.scope.resolve("discountService")
const discount = await discountService.retrieve(discount_id, {
select: ["id", "rule_id"],
})
const condition = await conditionService.retrieve(condition_id, {
select: ["id", "discount_rule_id"],
})
if (condition.discount_rule_id !== discount.rule_id) {
return next(
new MedusaError(
MedusaError.Types.INVALID_DATA,
`Condition with id ${condition_id} does not belong to Discount with id ${discount_id}`
)
)
}
next()
} catch (e) {
next(e)
}
}
@@ -1,34 +0,0 @@
import { default as authenticate } from "./authenticate"
import { default as authenticateCustomer } from "./authenticate-customer"
import { default as wrap } from "./await-middleware"
import { default as normalizeQuery } from "./normalized-query"
import { default as requireCustomerAuthentication } from "./require-customer-authentication"
export { default as authenticate } from "./authenticate"
export { default as authenticateCustomer } from "./authenticate-customer"
export { default as wrapHandler } from "./await-middleware"
export { canAccessBatchJob } from "./batch-job/can-access-batch-job"
export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job"
export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount"
export { default as errorHandler } from "./error-handler"
export { isFeatureFlagEnabled } from "./feature-flag-enabled"
export { default as normalizeQuery } from "./normalized-query"
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
export { transformBody } from "./transform-body"
export { transformIncludesOptions } from "./transform-includes-options"
export { transformQuery, transformStoreQuery } from "./transform-query"
/**
* @deprecated you can now import the middlewares directly without passing by the default export
* e.g `import { authenticate } from "@medusajs/medusa"
*/
export default {
authenticate,
authenticateCustomer,
requireCustomerAuthentication,
normalizeQuery,
/**
* @deprecated use `import { wrapHandler } from "@medusajs/medusa"`
*/
wrap,
}
@@ -1,40 +0,0 @@
import { NextFunction, Request, Response } from "express"
import PublishableApiKeyService from "../../../services/publishable-api-key"
export type PublishableApiKeyScopes = {
sales_channel_ids: string[]
}
/**
* The middleware, in case that a key is present in the request header,
* attaches ids of resources within the scope of the key to the req object.
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*
* @throws if sales channel id is passed as a url or body param
* but that id is not in the scope defined by the PK from the header
*/
async function extendRequestParams(
req: Request & { publishableApiKeyScopes: PublishableApiKeyScopes },
res: Response,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
"publishableApiKeyService"
)
req.publishableApiKeyScopes = await publishableKeyService.getResourceScopes(
pubKey
)
}
next()
}
export { extendRequestParams }
@@ -1,51 +0,0 @@
import { NextFunction, Request, Response } from "express"
import PublishableApiKeyService from "../../../services/publishable-api-key"
import { ProductService } from "../../../services"
/**
* The middleware check if requested product is assigned to a SC associated with PK in the header.
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*/
async function validateProductSalesChannelAssociation(
req: Request,
res: Response,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const productService: ProductService = req.scope.resolve("productService")
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
"publishableApiKeyService"
)
const { sales_channel_ids: salesChannelIds } =
await publishableKeyService.getResourceScopes(pubKey)
let isProductInSalesChannel = false
try {
isProductInSalesChannel = await productService.isProductInSalesChannels(
req.params.id,
salesChannelIds
)
} catch (error) {
next(error)
}
if (salesChannelIds.length && !isProductInSalesChannel) {
req.errors = req.errors ?? []
req.errors.push(
`Product with id: ${req.params.id} is not associated with sales channels defined by the Publishable API Key passed in the header of the request.`
)
}
}
next()
}
export { validateProductSalesChannelAssociation }
@@ -1,46 +0,0 @@
import { NextFunction, Request, Response } from "express"
import { PublishableApiKeyScopes } from "./extend-request-params"
/**
* The middleware will return 400 if sales channel id is passed as an url or body param
* but that id is not in the scope of the PK from the header.
*
* NOTE: must be applied after the `extendRequestParams` middleware
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*/
async function validateSalesChannelParam(
req: Request & { publishableApiKeyScopes: PublishableApiKeyScopes },
res: Response,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const scopes = req.publishableApiKeyScopes
let channelIds = req.body.sales_channel_id || req.query.sales_channel_id
if (!channelIds) {
return next()
}
channelIds = !Array.isArray(channelIds) ? [channelIds] : channelIds
if (
scopes.sales_channel_ids.length &&
!channelIds.every((sc) => scopes.sales_channel_ids.includes(sc))
) {
req.errors = req.errors ?? []
req.errors.push(
`Provided sales channel id param: ${channelIds} is not associated with the Publishable API Key passed in the header of the request.`
)
}
}
next()
}
export { validateSalesChannelParam }
@@ -1,54 +0,0 @@
import { NextFunction, Request, Response } from "express"
import PublishableApiKeyService from "../../../services/publishable-api-key"
import { ProductVariantService } from "../../../services"
/**
* The middleware check if requested product is assigned to a SC associated with PK in the header.
*
* @param req - request object
* @param res - response object
* @param next - next middleware call
*/
async function validateProductVariantSalesChannelAssociation(
req: Request,
res: Response,
next: NextFunction
) {
const pubKey = req.get("x-publishable-api-key")
if (pubKey) {
const productVariantService: ProductVariantService = req.scope.resolve(
"productVariantService"
)
const publishableKeyService: PublishableApiKeyService = req.scope.resolve(
"publishableApiKeyService"
)
const { sales_channel_ids: salesChannelIds } =
await publishableKeyService.getResourceScopes(pubKey)
let isVariantInSalesChannel = false
try {
isVariantInSalesChannel =
await productVariantService.isVariantInSalesChannels(
req.params.id,
salesChannelIds
)
} catch (error) {
next(error)
}
if (salesChannelIds.length && !isVariantInSalesChannel) {
req.errors = req.errors ?? []
req.errors.push(
`Variant with id: ${req.params.id} is not associated with sales channels defined by the Publishable API Key passed in the header of the request.`
)
}
}
next()
}
export { validateProductVariantSalesChannelAssociation }
@@ -1,37 +0,0 @@
import { NextFunction, Request, Response } from "express"
type GetProductsRequiredParams = {
id: string
}
export function validateProductsExist<T extends GetProductsRequiredParams = GetProductsRequiredParams>(
getProducts: (req) => T[]
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
const requestedProducts = getProducts(req)
if (!requestedProducts?.length) {
return next()
}
const productService = req.scope.resolve("productService")
const requestedProductIds = requestedProducts.map((product) => product.id)
const [productRecords] = await productService.listAndCount({
id: requestedProductIds,
})
const nonExistingProducts = requestedProductIds.filter(
(requestedProductId) =>
productRecords.findIndex(
(productRecord) => productRecord.id === requestedProductId
) === -1
)
if (nonExistingProducts.length) {
req.errors = req.errors ?? []
req.errors.push(`Products ${nonExistingProducts.join(", ")} do not exist`)
}
return next()
}
}
@@ -1,37 +0,0 @@
import { NextFunction, Request, Response } from "express"
import { SalesChannelService } from "../../../services"
import { ProductSalesChannelReq } from "../../../types/product"
export function validateSalesChannelsExist(
getSalesChannels: (req) => ProductSalesChannelReq[] | undefined
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
const salesChannels = getSalesChannels(req)
if (!salesChannels?.length) {
return next()
}
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const salesChannelIds = salesChannels.map((salesChannel) => salesChannel.id)
const [existingChannels] = await salesChannelService.listAndCount({
id: salesChannelIds,
})
const nonExistingSalesChannels = salesChannelIds.filter(
(scId) => existingChannels.findIndex((sc) => sc.id === scId) === -1
)
if (nonExistingSalesChannels.length) {
req.errors = req.errors ?? []
req.errors.push(
`Sales Channels ${nonExistingSalesChannels.join(", ")} do not exist`
)
}
return next()
}
}
@@ -1,51 +0,0 @@
import { NextFunction, Request, Response } from "express"
import { FlagRouter, MedusaV2Flag } from "@medusajs/utils"
import SalesChannelFeatureFlag from "../../loaders/feature-flags/sales-channels"
import { SalesChannelService } from "../../services"
/**
* Middleware that includes the default sales channel on the request, if no sales channels present
* @param context Object of options
* @param context.attachChannelAsArray Whether to attach the default sales channel as an array or just a string
*/
export function withDefaultSalesChannel(
{
attachChannelAsArray,
}: {
attachChannelAsArray: boolean
} = { attachChannelAsArray: false }
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, _, next: NextFunction) => {
const featureFlagRouter = req.scope.resolve(
"featureFlagRouter"
) as FlagRouter
if (
!featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key) ||
// Do not attach the default SC if the isolate product domain feature flag is enabled
featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key) ||
req.query.sales_channel_id?.length ||
req.get("x-publishable-api-key")
) {
return next()
}
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
try {
const defaultSalesChannel = await salesChannelService.retrieveDefault()
if (defaultSalesChannel?.id) {
req.query.sales_channel_id = attachChannelAsArray
? [defaultSalesChannel.id]
: defaultSalesChannel.id
}
} catch {
// noop
} finally {
next()
}
}
}
@@ -1,33 +0,0 @@
import { IsBoolean } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { AnalyticsConfigService } from "../../../../services"
import { CreateAnalyticsConfig } from "../../../../types/analytics-config"
// No OAS for this route, for internal use only.
export default async (req: Request, res: Response) => {
const userId = (req.user?.userId ?? req.user?.id)!
const validatedBody = req.validatedBody as CreateAnalyticsConfig
const analyticsConfigService: AnalyticsConfigService = req.scope.resolve(
"analyticsConfigService"
)
const manager: EntityManager = req.scope.resolve("manager")
const analyticsConfig = await manager.transaction(
async (transactionManager) => {
return await analyticsConfigService
.withTransaction(transactionManager)
.create(userId, validatedBody)
}
)
res.status(200).json({ analytics_config: analyticsConfig })
}
export class AdminPostAnalyticsConfigReq {
@IsBoolean()
opt_out: boolean
@IsBoolean()
anonymize?: boolean = false
}
@@ -1,23 +0,0 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { AnalyticsConfigService } from "../../../../services"
// No OAS for this route, for internal use only.
export default async (req: Request, res: Response) => {
const userId = (req.user?.userId ?? req.user?.id)!
const analyticsConfigService: AnalyticsConfigService = req.scope.resolve(
"analyticsConfigService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await analyticsConfigService
.withTransaction(transactionManager)
.delete(userId)
})
res
.status(200)
.json({ user_id: userId, object: "analytics_config", deleted: true })
}
@@ -1,14 +0,0 @@
import { Request, Response } from "express"
import { AnalyticsConfigService } from "../../../../services"
// No OAS for this route, for internal use only.
export default async (req: Request, res: Response): Promise<void> => {
const userId = (req.user?.userId ?? req.user?.id)!
const analyticsConfigService: AnalyticsConfigService = req.scope.resolve(
"analyticsConfigService"
)
const analyticsConfig = await analyticsConfigService.retrieve(userId)
res.status(200).json({ analytics_config: analyticsConfig })
}
@@ -1,43 +0,0 @@
import { Router } from "express"
import { AnalyticsConfig } from "../../../.."
import { DeleteResponse } from "../../../../types/common"
import middlewares, { transformBody } from "../../../middlewares"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { AdminPostAnalyticsConfigReq } from "./create-analytics-config"
import { AdminPostAnalyticsConfigAnalyticsConfigReq } from "./update-analytics-config"
const route = Router()
export default (app: Router) => {
app.use("/analytics-configs", isFeatureFlagEnabled("analytics"), route)
route.get("/", middlewares.wrap(require("./get-analytics-config").default))
route.post(
"/",
transformBody(AdminPostAnalyticsConfigReq),
middlewares.wrap(require("./create-analytics-config").default)
)
route.post(
"/update",
transformBody(AdminPostAnalyticsConfigAnalyticsConfigReq),
middlewares.wrap(require("./update-analytics-config").default)
)
route.delete(
"/",
middlewares.wrap(require("./delete-analytics-config").default)
)
return app
}
export type AdminAnalyticsConfigRes = {
analytics_config: AnalyticsConfig
}
export type AdminAnalyticsConfigDeleteRes = DeleteResponse
export * from "./create-analytics-config"
export * from "./update-analytics-config"
@@ -1,35 +0,0 @@
import { IsBoolean, IsOptional } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { AnalyticsConfigService } from "../../../../services"
import { UpdateAnalyticsConfig } from "../../../../types/analytics-config"
// No OAS for this route, for internal use only.
export default async (req: Request, res: Response) => {
const userId = (req.user?.userId ?? req.user?.id)!
const validatedBody = req.validatedBody as UpdateAnalyticsConfig
const analyticsConfigService: AnalyticsConfigService = req.scope.resolve(
"analyticsConfigService"
)
const manager: EntityManager = req.scope.resolve("manager")
const analyticsConfig = await manager.transaction(
async (transactionManager) => {
return await analyticsConfigService
.withTransaction(transactionManager)
.update(userId, validatedBody)
}
)
res.status(200).json({ analytics_config: analyticsConfig })
}
export class AdminPostAnalyticsConfigAnalyticsConfigReq {
@IsOptional()
@IsBoolean()
opt_out?: boolean
@IsOptional()
@IsBoolean()
anonymize?: boolean
}
@@ -1,98 +0,0 @@
import { IsNotEmpty, IsString } from "class-validator"
import { OauthService } from "../../../../services"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/apps/authorizations
* operationId: "PostApps"
* summary: "Generate Token for App"
* description: "Use an app's Oauth provider to generate and store a new token for authentication."
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostAppsReq"
* x-codegen:
* method: authorize
* x-codeSamples:
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/apps/authorizations' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "application_name": "example",
* "state": "ready",
* "code": "token"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Apps Oauth
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminAppsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(AdminPostAppsReq, req.body)
const oauthService: OauthService = req.scope.resolve("oauthService")
const data = await oauthService.generateToken(
validated.application_name,
validated.code,
validated.state
)
res.status(200).json({ apps: data })
}
/**
* @schema AdminPostAppsReq
* type: object
* required:
* - application_name
* - state
* - code
* properties:
* application_name:
* type: string
* description: Name of the application for to generate the token for.
* state:
* type: string
* description: State of the application.
* code:
* type: string
* description: The code for the generated token.
*/
export class AdminPostAppsReq {
@IsString()
@IsNotEmpty()
application_name: string
@IsString()
@IsNotEmpty()
state: string
@IsString()
@IsNotEmpty()
code: string
}
@@ -1,47 +0,0 @@
import { Router } from "express"
import { Oauth } from "../../../.."
import middlewares from "../../../middlewares"
const route = Router()
export default (app) => {
app.use("/apps", route)
route.get("/", middlewares.wrap(require("./list").default))
route.post(
"/authorizations",
middlewares.wrap(require("./authorize-app").default)
)
return app
}
/**
* @schema AdminAppsRes
* type: object
* required:
* - apps
* properties:
* apps:
* description: App details.
* $ref: "#/components/schemas/OAuth"
*/
export type AdminAppsRes = {
apps: Oauth
}
/**
* @schema AdminAppsListRes
* type: object
* required:
* - apps
* properties:
* apps:
* type: array
* description: An array of app details.
* items:
* $ref: "#/components/schemas/OAuth"
*/
export type AdminAppsListRes = {
apps: Oauth[]
}
@@ -1,48 +0,0 @@
import { OauthService } from "../../../../services"
/**
* @oas [get] /admin/apps
* operationId: "GetApps"
* summary: "List Applications"
* description: "Retrieve a list of applications registered in the Medusa backend."
* x-authenticated: true
* x-codegen:
* method: list
* x-codeSamples:
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/apps' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Apps Oauth
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminAppsListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const oauthService: OauthService = req.scope.resolve("oauthService")
const data = await oauthService.list({})
res.status(200).json({ apps: data })
}
@@ -1,141 +0,0 @@
import { IsEmail, IsNotEmpty, IsString } from "class-validator"
import jwt from "jsonwebtoken"
import _ from "lodash"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import AuthService from "../../../../services/auth"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/auth
* operationId: "PostAuth"
* summary: "User Login"
* x-authenticated: false
* description: "Log a User in and includes the Cookie session in the response header. The cookie session can be used in subsequent requests to authorize the user to perform admin functionalities.
* When using Medusa's JS or Medusa React clients, the cookie is automatically attached to subsequent requests."
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostAuthReq"
* x-codegen:
* method: createSession
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* medusa.admin.auth.createSession({
* email: "user@example.com",
* password: "supersecret"
* })
* .then(({ user }) => {
* console.log(user.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminLogin } from "medusa-react"
*
* const Login = () => {
* const adminLogin = useAdminLogin()
* // ...
*
* const handleLogin = () => {
* adminLogin.mutate({
* email: "user@example.com",
* password: "supersecret",
* }, {
* onSuccess: ({ user }) => {
* console.log(user)
* }
* })
* }
*
* // ...
* }
*
* export default Login
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/auth' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "email": "user@example.com",
* "password": "supersecret"
* }'
* tags:
* - Auth
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminAuthRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/incorrect_credentials"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(AdminPostAuthReq, req.body)
const authService: AuthService = req.scope.resolve("authService")
const manager: EntityManager = req.scope.resolve("manager")
const result = await manager.transaction(async (transactionManager) => {
return await authService
.withTransaction(transactionManager)
.authenticate(validated.email, validated.password)
})
if (result.success && result.user) {
// Set user id on session, this is stored on the server.
req.session.user_id = result.user.id
const cleanRes = _.omit(result.user, ["password_hash"])
res.json({ user: cleanRes })
} else {
res.sendStatus(401)
}
}
/**
* @schema AdminPostAuthReq
* type: object
* description: The admin's credentials used to log in.
* required:
* - email
* - password
* properties:
* email:
* type: string
* description: The user's email.
* format: email
* password:
* type: string
* description: The user's password.
* format: password
*/
export class AdminPostAuthReq {
@IsEmail()
@IsNotEmpty()
email: string
@IsString()
@IsNotEmpty()
password: string
}
@@ -1,77 +0,0 @@
/**
* @oas [delete] /admin/auth
* operationId: "DeleteAuth"
* summary: "User Logout"
* x-authenticated: true
* description: "Delete the current session for the logged in user. This will only work if you're using Cookie session for authentication. If the API token is still passed in the header,
* the user is still authorized to perform admin functionalities in other API Routes."
* x-codegen:
* method: deleteSession
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in
* medusa.admin.auth.deleteSession()
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteSession } from "medusa-react"
*
* const Logout = () => {
* const adminLogout = useAdminDeleteSession()
* // ...
*
* const handleLogout = () => {
* adminLogout.mutate(undefined, {
* onSuccess: () => {
* // user logged out.
* }
* })
* }
*
* // ...
* }
*
* export default Logout
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/auth' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Auth
* responses:
* "200":
* description: OK
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
if (req.session.customer_id) {
// if we are also logged in as a customer, persist that session
delete req.session.user_id
} else {
// otherwise, destroy the session
req.session.destroy()
}
res.sendStatus(200)
}
@@ -1,84 +0,0 @@
import UserService from "../../../../services/user"
import _ from "lodash"
/**
* @oas [get] /admin/auth
* operationId: "GetAuth"
* summary: "Get Current User"
* x-authenticated: true
* description: "Get the currently logged in user's details."
* x-codegen:
* method: getSession
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.auth.getSession()
* .then(({ user }) => {
* console.log(user.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminGetSession } from "medusa-react"
*
* const Profile = () => {
* const { user, isLoading } = useAdminGetSession()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {user && <span>{user.email}</span>}
* </div>
* )
* }
*
* export default Profile
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/auth' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Auth
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminAuthRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
try {
const userId = req.user.id || req.user.userId
const userService: UserService = req.scope.resolve("userService")
const user = await userService.retrieve(userId)
const cleanRes = _.omit(user, ["password_hash"])
res.status(200).json({ user: cleanRes })
} catch (err) {
res.sendStatus(400)
}
}
@@ -1,99 +0,0 @@
import jwt from "jsonwebtoken"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import AuthService from "../../../../services/auth"
import { validator } from "../../../../utils/validator"
import { AdminPostAuthReq } from "./create-session"
/**
* @oas [post] /admin/auth/token
* operationId: "PostToken"
* summary: "User Login (JWT)"
* x-authenticated: false
* description: "After a successful login, a JWT token is returned, which can be used to send authenticated requests."
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostAuthReq"
* x-codegen:
* method: getToken
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* medusa.admin.auth.getToken({
* email: 'user@example.com',
* password: 'supersecret'
* })
* .then(({ access_token }) => {
* console.log(access_token);
* })
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/auth/token' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "email": "user@example.com",
* "password": "supersecret"
* }'
* tags:
* - Auth
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBearerAuthRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/incorrect_credentials"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const {
projectConfig: { jwt_secret },
} = req.scope.resolve("configModule")
if (!jwt_secret) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
"Please configure jwt_secret in your environment"
)
}
const validated = await validator(AdminPostAuthReq, req.body)
const authService: AuthService = req.scope.resolve("authService")
const manager: EntityManager = req.scope.resolve("manager")
const result = await manager.transaction(async (transactionManager) => {
return await authService
.withTransaction(transactionManager)
.authenticate(validated.email, validated.password)
})
if (result.success && result.user) {
// Create jwt token to send back
const token = jwt.sign(
{ user_id: result.user.id, domain: "admin" },
jwt_secret,
{
expiresIn: "24h",
}
)
res.json({ access_token: token })
} else {
res.sendStatus(401)
}
}
@@ -1,60 +0,0 @@
import { Router } from "express"
import { User } from "../../../.."
import middlewares from "../../../middlewares"
const route = Router()
export default (app) => {
app.use("/auth", route)
route.get(
"/",
middlewares.authenticate(),
middlewares.wrap(require("./get-session").default)
)
route.post("/", middlewares.wrap(require("./create-session").default))
route.delete(
"/",
middlewares.authenticate(),
middlewares.wrap(require("./delete-session").default)
)
route.post("/token", middlewares.wrap(require("./get-token").default))
return app
}
/**
* @schema AdminAuthRes
* type: object
* description: "The user's details."
* required:
* - user
* properties:
* user:
* description: User details.
* $ref: "#/components/schemas/User"
*/
export type AdminAuthRes = {
user: Omit<User, "password_hash">
}
/**
* @schema AdminBearerAuthRes
* type: object
* description: "The access token of the user, if they're authenticated successfully."
* properties:
* access_token:
* description: Access token that can be used to send authenticated requests.
* type: string
*/
export type AdminBearerAuthRes = {
access_token: string
}
export * from "./create-session"
export * from "./delete-session"
export * from "./get-session"
export * from "./get-token"
@@ -1,94 +0,0 @@
import { BatchJobService } from "../../../../services"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/batch-jobs/{id}/cancel
* operationId: "PostBatchJobsBatchJobCancel"
* summary: "Cancel a Batch Job"
* description: "Mark a batch job as canceled. When a batch job is canceled, the processing of the batch job doesn’t automatically stop."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the batch job.
* x-codegen:
* method: cancel
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.batchJobs.cancel(batchJobId)
* .then(({ batch_job }) => {
* console.log(batch_job.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCancelBatchJob } from "medusa-react"
*
* type Props = {
* batchJobId: string
* }
*
* const BatchJob = ({ batchJobId }: Props) => {
* const cancelBatchJob = useAdminCancelBatchJob(batchJobId)
* // ...
*
* const handleCancel = () => {
* cancelBatchJob.mutate(undefined, {
* onSuccess: ({ batch_job }) => {
* console.log(batch_job)
* }
* })
* }
*
* // ...
* }
*
* export default BatchJob
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/batch-jobs/{id}/cancel' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Batch Jobs
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBatchJobRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
let batch_job = req.batch_job
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
const manager: EntityManager = req.scope.resolve("manager")
batch_job = await manager.transaction(async (transactionManager) => {
return await batchJobService
.withTransaction(transactionManager)
.cancel(batch_job)
})
res.json({ batch_job })
}
@@ -1,94 +0,0 @@
import { BatchJobService } from "../../../../services"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/batch-jobs/{id}/confirm
* operationId: "PostBatchJobsBatchJobConfirmProcessing"
* summary: "Confirm a Batch Job"
* description: "When a batch job is created, it is not executed automatically if `dry_run` is set to `true`. This API Route confirms that the batch job should be executed."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the batch job.
* x-codegen:
* method: confirm
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.batchJobs.confirm(batchJobId)
* .then(({ batch_job }) => {
* console.log(batch_job.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminConfirmBatchJob } from "medusa-react"
*
* type Props = {
* batchJobId: string
* }
*
* const BatchJob = ({ batchJobId }: Props) => {
* const confirmBatchJob = useAdminConfirmBatchJob(batchJobId)
* // ...
*
* const handleConfirm = () => {
* confirmBatchJob.mutate(undefined, {
* onSuccess: ({ batch_job }) => {
* console.log(batch_job)
* }
* })
* }
*
* // ...
* }
*
* export default BatchJob
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/batch-jobs/{id}/confirm' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Batch Jobs
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBatchJobRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
let batch_job = req.batch_job
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
const manager: EntityManager = req.scope.resolve("manager")
batch_job = await manager.transaction(async (transactionManager) => {
return await batchJobService
.withTransaction(transactionManager)
.confirm(batch_job)
})
res.json({ batch_job })
}
@@ -1,169 +0,0 @@
import { IsBoolean, IsObject, IsOptional, IsString } from "class-validator"
import { BatchJob } from "../../../../models"
import BatchJobService from "../../../../services/batch-job"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/batch-jobs
* operationId: "PostBatchJobs"
* summary: "Create a Batch Job"
* description: "Create a Batch Job to be executed asynchronously in the Medusa backend. If `dry_run` is set to `true`, the batch job will not be executed until the it is confirmed,
* which can be done using the Confirm Batch Job API Route."
* externalDocs:
* description: "How to create a batch job"
* url: "https://docs.medusajs.com/development/batch-jobs/create#create-batch-job"
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostBatchesReq"
* x-codegen:
* method: create
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.batchJobs.create({
* type: 'product-export',
* context: {},
* dry_run: false
* }).then((({ batch_job }) => {
* console.log(batch_job.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateBatchJob } from "medusa-react"
*
* const CreateBatchJob = () => {
* const createBatchJob = useAdminCreateBatchJob()
* // ...
*
* const handleCreateBatchJob = () => {
* createBatchJob.mutate({
* type: "publish-products",
* context: {},
* dry_run: true
* }, {
* onSuccess: ({ batch_job }) => {
* console.log(batch_job)
* }
* })
* }
*
* // ...
* }
*
* export default CreateBatchJob
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/batch-jobs' \
* -H 'Content-Type: application/json' \
* -H 'x-medusa-access-token: {api_token}' \
* --data-raw '{
* "type": "product-export",
* "context": { }
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Batch Jobs
* responses:
* 201:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBatchJobRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(AdminPostBatchesReq, req.body)
const batchJobService: BatchJobService = req.scope.resolve("batchJobService")
const userId = req.user.id ?? req.user.userId
const manager: EntityManager = req.scope.resolve("manager")
const batch_job = await manager.transaction(async (transactionManager) => {
const toCreate = await batchJobService
.withTransaction(transactionManager)
.prepareBatchJobForProcessing(validated, req)
return await batchJobService.withTransaction(transactionManager).create({
...toCreate,
created_by: userId,
})
})
res.status(201).json({ batch_job })
}
/**
* @schema AdminPostBatchesReq
* type: object
* description: The details of the batch job to create.
* required:
* - type
* - context
* properties:
* type:
* type: string
* description: >-
* The type of batch job to start, which is defined by the `batchType` property of the associated batch job strategy.
* example: product-export
* context:
* type: object
* description: Additional infomration regarding the batch to be used for processing.
* example:
* shape:
* prices:
* - region: null
* currency_code: "eur"
* dynamicImageColumnCount: 4
* dynamicOptionColumnCount: 2
* list_config:
* skip: 0
* take: 50
* order:
* created_at: "DESC"
* relations:
* - variants
* - variant.prices
* - images
* dry_run:
* type: boolean
* description: Set a batch job in dry_run mode, which would delay executing the batch job until it's confirmed.
* default: false
*/
export class AdminPostBatchesReq {
@IsString()
type: string
@IsObject()
context: BatchJob["context"]
@IsBoolean()
@IsOptional()
dry_run = false
}
@@ -1,78 +0,0 @@
/**
* @oas [get] /admin/batch-jobs/{id}
* operationId: "GetBatchJobsBatchJob"
* summary: "Get a Batch Job"
* description: "Retrieve the details of a batch job."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Batch Job
* x-codegen:
* method: retrieve
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.batchJobs.retrieve(batchJobId)
* .then(({ batch_job }) => {
* console.log(batch_job.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminBatchJob } from "medusa-react"
*
* type Props = {
* batchJobId: string
* }
*
* const BatchJob = ({ batchJobId }: Props) => {
* const { batch_job, isLoading } = useAdminBatchJob(batchJobId)
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {batch_job && <span>{batch_job.created_by}</span>}
* </div>
* )
* }
*
* export default BatchJob
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/batch-jobs/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Batch Jobs
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBatchJobRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const batch_job = req.batch_job
res.status(200).json({ batch_job: batch_job })
}
@@ -1,110 +0,0 @@
import { Router } from "express"
import { BatchJob } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares, {
canAccessBatchJob,
getRequestedBatchJob,
transformQuery,
} from "../../../middlewares"
import { AdminGetBatchParams } from "./list-batch-jobs"
export default (app) => {
const route = Router()
app.use("/batch-jobs", route)
route.get(
"/",
transformQuery(AdminGetBatchParams, {
defaultFields: defaultAdminBatchFields,
isList: true,
}),
middlewares.wrap(require("./list-batch-jobs").default)
)
route.post("/", middlewares.wrap(require("./create-batch-job").default))
const batchJobRouter = Router({ mergeParams: true })
route.use("/:id", getRequestedBatchJob, canAccessBatchJob, batchJobRouter)
batchJobRouter.get("/", middlewares.wrap(require("./get-batch-job").default))
batchJobRouter.post(
"/confirm",
middlewares.wrap(require("./confirm-batch-job").default)
)
batchJobRouter.post(
"/cancel",
middlewares.wrap(require("./cancel-batch-job").default)
)
return app
}
/**
* @schema AdminBatchJobRes
* type: object
* description: "The batch job's details."
* required:
* - batch_job
* properties:
* batch_job:
* description: Batch job details.
* $ref: "#/components/schemas/BatchJob"
*/
export type AdminBatchJobRes = {
batch_job: BatchJob
}
/**
* @schema AdminBatchJobListRes
* type: object
* required:
* - batch_jobs
* - count
* - offset
* - limit
* properties:
* batch_jobs:
* type: array
* description: An array of batch job details.
* items:
* $ref: "#/components/schemas/BatchJob"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of batch jobs skipped when retrieving the batch jobs.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminBatchJobListRes = PaginatedResponse & {
batch_jobs: BatchJob[]
}
export const defaultAdminBatchFields = [
"id",
"type",
"context",
"result",
"created_by",
"created_at",
"updated_at",
"deleted_at",
"confirmed_at",
"pre_processed_at",
"confirmed_at",
"processing_at",
"completed_at",
"canceled_at",
"failed_at",
]
export * from "./cancel-batch-job"
export * from "./confirm-batch-job"
export * from "./create-batch-job"
export * from "./get-batch-job"
export * from "./list-batch-jobs"
@@ -1,435 +0,0 @@
import { IsArray, IsNumber, IsOptional, IsString } from "class-validator"
import { Transform, Type } from "class-transformer"
import BatchJobService from "../../../../services/batch-job"
import { DateComparisonOperator } from "../../../../types/common"
import { IsType } from "../../../../utils/validators/is-type"
import { Request } from "express"
import { pickBy } from "lodash"
import { isDefined } from "medusa-core-utils"
/**
* @oas [get] /admin/batch-jobs
* operationId: "GetBatchJobs"
* summary: "List Batch Jobs"
* description: "Retrieve a list of Batch Jobs. The batch jobs can be filtered by fields such as `type` or `confirmed_at`. The batch jobs can also be sorted or paginated."
* x-authenticated: true
* parameters:
* - (query) limit=10 {integer} Limit the number of batch jobs returned.
* - (query) offset=0 {integer} The number of batch jobs to skip when retrieving the batch jobs.
* - in: query
* name: id
* style: form
* explode: false
* description: Filter by the batch ID
* schema:
* oneOf:
* - type: string
* description: batch job ID
* - type: array
* description: multiple batch job IDs
* items:
* type: string
* - in: query
* name: type
* style: form
* explode: false
* description: Filter by the batch type
* schema:
* type: array
* items:
* type: string
* - in: query
* name: confirmed_at
* style: form
* explode: false
* description: Filter by a confirmation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: pre_processed_at
* style: form
* explode: false
* description: Filter by a pre-processing date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: completed_at
* style: form
* explode: false
* description: Filter by a completion date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: failed_at
* style: form
* explode: false
* description: Filter by a failure date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: canceled_at
* style: form
* explode: false
* description: Filter by a cancelation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - (query) order {string} A batch-job field to sort-order the retrieved batch jobs by.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned batch jobs.
* - (query) fields {string} Comma-separated fields that should be included in the returned batch jobs.
* - in: query
* name: created_at
* style: form
* explode: false
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* style: form
* explode: false
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* x-codegen:
* method: list
* queryParams: AdminGetBatchParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.batchJobs.list()
* .then(({ batch_jobs, limit, offset, count }) => {
* console.log(batch_jobs.length)
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminBatchJobs } from "medusa-react"
*
* const BatchJobs = () => {
* const {
* batch_jobs,
* limit,
* offset,
* count,
* isLoading
* } = useAdminBatchJobs()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {batch_jobs?.length && (
* <ul>
* {batch_jobs.map((batchJob) => (
* <li key={batchJob.id}>
* {batchJob.id}
* </li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default BatchJobs
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/batch-jobs' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Batch Jobs
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminBatchJobListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res) => {
const batchService: BatchJobService = req.scope.resolve("batchJobService")
const created_by = req.user?.id || req.user?.userId
const [jobs, count] = await batchService.listAndCount(
pickBy({ created_by, ...(req.filterableFields ?? {}) }, (val) =>
isDefined(val)
),
req.listConfig
)
const { limit, offset } = req.validatedQuery
res.status(200).json({
batch_jobs: jobs,
count,
offset,
limit,
})
}
/**
* Request parameters used to configure and paginate retrieved batch jobs.
*/
export class AdminGetBatchPaginationParams {
/**
* {@inheritDoc FindPaginationParams.limit}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
limit = 10
/**
* {@inheritDoc FindPaginationParams.offset}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
offset = 0
/**
* {@inheritDoc FindParams.expand}
*/
@IsString()
@IsOptional()
expand?: string
/**
* {@inheritDoc FindParams.fields}
*/
@IsString()
@IsOptional()
fields?: string
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
*/
@IsString()
@IsOptional()
order?: string
}
/**
* Parameters used to filter and configure pagination of the retrieved batch jobs.
*/
export class AdminGetBatchParams extends AdminGetBatchPaginationParams {
/**
* IDs to filter batch jobs by.
*/
@IsOptional()
@IsArray()
@IsType([String, [String]])
id?: string | string[]
/**
* Types to filter batch jobs by.
*/
@IsArray()
@IsOptional()
type?: string[]
/**
* Date filters to apply on the batch jobs' `confirmed_at` date.
*/
@IsOptional()
@Transform(({ value }) => {
return value === "null" ? null : value
})
@Type(() => DateComparisonOperator)
confirmed_at?: DateComparisonOperator | null
/**
* Date filters to apply on the batch jobs' `pre_processed_at` date.
*/
@IsOptional()
@Transform(({ value }) => {
return value === "null" ? null : value
})
@Type(() => DateComparisonOperator)
pre_processed_at?: DateComparisonOperator | null
/**
* Date filters to apply on the batch jobs' `completed_at` date.
*/
@IsOptional()
@Transform(({ value }) => {
return value === "null" ? null : value
})
@Type(() => DateComparisonOperator)
completed_at?: DateComparisonOperator | null
/**
* Date filters to apply on the batch jobs' `failed_at` date.
*/
@IsOptional()
@Transform(({ value }) => {
return value === "null" ? null : value
})
@Type(() => DateComparisonOperator)
failed_at?: DateComparisonOperator | null
/**
* Date filters to apply on the batch jobs' `canceled_at` date.
*/
@IsOptional()
@Transform(({ value }) => {
return value === "null" ? null : value
})
@Type(() => DateComparisonOperator)
canceled_at?: DateComparisonOperator | null
/**
* Date filters to apply on the batch jobs' `created_at` date.
*/
@IsType([DateComparisonOperator])
@IsOptional()
created_at?: DateComparisonOperator
/**
* Date filters to apply on the batch jobs' `updated_at` date.
*/
@IsOptional()
@Type(() => DateComparisonOperator)
updated_at?: DateComparisonOperator
}
@@ -1,94 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("POST /admin/collections/:id/products/batch", () => {
describe("successfully adds products to collection", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/collections/${IdMap.getId("col")}/products/batch`,
{
payload: {
product_ids: ["prod_1", "prod_2"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns updated collection with new products", () => {
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
})
it("product collection service update", () => {
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledTimes(1)
expect(ProductCollectionServiceMock.addProducts).toHaveBeenCalledWith(
IdMap.getId("col"),
["prod_1", "prod_2"]
)
})
})
describe("error on non-existing collection", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/collections/null/products/batch`,
{
payload: {
product_ids: ["prod_1", "prod_2"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("throws error", () => {
expect(subject.body.message).toBe(
"Product collection with id: null was not found"
)
})
})
describe("error invalid request", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/collections/${IdMap.getId("col")}/products/batch`,
{
payload: {
product_ids: [],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
})
})
@@ -1,67 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("POST /admin/collections", () => {
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/collections", {
payload: {
title: "Suits",
handle: "suits",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns created product collection", () => {
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
})
it("calls production collection service create", () => {
expect(ProductCollectionServiceMock.create).toHaveBeenCalledTimes(1)
expect(ProductCollectionServiceMock.create).toHaveBeenCalledWith({
title: "Suits",
handle: "suits",
})
})
})
describe("invalid data returns error details", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/collections", {
payload: {
handle: "no-title-collection",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error details", () => {
expect(subject.body.type).toEqual("invalid_data")
expect(subject.body.message).toEqual(
"title should not be empty, title must be a string"
)
})
})
})
@@ -1,42 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("DELETE /admin/collections/:id", () => {
describe("successful removes collection", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/collections/${IdMap.getId("collection")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls product collection service delete", () => {
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledTimes(1)
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledWith(
IdMap.getId("collection")
)
})
it("returns delete result", () => {
expect(subject.body).toEqual({
id: IdMap.getId("collection"),
object: "product-collection",
deleted: true,
})
})
})
})
@@ -1,39 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { defaultAdminCollectionsRelations } from ".."
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("GET /admin/categories/:id", () => {
describe("get collection by id successfully", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/collections/${IdMap.getId("col")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls retrieve from product collection service", () => {
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("col"),
{relations: defaultAdminCollectionsRelations}
)
})
it("returns variant decorated", () => {
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
})
})
})
@@ -1,28 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("GET /admin/collections", () => {
describe("successful retrieval", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("GET", `/admin/collections`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls product collection service list", () => {
expect(ProductCollectionServiceMock.listAndCount).toHaveBeenCalledTimes(1)
})
})
})
@@ -1,64 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("DELETE /admin/collections/:id/products/batch", () => {
describe("successfully removes products from collection", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/collections/${IdMap.getId("col")}/products/batch`,
{
payload: {
product_ids: ["prod_1", "prod_2"],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("product collection service remove products", () => {
expect(ProductCollectionServiceMock.removeProducts).toHaveBeenCalledTimes(
1
)
expect(
ProductCollectionServiceMock.removeProducts
).toHaveBeenCalledWith(IdMap.getId("col"), ["prod_1", "prod_2"])
})
})
describe("error on invalid request", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/collections/${IdMap.getId("col")}/products/batch`,
{
payload: {
product_ids: [],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
})
})
@@ -1,44 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
describe("POST /admin/collections/:id", () => {
describe("successful update", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/collections/${IdMap.getId("col")}`,
{
payload: {
title: "Suits and vests",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns updated product collection", () => {
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
})
it("product collection service update", () => {
expect(ProductCollectionServiceMock.update).toHaveBeenCalledTimes(1)
expect(ProductCollectionServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("col"),
{
title: "Suits and vests",
}
)
})
})
})
@@ -1,146 +0,0 @@
import { ArrayNotEmpty, IsString } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import ProductCollectionService from "../../../../services/product-collection"
import { defaultAdminCollectionsRelations } from "./index"
/**
* @oas [post] /admin/collections/{id}/products/batch
* operationId: "PostProductsToCollection"
* summary: "Add Products to Collection"
* description: "Add products to a product collection."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the product collection.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostProductsToCollectionReq"
* x-codegen:
* method: addProducts
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.addProducts(collectionId, {
* product_ids: [
* productId1,
* productId2
* ]
* })
* .then(({ collection }) => {
* console.log(collection.products)
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminAddProductsToCollection } from "medusa-react"
*
* type Props = {
* collectionId: string
* }
*
* const Collection = ({ collectionId }: Props) => {
* const addProducts = useAdminAddProductsToCollection(collectionId)
* // ...
*
* const handleAddProducts = (productIds: string[]) => {
* addProducts.mutate({
* product_ids: productIds
* }, {
* onSuccess: ({ collection }) => {
* console.log(collection.products)
* }
* })
* }
*
* // ...
* }
*
* export default Collection
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/collections/{id}/products/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "product_ids": [
* "prod_01G1G5V2MBA328390B5AXJ610F"
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const { validatedBody } = req as {
validatedBody: AdminPostProductsToCollectionReq
}
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const updated = await manager.transaction(async (transactionManager) => {
return await productCollectionService
.withTransaction(transactionManager)
.addProducts(id, validatedBody.product_ids)
})
const collection = await productCollectionService.retrieve(updated.id, {
relations: defaultAdminCollectionsRelations,
})
res.status(200).json({ collection })
}
/**
* @schema AdminPostProductsToCollectionReq
* type: object
* description: "The details of the products to add to the collection."
* required:
* - product_ids
* properties:
* product_ids:
* description: "An array of Product IDs to add to the Product Collection."
* type: array
* items:
* description: "The ID of a Product to add to the Product Collection."
* type: string
*/
export class AdminPostProductsToCollectionReq {
@ArrayNotEmpty()
@IsString({ each: true })
product_ids: string[]
}
@@ -1,145 +0,0 @@
import { IsNotEmpty, IsObject, IsOptional, IsString } from "class-validator"
import ProductCollectionService from "../../../../services/product-collection"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { defaultAdminCollectionsRelations } from "."
/**
* @oas [post] /admin/collections
* operationId: "PostCollections"
* summary: "Create a Collection"
* description: "Create a Product Collection."
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCollectionsReq"
* x-codegen:
* method: create
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.create({
* title: "New Collection"
* })
* .then(({ collection }) => {
* console.log(collection.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateCollection } from "medusa-react"
*
* const CreateCollection = () => {
* const createCollection = useAdminCreateCollection()
* // ...
*
* const handleCreate = (title: string) => {
* createCollection.mutate({
* title
* }, {
* onSuccess: ({ collection }) => {
* console.log(collection.id)
* }
* })
* }
*
* // ...
* }
*
* export default CreateCollection
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/collections' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "title": "New Collection"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { validatedBody } = req as { validatedBody: AdminPostCollectionsReq }
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const created = await manager.transaction(async (transactionManager) => {
return await productCollectionService
.withTransaction(transactionManager)
.create(validatedBody)
})
const collection = await productCollectionService.retrieve(created.id, {
relations: defaultAdminCollectionsRelations,
})
res.status(200).json({ collection })
}
/**
* @schema AdminPostCollectionsReq
* type: object
* description: The product collection's details.
* required:
* - title
* properties:
* title:
* type: string
* description: The title of the collection.
* handle:
* type: string
* description: An optional handle to be used in slugs. If none is provided, the kebab-case version of the title will be used.
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCollectionsReq {
@IsString()
@IsNotEmpty()
title: string
@IsString()
@IsOptional()
handle?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,103 +0,0 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import ProductCollectionService from "../../../../services/product-collection"
/**
* @oas [delete] /admin/collections/{id}
* operationId: "DeleteCollectionsCollection"
* summary: "Delete a Collection"
* description: "Delete a Product Collection. This does not delete associated products."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Collection.
* x-codegen:
* method: delete
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.delete(collectionId)
* .then(({ id, object, deleted }) => {
* console.log(id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteCollection } from "medusa-react"
*
* type Props = {
* collectionId: string
* }
*
* const Collection = ({ collectionId }: Props) => {
* const deleteCollection = useAdminDeleteCollection(collectionId)
* // ...
*
* const handleDelete = (title: string) => {
* deleteCollection.mutate(void 0, {
* onSuccess: ({ id, object, deleted }) => {
* console.log(id)
* }
* })
* }
*
* // ...
* }
*
* export default Collection
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/collections/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsDeleteRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await productCollectionService
.withTransaction(transactionManager)
.delete(id)
})
res.json({
id,
object: "product-collection",
deleted: true,
})
}
@@ -1,91 +0,0 @@
import { Request, Response } from "express"
import ProductCollectionService from "../../../../services/product-collection"
import { defaultAdminCollectionsRelations } from "."
/**
* @oas [get] /admin/collections/{id}
* operationId: "GetCollectionsCollection"
* summary: "Get a Collection"
* description: "Retrieve a Product Collection by its ID. The products associated with it are expanded and returned as well."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Product Collection
* x-codegen:
* method: retrieve
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.retrieve(collectionId)
* .then(({ collection }) => {
* console.log(collection.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCollection } from "medusa-react"
*
* type Props = {
* collectionId: string
* }
*
* const Collection = ({ collectionId }: Props) => {
* const { collection, isLoading } = useAdminCollection(collectionId)
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {collection && <span>{collection.title}</span>}
* </div>
* )
* }
*
* export default Collection
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/collections/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const collection = await productCollectionService.retrieve(id, {
relations: defaultAdminCollectionsRelations,
})
res.status(200).json({ collection })
}
@@ -1,176 +0,0 @@
import { Router } from "express"
import "reflect-metadata"
import { ProductCollection } from "../../../.."
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import { AdminGetCollectionsParams } from "./list-collections"
import { AdminPostCollectionsReq } from "./create-collection"
import { AdminPostCollectionsCollectionReq } from "./update-collection"
import { AdminPostProductsToCollectionReq } from "./add-products"
import { AdminDeleteProductsFromCollectionReq } from "./remove-products"
export default (app) => {
const route = Router()
app.use("/collections", route)
route.post(
"/",
transformBody(AdminPostCollectionsReq),
middlewares.wrap(require("./create-collection").default)
)
route.get(
"/",
transformQuery(AdminGetCollectionsParams, {
defaultRelations: defaultAdminCollectionsRelations,
defaultFields: defaultAdminCollectionsFields,
isList: true,
}),
middlewares.wrap(require("./list-collections").default)
)
const collectionRouter = Router({ mergeParams: true })
route.use("/:id", collectionRouter)
collectionRouter.post(
"/",
transformBody(AdminPostCollectionsCollectionReq),
middlewares.wrap(require("./update-collection").default)
)
collectionRouter.get(
"/",
middlewares.wrap(require("./get-collection").default)
)
collectionRouter.delete(
"/",
middlewares.wrap(require("./delete-collection").default)
)
collectionRouter.post(
"/products/batch",
transformBody(AdminPostProductsToCollectionReq),
middlewares.wrap(require("./add-products").default)
)
collectionRouter.delete(
"/products/batch",
transformBody(AdminDeleteProductsFromCollectionReq),
middlewares.wrap(require("./remove-products").default)
)
return app
}
export const defaultAdminCollectionsFields = [
"id",
"title",
"handle",
"created_at",
"updated_at",
]
export const defaultAdminCollectionsRelations = ["products.profiles"]
/**
* @schema AdminCollectionsListRes
* type: object
* required:
* - collections
* - count
* - offset
* - limit
* properties:
* collections:
* type: array
* description: an array of collection details
* items:
* $ref: "#/components/schemas/ProductCollection"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of product collections skipped when retrieving the product collections.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminCollectionsListRes = PaginatedResponse & {
collections: ProductCollection[]
}
/**
* @schema AdminCollectionsDeleteRes
* type: object
* required:
* - id
* - object
* - deleted
* properties:
* id:
* type: string
* description: The ID of the deleted Collection
* object:
* type: string
* description: The type of the object that was deleted.
* default: product-collection
* deleted:
* type: boolean
* description: Whether the collection was deleted successfully or not.
* default: true
*/
export type AdminCollectionsDeleteRes = DeleteResponse
/**
* @schema AdminDeleteProductsFromCollectionRes
* type: object
* description: "Deletion operation details"
* required:
* - id
* - object
* - removed_products
* properties:
* id:
* type: string
* description: "The ID of the collection"
* object:
* type: string
* description: "The type of object the removal was executed on"
* default: product-collection
* removed_products:
* description: "The IDs of the products removed from the collection"
* type: array
* items:
* description: "The ID of the Product removed from the Product Collection."
* type: string
*/
export type AdminDeleteProductsFromCollectionRes = {
id: string
object: string
removed_products: string[]
}
/**
* @schema AdminCollectionsRes
* type: object
* description: The collection's details.
* x-expanded-relations:
* field: collection
* relations:
* - products
* required:
* - collection
* properties:
* collection:
* description: "Product Collection details."
* $ref: "#/components/schemas/ProductCollection"
*/
export type AdminCollectionsRes = {
collection: ProductCollection
}
export * from "./add-products"
export * from "./create-collection"
export * from "./delete-collection"
export * from "./get-collection"
export * from "./list-collections"
export * from "./remove-products"
export * from "./update-collection"
@@ -1,265 +0,0 @@
import { IsNumber, IsOptional, IsString, ValidateNested } from "class-validator"
import { Request, Response } from "express"
import { Type } from "class-transformer"
import ProductCollectionService from "../../../../services/product-collection"
import { DateComparisonOperator } from "../../../../types/common"
/**
* @oas [get] /admin/collections
* operationId: "GetCollections"
* summary: "List Collections"
* description: "Retrieve a list of Product Collection. The product collections can be filtered by fields such as `handle` or `title`. The collections can also be sorted or paginated."
* x-authenticated: true
* parameters:
* - (query) limit=10 {integer} The number of collections to return.
* - (query) offset=0 {integer} The number of collections to skip when retrieving the collections.
* - (query) title {string} Filter collections by their title.
* - (query) handle {string} Filter collections by their handle.
* - (query) q {string} a term to search collections by their title or handle.
* - (query) order {string} A field to sort-order the retrieved collections by.
* - (query) discount_condition_id {string} Filter collections by a discount condition ID associated with them.
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: deleted_at
* description: Filter by a deletion date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* x-codegen:
* method: list
* queryParams: AdminGetCollectionsParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.list()
* .then(({ collections, limit, offset, count }) => {
* console.log(collections.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCollections } from "medusa-react"
*
* const Collections = () => {
* const { collections, isLoading } = useAdminCollections()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {collections && !collections.length && <span>
* No Product Collections
* </span>}
* {collections && collections.length > 0 && (
* <ul>
* {collections.map((collection) => (
* <li key={collection.id}>{collection.title}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default Collections
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/collections' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const { filterableFields, listConfig } = req
const { skip, take } = listConfig
const [collections, count] = await productCollectionService.listAndCount(
filterableFields,
listConfig
)
res.status(200).json({
collections,
count,
offset: skip,
limit: take,
})
}
/**
* Parameters used to configure the pagination of the retrieved product collections.
*/
export class AdminGetCollectionsPaginationParams {
/**
* {@inheritDoc FindPaginationParams.limit}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
limit = 10
/**
* {@inheritDoc FindPaginationParams.offset}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
offset = 0
}
/**
* Parameters used to filter and configure the pagination of the retrieved product collections.
*/
// eslint-disable-next-line max-len
export class AdminGetCollectionsParams extends AdminGetCollectionsPaginationParams {
/**
* Title to filter product collections by.
*/
@IsOptional()
@IsString()
title?: string
/**
* Handle to filter product collections by.
*/
@IsOptional()
@IsString()
handle?: string
/**
* Date filters to apply on the product collections' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
created_at?: DateComparisonOperator
/**
* Date filters to apply on the product collections' `updated_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
updated_at?: DateComparisonOperator
/**
* Date filters to apply on the product collections' `deleted_at` date.
*/
@ValidateNested()
@IsOptional()
@Type(() => DateComparisonOperator)
deleted_at?: DateComparisonOperator
/**
* Term to search product collections by their title and handle.
*/
@IsString()
@IsOptional()
q?: string
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
*/
@IsString()
@IsOptional()
order?: string
/**
* Filter product collections by their associated discount condition's ID.
*/
@IsString()
@IsOptional()
discount_condition_id?: string
}
@@ -1,145 +0,0 @@
import { ArrayNotEmpty, IsString } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import ProductCollectionService from "../../../../services/product-collection"
/**
* @oas [delete] /admin/collections/{id}/products/batch
* operationId: "DeleteProductsFromCollection"
* summary: "Remove Products from Collection"
* description: "Remove a list of products from a collection. This would not delete the product, only the association between the product and the collection."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Product Collection.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDeleteProductsFromCollectionReq"
* x-codegen:
* method: removeProducts
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.removeProducts(collectionId, {
* product_ids: [
* productId1,
* productId2
* ]
* })
* .then(({ id, object, removed_products }) => {
* console.log(removed_products)
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminRemoveProductsFromCollection } from "medusa-react"
*
* type Props = {
* collectionId: string
* }
*
* const Collection = ({ collectionId }: Props) => {
* const removeProducts = useAdminRemoveProductsFromCollection(collectionId)
* // ...
*
* const handleRemoveProducts = (productIds: string[]) => {
* removeProducts.mutate({
* product_ids: productIds
* }, {
* onSuccess: ({ id, object, removed_products }) => {
* console.log(removed_products)
* }
* })
* }
*
* // ...
* }
*
* export default Collection
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/collections/{id}/products/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "product_ids": [
* "prod_01G1G5V2MBA328390B5AXJ610F"
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDeleteProductsFromCollectionRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const { validatedBody } = req as {
validatedBody: AdminDeleteProductsFromCollectionReq
}
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await productCollectionService
.withTransaction(transactionManager)
.removeProducts(id, validatedBody.product_ids)
})
res.json({
id,
object: "product-collection",
removed_products: validatedBody.product_ids,
})
}
/**
* @schema AdminDeleteProductsFromCollectionReq
* type: object
* description: "The details of the products to remove from the collection."
* required:
* - product_ids
* properties:
* product_ids:
* description: "An array of Product IDs to remove from the Product Collection."
* type: array
* items:
* description: "The ID of a Product to add to the Product Collection."
* type: string
*/
export class AdminDeleteProductsFromCollectionReq {
@ArrayNotEmpty()
@IsString({ each: true })
product_ids: string[]
}
@@ -1,152 +0,0 @@
import { IsObject, IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import ProductCollectionService from "../../../../services/product-collection"
import { defaultAdminCollectionsRelations } from "."
/**
* @oas [post] /admin/collections/{id}
* operationId: "PostCollectionsCollection"
* summary: "Update a Collection"
* description: "Update a Product Collection's details."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Collection.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCollectionsCollectionReq"
* x-codegen:
* method: update
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.collections.update(collectionId, {
* title: "New Collection"
* })
* .then(({ collection }) => {
* console.log(collection.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateCollection } from "medusa-react"
*
* type Props = {
* collectionId: string
* }
*
* const Collection = ({ collectionId }: Props) => {
* const updateCollection = useAdminUpdateCollection(collectionId)
* // ...
*
* const handleUpdate = (title: string) => {
* updateCollection.mutate({
* title
* }, {
* onSuccess: ({ collection }) => {
* console.log(collection.id)
* }
* })
* }
*
* // ...
* }
*
* export default Collection
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/collections/{id}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "title": "New Collection"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Product Collections
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCollectionsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const { validatedBody } = req as {
validatedBody: AdminPostCollectionsCollectionReq
}
const productCollectionService: ProductCollectionService = req.scope.resolve(
"productCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const updated = await manager.transaction(async (transactionManager) => {
return await productCollectionService
.withTransaction(transactionManager)
.update(id, validatedBody)
})
const collection = await productCollectionService.retrieve(updated.id, {
relations: defaultAdminCollectionsRelations,
})
res.status(200).json({ collection })
}
/**
* @schema AdminPostCollectionsCollectionReq
* type: object
* description: The product collection's details to update.
* properties:
* title:
* type: string
* description: The title of the collection.
* handle:
* type: string
* description: An optional handle to be used in slugs. If none is provided, the kebab-case version of the title will be used.
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCollectionsCollectionReq {
@IsString()
@IsOptional()
title?: string
@IsString()
@IsOptional()
handle?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,52 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { currency, CurrencyServiceMock } from "../../../../../services/__mocks__/currency";
import TaxInclusivePricingFeatureFlag from "../../../../../loaders/feature-flags/tax-inclusive-pricing";
describe("GET /admin/currencies/", () => {
describe("successfully list the currency", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/currencies`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
flags: [TaxInclusivePricingFeatureFlag],
}
)
})
afterAll(() => {
jest.clearAllMocks()
})
it("calls the listAndCount method from the currency service", () => {
expect(CurrencyServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(CurrencyServiceMock.listAndCount).toHaveBeenCalledWith(
{},
{
order: {},
select: undefined,
relations: [],
skip: 0,
take: 20
}
)
})
it("returns the expected currencies", () => {
expect(subject.body).toEqual({
currencies: [currency],
offset: 0,
limit: 20,
count: 1,
})
})
})
})
@@ -1,48 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { currency, CurrencyServiceMock } from "../../../../../services/__mocks__/currency";
import TaxInclusivePricingFeatureFlag from "../../../../../loaders/feature-flags/tax-inclusive-pricing";
describe("POST /admin/currencies/:code", () => {
let subject
const code = IdMap.getId("currency-1")
beforeAll(async () => {
subject = await request(
"POST",
`/admin/currencies/${code}`,
{
payload: {
includes_tax: true,
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
flags: [TaxInclusivePricingFeatureFlag],
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns updated currency", () => {
expect(subject.body.currency).toEqual({
...currency,
includes_tax: true,
})
})
it("calls service update", () => {
expect(CurrencyServiceMock.update).toHaveBeenCalledTimes(1)
expect(CurrencyServiceMock.update).toHaveBeenCalledWith(
code,
{
includes_tax: true,
}
)
})
})
@@ -1,80 +0,0 @@
import { Router } from "express"
import { Currency } from "../../../.."
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { PaginatedResponse } from "../../../../types/common"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { AdminGetCurrenciesParams } from "./list-currencies"
import { AdminPostCurrenciesCurrencyReq } from "./update-currency"
export default (app) => {
const route = Router()
app.use("/currencies", route)
route.get(
"/",
transformQuery(AdminGetCurrenciesParams, {
isList: true,
}),
middlewares.wrap(require("./list-currencies").default)
)
route.post(
"/:code",
transformBody(AdminPostCurrenciesCurrencyReq),
isFeatureFlagEnabled(TaxInclusivePricingFeatureFlag.key),
middlewares.wrap(require("./update-currency").default)
)
return app
}
/**
* @schema AdminCurrenciesListRes
* type: object
* description: List of currencies with pagination fields.
* required:
* - currencies
* - count
* - offset
* - limit
* properties:
* currencies:
* type: array
* description: An array of currency details.
* items:
* $ref: "#/components/schemas/Currency"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of currencies skipped when retrieving the currencies.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminCurrenciesListRes = PaginatedResponse & {
currencies: Currency[]
}
/**
* @schema AdminCurrenciesRes
* type: object
* description: A currency's details.
* required:
* - currency
* properties:
* currency:
* description: Currency details.
* $ref: "#/components/schemas/Currency"
*/
export type AdminCurrenciesRes = {
currency: Currency
}
export * from "./list-currencies"
export * from "./update-currency"
@@ -1,157 +0,0 @@
import { IsBoolean, IsOptional, IsString } from "class-validator"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { Currency } from "../../../../models"
import { CurrencyService } from "../../../../services"
import { FindPaginationParams } from "../../../../types/common"
import { ExtendedRequest } from "../../../../types/global"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
/**
* @oas [get] /admin/currencies
* operationId: "GetCurrencies"
* summary: "List Currency"
* description: "Retrieve a list of currencies. The currencies can be filtered by fields such as `code`. The currencies can also be sorted or paginated."
* x-authenticated: true
* parameters:
* - (query) code {string} filter by currency code.
* - in: query
* name: includes_tax
* description: filter currencies by whether they include taxes or not.
* schema:
* type: boolean
* x-featureFlag: "tax_inclusive_pricing"
* - (query) order {string} A field to sort order the retrieved currencies by.
* - (query) q {string} Term used to search currencies' name and code.
* - (query) offset=0 {number} The number of currencies to skip when retrieving the currencies.
* - (query) limit=20 {number} The number of currencies to return.
* x-codegen:
* method: list
* queryParams: AdminGetCurrenciesParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.currencies.list()
* .then(({ currencies, count, offset, limit }) => {
* console.log(currencies.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCurrencies } from "medusa-react"
*
* const Currencies = () => {
* const { currencies, isLoading } = useAdminCurrencies()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {currencies && !currencies.length && (
* <span>No Currencies</span>
* )}
* {currencies && currencies.length > 0 && (
* <ul>
* {currencies.map((currency) => (
* <li key={currency.code}>{currency.name}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default Currencies
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/currencies' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Currencies
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCurrenciesListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: ExtendedRequest<Currency>, res) => {
const currencyService: CurrencyService = req.scope.resolve("currencyService")
const { skip, take } = req.listConfig
req.listConfig.select = undefined
if (req.listConfig.order && req.listConfig.order["created_at"]) {
delete req.listConfig.order["created_at"]
}
const [currencies, count] = await currencyService.listAndCount(
req.filterableFields,
req.listConfig
)
res.json({
currencies,
count,
offset: skip,
limit: take,
})
}
/**
* Parameters used to filter and configure the pagination of the retrieved currencies.
*/
export class AdminGetCurrenciesParams extends FindPaginationParams {
/**
* Code to filter currencies by.
*/
@IsString()
@IsOptional()
code?: string
/**
* Search parameter for currencies.
*/
@IsString()
@IsOptional()
q?: string
/**
* Filter currencies by whether they include tax.
*
* @featureFlag tax_inclusive_pricing
*/
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
IsBoolean(),
IsOptional(),
])
includes_tax?: boolean
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
* By default, the returned currencies will be sorted by their `created_at` field.
*/
@IsString()
@IsOptional()
order?: string
}
@@ -1,131 +0,0 @@
import { IsBoolean, IsOptional } from "class-validator"
import { Currency } from "../../../../models"
import { ExtendedRequest } from "../../../../types/global"
import { CurrencyService } from "../../../../services"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import TaxInclusivePricingFeatureFlag from "../../../../loaders/feature-flags/tax-inclusive-pricing"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/currencies/{code}
* operationId: "PostCurrenciesCurrency"
* summary: "Update a Currency"
* description: "Update a Currency's details."
* x-authenticated: true
* parameters:
* - (path) code=* {string} The code of the Currency.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCurrenciesCurrencyReq"
* x-codegen:
* method: update
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.currencies.update(code, {
* includes_tax: true
* })
* .then(({ currency }) => {
* console.log(currency.code);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateCurrency } from "medusa-react"
*
* type Props = {
* currencyCode: string
* }
*
* const Currency = ({ currencyCode }: Props) => {
* const updateCurrency = useAdminUpdateCurrency(currencyCode)
* // ...
*
* const handleUpdate = (includes_tax: boolean) => {
* updateCurrency.mutate({
* includes_tax,
* }, {
* onSuccess: ({ currency }) => {
* console.log(currency)
* }
* })
* }
*
* // ...
* }
*
* export default Currency
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/currencies/{code}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "includes_tax": true
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Currencies
* responses:
* "200":
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCurrenciesRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: ExtendedRequest<Currency>, res) => {
const code = req.params.code as string
const data = req.validatedBody as AdminPostCurrenciesCurrencyReq
const currencyService: CurrencyService = req.scope.resolve("currencyService")
const manager: EntityManager = req.scope.resolve("manager")
const currency = await manager.transaction(async (transactionManager) => {
return await currencyService
.withTransaction(transactionManager)
.update(code, data)
})
res.json({ currency })
}
/**
* @schema AdminPostCurrenciesCurrencyReq
* type: object
* description: "The details to update in the currency"
* properties:
* includes_tax:
* type: boolean
* x-featureFlag: "tax_inclusive_pricing"
* description: "Tax included in prices of currency."
*/
export class AdminPostCurrenciesCurrencyReq {
@FeatureFlagDecorators(TaxInclusivePricingFeatureFlag.key, [
IsOptional(),
IsBoolean(),
])
includes_tax?: boolean
}
@@ -1,65 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CustomerGroupServiceMock } from "../../../../../services/__mocks__/customer-group"
describe("POST /customer-groups", () => {
describe("successfully calls create customer group", () => {
let subject
beforeAll(async () => {
subject = await request("POST", `/admin/customer-groups`, {
payload: {
name: "test group",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls CustomerGroupService create", () => {
expect(CustomerGroupServiceMock.create).toHaveBeenCalledTimes(1)
expect(CustomerGroupServiceMock.create).toHaveBeenCalledWith({
name: "test group",
})
})
})
describe("fails if no name is provided for the group ", () => {
let subject
beforeAll(async () => {
subject = await request("POST", `/admin/customer-groups`, {
payload: {},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
afterAll(() => {
jest.clearAllMocks()
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns descriptive error that name is missing", () => {
expect(subject.body.type).toEqual("invalid_data")
expect(subject.body.message).toEqual("name must be a string")
})
})
})
@@ -1,33 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { CustomerGroupServiceMock } from "../../../../../services/__mocks__/customer-group"
describe("GET /customer-groups", () => {
let subject
const id = "123"
beforeAll(async () => {
subject = await request(
"GET",
`/admin/customer-groups/${id}?expand=customers`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls CustomerGroupService get", () => {
expect(CustomerGroupServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CustomerGroupServiceMock.retrieve).toHaveBeenCalledWith(id, {
relations: ["customers"],
})
})
})
@@ -1,163 +0,0 @@
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { CustomerGroupsBatchCustomer } from "../../../../types/customer-groups"
import { EntityManager } from "typeorm"
import { Type } from "class-transformer"
import { ValidateNested } from "class-validator"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/customer-groups/{id}/customers/batch
* operationId: "PostCustomerGroupsGroupCustomersBatch"
* summary: "Add Customers to Group"
* description: "Add a list of customers to a customer group."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the customer group.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCustomerGroupsGroupCustomersBatchReq"
* x-codegen:
* method: addCustomers
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.addCustomers(customerGroupId, {
* customer_ids: [
* {
* id: customerId
* }
* ]
* })
* .then(({ customer_group }) => {
* console.log(customer_group.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminAddCustomersToCustomerGroup,
* } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const addCustomers = useAdminAddCustomersToCustomerGroup(
* customerGroupId
* )
* // ...
*
* const handleAddCustomers= (customerId: string) => {
* addCustomers.mutate({
* customer_ids: [
* {
* id: customerId,
* },
* ],
* })
* }
*
* // ...
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/customer-groups/{id}/customers/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "customer_ids": [
* {
* "id": "cus_01G2Q4BS9GAHDBMDEN4ZQZCJB2"
* }
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const validated = await validator(
AdminPostCustomerGroupsGroupCustomersBatchReq,
req.body
)
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const manager: EntityManager = req.scope.resolve("manager")
const customer_group = await manager.transaction(
async (transactionManager) => {
return await customerGroupService
.withTransaction(transactionManager)
.addCustomers(
id,
validated.customer_ids.map(({ id }) => id)
)
}
)
res.status(200).json({ customer_group })
}
/**
* @schema AdminPostCustomerGroupsGroupCustomersBatchReq
* type: object
* description: "The customers to add to the customer group."
* required:
* - customer_ids
* properties:
* customer_ids:
* description: "The ids of the customers to add"
* type: array
* items:
* type: object
* required:
* - id
* properties:
* id:
* description: ID of the customer
* type: string
*/
export class AdminPostCustomerGroupsGroupCustomersBatchReq {
@ValidateNested({ each: true })
@Type(() => CustomerGroupsBatchCustomer)
customer_ids: CustomerGroupsBatchCustomer[]
}
@@ -1,133 +0,0 @@
import { IsObject, IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/customer-groups
* operationId: "PostCustomerGroups"
* summary: "Create a Customer Group"
* description: "Create a Customer Group."
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCustomerGroupsReq"
* x-codegen:
* method: create
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.create({
* name: "VIP"
* })
* .then(({ customer_group }) => {
* console.log(customer_group.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateCustomerGroup } from "medusa-react"
*
* const CreateCustomerGroup = () => {
* const createCustomerGroup = useAdminCreateCustomerGroup()
* // ...
*
* const handleCreate = (name: string) => {
* createCustomerGroup.mutate({
* name,
* })
* }
*
* // ...
* }
*
* export default CreateCustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/customer-groups' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "name": "VIP"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const validated = await validator(AdminPostCustomerGroupsReq, req.body)
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const manager: EntityManager = req.scope.resolve("manager")
const customerGroup = await manager.transaction(
async (transactionManager) => {
return await customerGroupService
.withTransaction(transactionManager)
.create(validated)
}
)
res.status(200).json({ customer_group: customerGroup })
}
/**
* @schema AdminPostCustomerGroupsReq
* type: object
* description: "The details of the customer group to create."
* required:
* - name
* properties:
* name:
* type: string
* description: Name of the customer group
* metadata:
* type: object
* description: Metadata of the customer group.
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCustomerGroupsReq {
@IsString()
name: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,102 +0,0 @@
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { EntityManager } from "typeorm"
/**
* @oas [delete] /admin/customer-groups/{id}
* operationId: "DeleteCustomerGroupsCustomerGroup"
* summary: "Delete a Customer Group"
* description: "Delete a customer group. This doesn't delete the customers associated with the customer group."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Customer Group
* x-codegen:
* method: delete
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.delete(customerGroupId)
* .then(({ id, object, deleted }) => {
* console.log(id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteCustomerGroup } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const deleteCustomerGroup = useAdminDeleteCustomerGroup(
* customerGroupId
* )
* // ...
*
* const handleDeleteCustomerGroup = () => {
* deleteCustomerGroup.mutate()
* }
*
* // ...
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/customer-groups/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsDeleteRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await customerGroupService
.withTransaction(transactionManager)
.delete(id)
})
res.json({
id: id,
object: "customer_group",
deleted: true,
})
}
@@ -1,164 +0,0 @@
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { CustomerGroupsBatchCustomer } from "../../../../types/customer-groups"
import { EntityManager } from "typeorm"
import { Type } from "class-transformer"
import { ValidateNested } from "class-validator"
import { validator } from "../../../../utils/validator"
/**
* @oas [delete] /admin/customer-groups/{id}/customers/batch
* operationId: "DeleteCustomerGroupsGroupCustomerBatch"
* summary: "Remove Customers from Group"
* description: "Remove a list of customers from a customer group. This doesn't delete the customer, only the association between the customer and the customer group."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the customer group.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDeleteCustomerGroupsGroupCustomerBatchReq"
* x-codegen:
* method: removeCustomers
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.removeCustomers(customerGroupId, {
* customer_ids: [
* {
* id: customerId
* }
* ]
* })
* .then(({ customer_group }) => {
* console.log(customer_group.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminRemoveCustomersFromCustomerGroup,
* } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const removeCustomers =
* useAdminRemoveCustomersFromCustomerGroup(
* customerGroupId
* )
* // ...
*
* const handleRemoveCustomer = (customerId: string) => {
* removeCustomers.mutate({
* customer_ids: [
* {
* id: customerId,
* },
* ],
* })
* }
*
* // ...
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/customer-groups/{id}/customers/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "customer_ids": [
* {
* "id": "cus_01G2Q4BS9GAHDBMDEN4ZQZCJB2"
* }
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const validated = await validator(
AdminDeleteCustomerGroupsGroupCustomerBatchReq,
req.body
)
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const manager: EntityManager = req.scope.resolve("manager")
const customer_group = await manager.transaction(
async (transactionManager) => {
return await customerGroupService
.withTransaction(transactionManager)
.removeCustomer(
id,
validated.customer_ids.map(({ id }) => id)
)
}
)
res.status(200).json({ customer_group })
}
/**
* @schema AdminDeleteCustomerGroupsGroupCustomerBatchReq
* type: object
* description: "The customers to remove from the customer group."
* required:
* - customer_ids
* properties:
* customer_ids:
* description: "The ids of the customers to remove"
* type: array
* items:
* type: object
* required:
* - id
* properties:
* id:
* description: ID of the customer
* type: string
*/
export class AdminDeleteCustomerGroupsGroupCustomerBatchReq {
@ValidateNested({ each: true })
@Type(() => CustomerGroupsBatchCustomer)
customer_ids: CustomerGroupsBatchCustomer[]
}
@@ -1,149 +0,0 @@
import { Request, Response } from "express"
import CustomerController from "../../../../controllers/customers"
import { IsNumber, IsOptional, IsString } from "class-validator"
import { Type } from "class-transformer"
/**
* @oas [get] /admin/customer-groups/{id}/customers
* operationId: "GetCustomerGroupsGroupCustomers"
* summary: "List Customers"
* description: "Retrieve a list of customers in a customer group. The customers can be filtered by the `q` field. The customers can also be paginated."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the customer group.
* - (query) limit=50 {integer} The number of customers to return.
* - (query) offset=0 {integer} The number of customers to skip when retrieving the customers.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customers.
* - (query) q {string} a term to search customers by email, first_name, and last_name.
* x-codegen:
* method: listCustomers
* queryParams: AdminGetGroupsGroupCustomersParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.listCustomers(customerGroupId)
* .then(({ customers }) => {
* console.log(customers.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCustomerGroupCustomers } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const {
* customers,
* isLoading,
* } = useAdminCustomerGroupCustomers(
* customerGroupId
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {customers && !customers.length && (
* <span>No customers</span>
* )}
* {customers && customers.length > 0 && (
* <ul>
* {customers.map((customer) => (
* <li key={customer.id}>{customer.first_name}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/customer-groups/{id}/customers' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomersListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
req.query.groups = [id]
const result = await CustomerController.listAndCount(
req.scope,
req.query,
req.body
)
res.json(result)
}
/**
* Parameters used to filter and configure the pagination of the retrieved customer group's customers.
*/
// eslint-disable-next-line max-len
export class AdminGetGroupsGroupCustomersParams {
/**
* Search term to search customers by their email, first name, and last name.
*/
@IsString()
@IsOptional()
q?: string
/**
* {@inheritDoc FindPaginationParams.limit}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
limit = 50
/**
* {@inheritDoc FindPaginationParams.offset}
*/
@IsOptional()
@IsNumber()
@IsOptional()
@Type(() => Number)
offset = 0
/**
* {@inheritDoc FindParams.expand}
*/
@IsString()
@IsOptional()
expand?: string
}
@@ -1,100 +0,0 @@
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { FindParams } from "../../../../types/common"
/**
* @oas [get] /admin/customer-groups/{id}
* operationId: "GetCustomerGroupsGroup"
* summary: "Get a Customer Group"
* description: "Retrieve a Customer Group by its ID. You can expand the customer group's relations or select the fields that should be returned."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Customer Group.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customer group.
* - (query) fields {string} Comma-separated fields that should be included in the returned customer group.
* x-codegen:
* method: retrieve
* queryParams: AdminGetCustomerGroupsGroupParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.retrieve(customerGroupId)
* .then(({ customer_group }) => {
* console.log(customer_group.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCustomerGroup } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const { customer_group, isLoading } = useAdminCustomerGroup(
* customerGroupId
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {customer_group && <span>{customer_group.name}</span>}
* </div>
* )
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/customer-groups/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const customerGroup = await customerGroupService.retrieve(
id,
req.retrieveConfig
)
res.json({ customer_group: customerGroup })
}
export class AdminGetCustomerGroupsGroupParams extends FindParams {}
@@ -1,131 +0,0 @@
import { Router } from "express"
import { CustomerGroup } from "../../../.."
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import { AdminGetCustomerGroupsGroupParams } from "./get-customer-group"
import { AdminGetCustomerGroupsParams } from "./list-customer-groups"
const route = Router()
export default (app) => {
app.use("/customer-groups", route)
route.post("/", middlewares.wrap(require("./create-customer-group").default))
route.get(
"/",
transformQuery(AdminGetCustomerGroupsParams, {
defaultRelations: defaultAdminCustomerGroupsRelations,
isList: true,
}),
middlewares.wrap(require("./list-customer-groups").default)
)
const customerGroupRouter = Router({ mergeParams: true })
route.use("/:id", customerGroupRouter)
customerGroupRouter.get(
"/",
transformQuery(AdminGetCustomerGroupsGroupParams, {
defaultRelations: defaultAdminCustomerGroupsRelations,
}),
middlewares.wrap(require("./get-customer-group").default)
)
customerGroupRouter.delete(
"/",
middlewares.wrap(require("./delete-customer-group").default)
)
customerGroupRouter.post(
"/",
middlewares.wrap(require("./update-customer-group").default)
)
customerGroupRouter.get(
"/customers",
middlewares.wrap(require("./get-customer-group-customers").default)
)
customerGroupRouter.post(
"/customers/batch",
middlewares.wrap(require("./add-customers-batch").default)
)
customerGroupRouter.delete(
"/customers/batch",
middlewares.wrap(require("./delete-customers-batch").default)
)
return app
}
/* ************************************** */
/* ******** EXPORT API CLIENT TYPES ***** */
/* ************************************** */
/**
* @schema AdminCustomerGroupsRes
* type: object
* description: "The customer group's details."
* required:
* - customer_group
* properties:
* customer_group:
* description: Customer group details.
* $ref: "#/components/schemas/CustomerGroup"
*/
export type AdminCustomerGroupsRes = {
customer_group: CustomerGroup
}
/**
* @schema AdminCustomerGroupsDeleteRes
* type: object
* required:
* - id
* - object
* - deleted
* properties:
* id:
* type: string
* description: The ID of the deleted customer group.
* object:
* type: string
* description: The type of the object that was deleted.
* default: customer_group
* deleted:
* type: boolean
* description: Whether the customer group was deleted successfully or not.
* default: true
*/
export type AdminCustomerGroupsDeleteRes = DeleteResponse
/**
* @schema AdminCustomerGroupsListRes
* type: object
* required:
* - customer_groups
* - count
* - offset
* - limit
* properties:
* customer_groups:
* type: array
* description: An array of customer group details.
* items:
* $ref: "#/components/schemas/CustomerGroup"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of customer groups skipped when retrieving the customer groups.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminCustomerGroupsListRes = PaginatedResponse & {
customer_groups: CustomerGroup[]
}
export const defaultAdminCustomerGroupsRelations = []
export * from "./add-customers-batch"
export * from "./create-customer-group"
export * from "./delete-customers-batch"
export * from "./get-customer-group"
export * from "./list-customer-groups"
export * from "./update-customer-group"
@@ -1,242 +0,0 @@
import { IsNumber, IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { Type } from "class-transformer"
import { CustomerGroupService } from "../../../../services"
import { FilterableCustomerGroupProps } from "../../../../types/customer-groups"
/**
* @oas [get] /admin/customer-groups
* operationId: "GetCustomerGroups"
* summary: "List Customer Groups"
* description: "Retrieve a list of customer groups. The customer groups can be filtered by fields such as `name` or `id. The customer groups can also be sorted or paginated."
* x-authenticated: true
* parameters:
* - (query) q {string} term to search customer groups by name.
* - (query) offset=0 {integer} The number of customer groups to skip when retrieving the customer groups.
* - (query) order {string} A field to sort order the retrieved customer groups by.
* - (query) discount_condition_id {string} Filter by discount condition ID.
* - in: query
* name: id
* style: form
* explode: false
* description: Filter by the customer group ID
* schema:
* oneOf:
* - type: string
* description: customer group ID
* - type: array
* description: an array of customer group IDs
* items:
* type: string
* - type: object
* properties:
* lt:
* type: string
* description: filter by IDs less than this ID
* gt:
* type: string
* description: filter by IDs greater than this ID
* lte:
* type: string
* description: filter by IDs less than or equal to this ID
* gte:
* type: string
* description: filter by IDs greater than or equal to this ID
* - in: query
* name: name
* style: form
* explode: false
* description: Filter by the customer group name
* schema:
* type: array
* description: an array of customer group names
* items:
* type: string
* description: customer group name
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - (query) limit=10 {integer} The number of customer groups to return.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customer groups.
* - (query) fields {string} Comma-separated fields that should be included in the returned customer groups.
* x-codegen:
* method: list
* queryParams: AdminGetCustomerGroupsParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.list()
* .then(({ customer_groups, limit, offset, count }) => {
* console.log(customer_groups.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCustomerGroups } from "medusa-react"
*
* const CustomerGroups = () => {
* const {
* customer_groups,
* isLoading,
* } = useAdminCustomerGroups()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {customer_groups && !customer_groups.length && (
* <span>No Customer Groups</span>
* )}
* {customer_groups && customer_groups.length > 0 && (
* <ul>
* {customer_groups.map(
* (customerGroup) => (
* <li key={customerGroup.id}>
* {customerGroup.name}
* </li>
* )
* )}
* </ul>
* )}
* </div>
* )
* }
*
* export default CustomerGroups
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/customer-groups' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const [data, count] = await customerGroupService.listAndCount(
req.filterableFields,
req.listConfig
)
const { limit, offset } = req.validatedQuery
res.json({
count,
customer_groups: data,
offset,
limit,
})
}
/**
* Parameters used to filter and configure the pagination of the retrieved customer groups.
*/
export class AdminGetCustomerGroupsParams extends FilterableCustomerGroupProps {
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
*/
@IsString()
@IsOptional()
order?: string
/**
* {@inheritDoc FindPaginationParams.offset}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
offset?: number = 0
/**
* {@inheritDoc FindPaginationParams.limit}
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
limit?: number = 10
/**
* {@inheritDoc FindParams.expand}
*/
@IsString()
@IsOptional()
expand?: string
/**
* {@inheritDoc FindPaginationParams.fields}
*/
@IsString()
@IsOptional()
fields?: string
}
@@ -1,159 +0,0 @@
import { IsObject, IsOptional, IsString } from "class-validator"
import { Request, Response } from "express"
import { CustomerGroupService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { defaultAdminCustomerGroupsRelations } from "."
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/customer-groups/{id}
* operationId: "PostCustomerGroupsGroup"
* summary: "Update a Customer Group"
* description: "Update a Customer Group's details."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the customer group.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCustomerGroupsGroupReq"
* x-codegen:
* method: update
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customerGroups.update(customerGroupId, {
* name: "VIP"
* })
* .then(({ customer_group }) => {
* console.log(customer_group.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateCustomerGroup } from "medusa-react"
*
* type Props = {
* customerGroupId: string
* }
*
* const CustomerGroup = ({ customerGroupId }: Props) => {
* const updateCustomerGroup = useAdminUpdateCustomerGroup(
* customerGroupId
* )
* // ..
*
* const handleUpdate = (name: string) => {
* updateCustomerGroup.mutate({
* name,
* })
* }
*
* // ...
* }
*
* export default CustomerGroup
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/customer-groups/{id}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "name": "VIP"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customer Groups
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomerGroupsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { id } = req.params
const validatedBody = await validator(
AdminPostCustomerGroupsGroupReq,
req.body
)
const validatedQuery = await validator(FindParams, req.query)
const customerGroupService: CustomerGroupService = req.scope.resolve(
"customerGroupService"
)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await customerGroupService
.withTransaction(transactionManager)
.update(id, validatedBody)
})
let expandFields: string[] = []
if (validatedQuery.expand) {
expandFields = validatedQuery.expand.split(",")
}
const findConfig = {
relations: expandFields.length
? expandFields
: defaultAdminCustomerGroupsRelations,
}
const customerGroup = await customerGroupService.retrieve(id, findConfig)
res.json({ customer_group: customerGroup })
}
/**
* @schema AdminPostCustomerGroupsGroupReq
* type: object
* description: "The details to update in the customer group."
* properties:
* name:
* description: "Name of the customer group"
* type: string
* metadata:
* description: "Metadata of the customer group."
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCustomerGroupsGroupReq {
@IsString()
@IsOptional()
name?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,170 +0,0 @@
import { IsEmail, IsObject, IsOptional, IsString } from "class-validator"
import { CustomerService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/customers
* operationId: "PostCustomers"
* summary: "Create a Customer"
* description: "Create a customer as an admin."
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCustomersReq"
* x-codegen:
* method: create
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customers.create({
* email: "user@example.com",
* first_name: "Caterina",
* last_name: "Yost",
* password: "supersecret"
* })
* .then(({ customer }) => {
* console.log(customer.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateCustomer } from "medusa-react"
*
* type CustomerData = {
* first_name: string
* last_name: string
* email: string
* password: string
* }
*
* const CreateCustomer = () => {
* const createCustomer = useAdminCreateCustomer()
* // ...
*
* const handleCreate = (customerData: CustomerData) => {
* createCustomer.mutate(customerData, {
* onSuccess: ({ customer }) => {
* console.log(customer.id)
* }
* })
* }
*
* // ...
* }
*
* export default CreateCustomer
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/customers' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "email": "user@example.com",
* "first_name": "Caterina",
* "last_name": "Yost",
* "password": "supersecret"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customers
* responses:
* 201:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(AdminPostCustomersReq, req.body)
const customerService: CustomerService = req.scope.resolve("customerService")
const manager: EntityManager = req.scope.resolve("manager")
const customer = await manager.transaction(async (transactionManager) => {
return await customerService
.withTransaction(transactionManager)
.create(validated)
})
res.status(201).json({ customer })
}
/**
* @schema AdminPostCustomersReq
* type: object
* description: "The details of the customer to create."
* required:
* - email
* - first_name
* - last_name
* - password
* properties:
* email:
* type: string
* description: The customer's email.
* format: email
* first_name:
* type: string
* description: The customer's first name.
* last_name:
* type: string
* description: The customer's last name.
* password:
* type: string
* description: The customer's password.
* format: password
* phone:
* type: string
* description: The customer's phone number.
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCustomersReq {
@IsEmail()
email: string
@IsString()
first_name: string
@IsString()
last_name: string
@IsString()
password: string
@IsString()
@IsOptional()
phone?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,105 +0,0 @@
import CustomerService from "../../../../services/customer"
import { FindParams } from "../../../../types/common"
import { defaultAdminCustomersRelations } from "."
import { validator } from "../../../../utils/validator"
/**
* @oas [get] /admin/customers/{id}
* operationId: "GetCustomersCustomer"
* summary: "Get a Customer"
* description: "Retrieve the details of a customer."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Customer.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customer.
* - (query) fields {string} Comma-separated fields that should be included in the returned customer.
* x-codegen:
* method: retrieve
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customers.retrieve(customerId)
* .then(({ customer }) => {
* console.log(customer.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCustomer } from "medusa-react"
*
* type Props = {
* customerId: string
* }
*
* const Customer = ({ customerId }: Props) => {
* const { customer, isLoading } = useAdminCustomer(
* customerId
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {customer && <span>{customer.first_name}</span>}
* </div>
* )
* }
*
* export default Customer
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/customers/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customers
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const validated = await validator(FindParams, req.query)
const customerService: CustomerService = req.scope.resolve("customerService")
let expandFields: string[] = []
if (validated.expand) {
expandFields = validated.expand.split(",")
}
const findConfig = {
relations: expandFields.length
? expandFields
: defaultAdminCustomersRelations,
}
const customer = await customerService.retrieve(id, findConfig)
res.json({ customer })
}
@@ -1,77 +0,0 @@
import { Router } from "express"
import { Customer } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares from "../../../middlewares"
const route = Router()
export default (app) => {
app.use("/customers", route)
route.get(
"/",
middlewares.normalizeQuery(),
middlewares.wrap(require("./list-customers").default)
)
route.get("/:id", middlewares.wrap(require("./get-customer").default))
route.post("/", middlewares.wrap(require("./create-customer").default))
route.post("/:id", middlewares.wrap(require("./update-customer").default))
return app
}
/**
* @schema AdminCustomersRes
* type: object
* description: "The customer's details."
* x-expanded-relations:
* field: customer
* relations:
* - orders
* - shipping_addresses
* required:
* - customer
* properties:
* customer:
* description: "Customer details."
* $ref: "#/components/schemas/Customer"
*/
export type AdminCustomersRes = {
customer: Customer
}
/**
* @schema AdminCustomersListRes
* description: The list of customers with pagination fields.
* type: object
* required:
* - customers
* - count
* - offset
* - limit
* properties:
* customers:
* type: array
* description: "An array of customer details."
* items:
* $ref: "#/components/schemas/Customer"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of customers skipped when retrieving the customers.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminCustomersListRes = PaginatedResponse & {
customers: Customer[]
}
export const defaultAdminCustomersRelations = ["orders", "shipping_addresses"]
export * from "./create-customer"
export * from "./get-customer"
export * from "./list-customers"
export * from "./update-customer"
@@ -1,193 +0,0 @@
import { IsNumber, IsOptional, IsString } from "class-validator"
import { Type } from "class-transformer"
import { Request, Response } from "express"
import customerController from "../../../../controllers/customers"
import { AdminListCustomerSelector } from "../../../../types/customers"
/**
* @oas [get] /admin/customers
* operationId: "GetCustomers"
* summary: "List Customers"
* description: "Retrieve a list of Customers. The customers can be filtered by fields such as `q` or `groups`. The customers can also be paginated."
* x-authenticated: true
* parameters:
* - (query) limit=50 {integer} The number of customers to return.
* - (query) offset=0 {integer} The number of customers to skip when retrieving the customers.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customers.
* - (query) fields {string} Comma-separated fields that should be included in the returned customers.
* - (query) q {string} term to search customers' email, first_name, and last_name fields.
* - (query) has_account {boolean} Filter customers by whether they have an account.
* - (query) order {string} A field to sort-order the retrieved customers by.
* - in: query
* name: groups
* style: form
* explode: false
* description: Filter by customer group IDs.
* schema:
* type: array
* items:
* type: string
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* x-codegen:
* method: list
* queryParams: AdminGetCustomersParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customers.list()
* .then(({ customers, limit, offset, count }) => {
* console.log(customers.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCustomers } from "medusa-react"
*
* const Customers = () => {
* const { customers, isLoading } = useAdminCustomers()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {customers && !customers.length && (
* <span>No customers</span>
* )}
* {customers && customers.length > 0 && (
* <ul>
* {customers.map((customer) => (
* <li key={customer.id}>{customer.first_name}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default Customers
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/customers' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customers
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomersListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const result = await customerController.listAndCount(
req.scope,
req.query,
req.body
)
res.json(result)
}
/**
* Parameters used to filter and configure the pagination of the retrieved customers.
*/
export class AdminGetCustomersParams extends AdminListCustomerSelector {
/**
* {@inheritDoc FindPaginationParams.limit}
* @defaultValue 50
*/
@IsNumber()
@IsOptional()
@Type(() => Number)
limit = 50
/**
* {@inheritDoc FindPaginationParams.offset}
* @defaultValue 0
*/
@IsOptional()
@IsNumber()
@IsOptional()
@Type(() => Number)
offset = 0
/**
* {@inheritDoc FindParams.expand}
*/
@IsString()
@IsOptional()
expand?: string
/**
* {@inheritDoc FindParams.fields}
*/
@IsString()
@IsOptional()
fields?: string
}
@@ -1,227 +0,0 @@
import {
IsArray,
IsEmail,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import CustomerService from "../../../../services/customer"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { MedusaError } from "medusa-core-utils"
import { Type } from "class-transformer"
import { defaultAdminCustomersRelations } from "."
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/customers/{id}
* operationId: "PostCustomersCustomer"
* summary: "Update a Customer"
* description: "Update a Customer's details."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Customer.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned customer.
* - (query) fields {string} Comma-separated fields that should be retrieved in the returned customer.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostCustomersCustomerReq"
* x-codegen:
* method: update
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.customers.update(customerId, {
* first_name: "Dolly"
* })
* .then(({ customer }) => {
* console.log(customer.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateCustomer } from "medusa-react"
*
* type CustomerData = {
* first_name: string
* last_name: string
* email: string
* password: string
* }
*
* type Props = {
* customerId: string
* }
*
* const Customer = ({ customerId }: Props) => {
* const updateCustomer = useAdminUpdateCustomer(customerId)
* // ...
*
* const handleUpdate = (customerData: CustomerData) => {
* updateCustomer.mutate(customerData)
* }
*
* // ...
* }
*
* export default Customer
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/customers/{id}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "first_name": "Dolly"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Customers
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminCustomersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const validatedBody = await validator(AdminPostCustomersCustomerReq, req.body)
const validatedQuery = await validator(FindParams, req.query)
const customerService: CustomerService = req.scope.resolve("customerService")
let customer = await customerService.retrieve(id)
if (validatedBody.email && customer.has_account) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Email cannot be changed when the user has registered their account"
)
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await customerService
.withTransaction(transactionManager)
.update(id, validatedBody)
})
let expandFields: string[] = []
if (validatedQuery.expand) {
expandFields = validatedQuery.expand.split(",")
}
const findConfig = {
relations: expandFields.length
? expandFields
: defaultAdminCustomersRelations,
}
customer = await customerService.retrieve(id, findConfig)
res.status(200).json({ customer })
}
class Group {
@IsString()
id: string
}
/**
* @schema AdminPostCustomersCustomerReq
* type: object
* description: "The details of the customer to update."
* properties:
* email:
* type: string
* description: The Customer's email. You can't update the email of a registered customer.
* format: email
* first_name:
* type: string
* description: The Customer's first name.
* last_name:
* type: string
* description: The Customer's last name.
* phone:
* type: string
* description: The Customer's phone number.
* password:
* type: string
* description: The Customer's password.
* format: password
* groups:
* type: array
* description: A list of customer groups to which the customer belongs.
* items:
* type: object
* required:
* - id
* properties:
* id:
* description: The ID of a customer group
* type: string
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostCustomersCustomerReq {
@IsEmail()
@IsOptional()
email?: string
@IsString()
@IsOptional()
first_name?: string
@IsString()
@IsOptional()
last_name?: string
@IsString()
@IsOptional()
password?: string
@IsString()
@IsOptional()
phone?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
@IsArray()
@IsOptional()
@Type(() => Group)
@ValidateNested({ each: true })
groups?: Group[]
}
@@ -1,62 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
describe("successful addition", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}/regions/${IdMap.getId(
"region-france"
)}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("total10"),
{
select: [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
],
relations: ["parent_discount", "regions", "rule.conditions"],
}
)
expect(DiscountServiceMock.addRegion).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.addRegion).toHaveBeenCalledWith(
IdMap.getId("total10"),
IdMap.getId("region-france")
)
})
})
})
@@ -1,318 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
const validRegionId = IdMap.getId("region-france")
jest.setTimeout(30000)
describe("POST /admin/discounts", () => {
const adminSession = {
jwt: {
userId: IdMap.getId("admin_user"),
},
}
describe("successful creation", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: "02/02/2021 13:45",
ends_at: "03/14/2021 04:30",
},
adminSession,
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service create", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: new Date("02/02/2021 13:45"),
ends_at: new Date("03/14/2021 04:30"),
is_disabled: false,
is_dynamic: false,
})
})
})
describe("unsuccessful creation with dynamic discount using an invalid iso8601 duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "PaMT2D",
},
adminSession,
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`"valid_duration" must be a valid ISO 8601 duration`
)
})
})
describe("successful creation with dynamic discount", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
},
adminSession,
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service create", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: new Date("02/02/2021 13:45"),
is_disabled: false,
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
})
})
})
describe("fails on invalid data", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/discounts", {
payload: {
code: "10%OFF",
rule: {
description: "Test",
value: 10,
allocation: "total",
},
regions: [validRegionId],
},
adminSession,
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`Invalid rule type, must be one of "fixed", "percentage" or "free_shipping"`
)
})
})
describe("fails on xor constraint for conditions", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
conditions: [
{
products: ["product1"],
operator: "in",
product_types: ["producttype1"],
},
],
},
regions: [validRegionId],
starts_at: "02/02/2021 13:45",
is_dynamic: true,
valid_duration: "P1Y2M03DT04H05M",
},
adminSession,
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`Only one of products, product_types is allowed, Only one of product_types, products is allowed`
)
})
})
describe("fails on invalid date intervals", () => {
let subject
beforeAll(async () => {
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
ends_at: "02/02/2021",
starts_at: "03/14/2021",
},
adminSession,
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`"ends_at" must be greater than "starts_at"`
)
})
})
describe("successfully creates a dynamic discount without setting valid duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
is_dynamic: true,
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: "03/14/2021 14:30",
},
adminSession,
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("returns error", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
is_dynamic: true,
is_disabled: false,
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
regions: [validRegionId],
starts_at: new Date("03/14/2021 14:30"),
})
})
})
describe("unsuccessful creation with invalid regions", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("POST", "/admin/discounts", {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
},
},
adminSession,
})
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`each value in regions must be a string, regions must be an array`
)
})
it("does not call service create", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledTimes(0)
})
})
})
@@ -1,42 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
describe("DELETE /admin/discounts/discount_id", () => {
describe("successful deletion", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/discounts/${IdMap.getId("total10")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service delete", () => {
expect(DiscountServiceMock.delete).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.delete).toHaveBeenCalledWith(
IdMap.getId("total10")
)
})
it("returns correct delete object", () => {
expect(subject.body).toEqual({
id: IdMap.getId("total10"),
object: "discount",
deleted: true,
})
})
})
})
@@ -1,58 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
const defaultRelations = ["parent_discount", "regions", "rule.conditions"]
describe("GET /admin/discounts/:discount_id", () => {
describe("successful retrieval", () => {
let subject
beforeAll(async () => {
subject = await request(
"GET",
`/admin/discounts/${IdMap.getId("total10")}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("total10"),
{
select: defaultFields,
relations: defaultRelations,
}
)
})
})
})
@@ -1,142 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from ".."
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
describe("GET /admin/discounts", () => {
describe("successful retrieval of discounts", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("GET", `/admin/discounts`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve with config", () => {
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledWith(
{},
{
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
skip: 0,
take: 20,
order: { created_at: "DESC" },
}
)
})
})
describe("successful retrieval of discounts with query config", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"GET",
`/admin/discounts?q=OLI&limit=40&offset=20&is_dynamic=false&is_disabled=false`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve with config", () => {
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledWith(
{ q: "OLI", is_dynamic: false, is_disabled: false },
{
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
skip: 20,
take: 40,
order: { created_at: "DESC" },
}
)
})
})
describe("successful retrieval of dynamic discounts", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("GET", `/admin/discounts?is_dynamic=true`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve with corresponding config", () => {
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledWith(
{ is_dynamic: true },
{
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
skip: 0,
take: 20,
order: { created_at: "DESC" },
}
)
})
})
describe("successful retrieval of disabled discounts", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request("GET", `/admin/discounts?is_disabled=true`, {
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
})
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve with corresponding config", () => {
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.listAndCount).toHaveBeenCalledWith(
{ is_disabled: true },
{
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
skip: 0,
take: 20,
order: { created_at: "DESC" },
}
)
})
})
})
@@ -1,66 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
const defaultRelations = ["parent_discount", "regions", "rule.conditions"]
describe("DELETE /admin/discounts/:discount_id/regions/region_id", () => {
describe("successful removal", () => {
let subject
beforeAll(async () => {
subject = await request(
"DELETE",
`/admin/discounts/${IdMap.getId("total10")}/regions/${IdMap.getId(
"region-france"
)}`,
{
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service retrieve", () => {
expect(DiscountServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("total10"),
{
select: defaultFields,
relations: defaultRelations,
}
)
expect(DiscountServiceMock.removeRegion).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.removeRegion).toHaveBeenCalledWith(
IdMap.getId("total10"),
IdMap.getId("region-france")
)
})
})
})
@@ -1,128 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { DiscountServiceMock } from "../../../../../services/__mocks__/discount"
describe("POST /admin/discounts", () => {
describe("successful update", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
value: 10,
allocation: "total",
},
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls service method", () => {
expect(DiscountServiceMock.update).toHaveBeenCalledTimes(1)
expect(DiscountServiceMock.update).toHaveBeenCalledWith(
IdMap.getId("total10"),
{
code: "10TOTALOFF",
rule: {
id: "1234",
value: 10,
allocation: "total",
},
}
)
})
})
describe("unsuccessful update with dynamic discount using an invalid iso8601 duration", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
value: 10,
},
starts_at: "02/02/2021 13:45",
valid_duration: "PaMT2D",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`"valid_duration" must be a valid ISO 8601 duration`
)
})
})
describe("fails on invalid date intervals", () => {
let subject
beforeAll(async () => {
jest.clearAllMocks()
subject = await request(
"POST",
`/admin/discounts/${IdMap.getId("total10")}`,
{
payload: {
code: "10TOTALOFF",
rule: {
id: "1234",
value: 10,
allocation: "total",
},
ends_at: "02/02/2021",
starts_at: "03/14/2021",
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 400", () => {
expect(subject.status).toEqual(400)
})
it("returns error", () => {
expect(subject.body.message).toEqual(
`"ends_at" must be greater than "starts_at"`
)
})
})
})
@@ -1,104 +0,0 @@
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
import { Discount } from "../../../.."
import DiscountService from "../../../../services/discount"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/discounts/{id}/regions/{region_id}
* operationId: "PostDiscountsDiscountRegionsRegion"
* summary: "Add Region to Discount"
* description: "Add a Region to the list of Regions a Discount can be used in."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount.
* - (path) region_id=* {string} The ID of the Region.
* x-codegen:
* method: addRegion
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.addRegion(discountId, regionId)
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDiscountAddRegion } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const addRegion = useAdminDiscountAddRegion(discountId)
* // ...
*
* const handleAdd = (regionId: string) => {
* addRegion.mutate(regionId, {
* onSuccess: ({ discount }) => {
* console.log(discount.regions)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}/regions/{region_id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { discount_id, region_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.addRegion(discount_id, region_id)
})
const discount: Discount = await discountService.retrieve(discount_id, {
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
})
res.status(200).json({ discount })
}
@@ -1,182 +0,0 @@
import DiscountConditionService from "../../../../services/discount-condition"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { DiscountService } from "../../../../services"
import {
DiscountConditionInput,
DiscountConditionMapTypeToProperty,
} from "../../../../types/discount"
import { IsArray } from "class-validator"
import { FindParams } from "../../../../types/common"
/**
* @oas [post] /admin/discounts/{discount_id}/conditions/{condition_id}/batch
* operationId: "PostDiscountsDiscountConditionsConditionBatch"
* summary: "Add Batch Resources"
* description: "Add a batch of resources to a discount condition. The type of resource depends on the type of discount condition. For example, if the discount condition's type is `products`,
* the resources being added should be products."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the discount the condition belongs to.
* - (path) condition_id=* {string} The ID of the discount condition on which to add the item.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsDiscountConditionsConditionBatchReq"
* x-codegen:
* method: addConditionResourceBatch
* queryParams: AdminPostDiscountsDiscountConditionsConditionBatchParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.addConditionResourceBatch(discountId, conditionId, {
* resources: [{ id: itemId }]
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminAddDiscountConditionResourceBatch
* } from "medusa-react"
*
* type Props = {
* discountId: string
* conditionId: string
* }
*
* const DiscountCondition = ({
* discountId,
* conditionId
* }: Props) => {
* const addConditionResources = useAdminAddDiscountConditionResourceBatch(
* discountId,
* conditionId
* )
* // ...
*
* const handleAdd = (itemId: string) => {
* addConditionResources.mutate({
* resources: [
* {
* id: itemId
* }
* ]
* }, {
* onSuccess: ({ discount }) => {
* console.log(discount.id)
* }
* })
* }
*
* // ...
* }
*
* export default DiscountCondition
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}/conditions/{condition_id}/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "resources": [{ "id": "item_id" }]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id, condition_id } = req.params
const validatedBody =
req.validatedBody as AdminPostDiscountsDiscountConditionsConditionBatchReq
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const condition = await conditionService.retrieve(condition_id, {
select: ["id", "type", "discount_rule_id"],
})
const updateObj: DiscountConditionInput = {
id: condition_id,
rule_id: condition.discount_rule_id,
[DiscountConditionMapTypeToProperty[condition.type]]:
validatedBody.resources,
}
await manager.transaction(async (transactionManager) => {
await conditionService
.withTransaction(transactionManager)
.upsertCondition(updateObj, false)
})
const discountService: DiscountService = req.scope.resolve("discountService")
const discount = await discountService.retrieve(
discount_id,
req.retrieveConfig
)
res.status(200).json({ discount })
}
/**
* @schema AdminPostDiscountsDiscountConditionsConditionBatchReq
* type: object
* description: "The details of the resources to add."
* required:
* - resources
* properties:
* resources:
* description: The resources to be added to the discount condition
* type: array
* items:
* type: object
* required:
* - id
* properties:
* id:
* description: The ID of the item
* type: string
*/
export class AdminPostDiscountsDiscountConditionsConditionBatchReq {
@IsArray()
resources: { id: string }[]
}
// eslint-disable-next-line max-len
export class AdminPostDiscountsDiscountConditionsConditionBatchParams extends FindParams {}
@@ -1,181 +0,0 @@
import { Request, Response } from "express"
import { DiscountConditionOperator } from "../../../../models"
import { IsString } from "class-validator"
import { AdminUpsertConditionsReq } from "../../../../types/discount"
import DiscountConditionService from "../../../../services/discount-condition"
import { DiscountService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
/**
* @oas [post] /admin/discounts/{discount_id}/conditions
* operationId: "PostDiscountsDiscountConditions"
* summary: "Create a Condition"
* description: "Create a Discount Condition. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided, based on the type of discount condition.
* For example, if the discount condition's type is `products`, the `products` field should be provided in the request body."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the discount.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsDiscountConditions"
* x-codegen:
* method: createCondition
* queryParams: AdminPostDiscountsDiscountConditionsParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* import { DiscountConditionOperator } from "@medusajs/medusa"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.createCondition(discountId, {
* operator: DiscountConditionOperator.IN,
* products: [productId]
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { DiscountConditionOperator } from "@medusajs/medusa"
* import { useAdminDiscountCreateCondition } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const createCondition = useAdminDiscountCreateCondition(discountId)
* // ...
*
* const handleCreateCondition = (
* operator: DiscountConditionOperator,
* products: string[]
* ) => {
* createCondition.mutate({
* operator,
* products
* }, {
* onSuccess: ({ discount }) => {
* console.log(discount.id)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}/conditions' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "operator": "in"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const discountService: DiscountService = req.scope.resolve("discountService")
let discount = await discountService.retrieve(discount_id)
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await conditionService
.withTransaction(transactionManager)
.upsertCondition({
...(req.validatedBody as AdminPostDiscountsDiscountConditions),
rule_id: discount.rule_id,
})
})
discount = await discountService.retrieve(discount.id, req.retrieveConfig)
res.status(200).json({ discount })
}
/**
* @schema AdminPostDiscountsDiscountConditions
* type: object
* required:
* - operator
* properties:
* operator:
* description: >-
* Operator of the condition. `in` indicates that discountable resources are within the specified resources. `not_in` indicates that
* discountable resources are everything but the specified resources.
* type: string
* enum: [in, not_in]
* products:
* type: array
* description: list of product IDs if the condition's type is `products`.
* items:
* type: string
* product_types:
* type: array
* description: list of product type IDs if the condition's type is `product_types`.
* items:
* type: string
* product_collections:
* type: array
* description: list of product collection IDs if the condition's type is `product_collections`.
* items:
* type: string
* product_tags:
* type: array
* description: list of product tag IDs if the condition's type is `product_tags`.
* items:
* type: string
* customer_groups:
* type: array
* description: list of customer group IDs if the condition's type is `customer_groups`.
* items:
* type: string
*/
// eslint-disable-next-line max-len
export class AdminPostDiscountsDiscountConditions extends AdminUpsertConditionsReq {
@IsString()
operator: DiscountConditionOperator
}
export class AdminPostDiscountsDiscountConditionsParams extends FindParams {}
@@ -1,382 +0,0 @@
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsPositive,
IsString,
ValidateNested,
} from "class-validator"
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import {
AllocationType,
DiscountConditionOperator,
DiscountRuleType,
} from "../../../../models"
import DiscountService from "../../../../services/discount"
import { FindParams } from "../../../../types/common"
import { AdminUpsertConditionsReq } from "../../../../types/discount"
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
/**
* @oas [post] /admin/discounts
* operationId: "PostDiscounts"
* summary: "Create a Discount"
* x-authenticated: true
* description: "Create a Discount with a given set of rules that defines how the Discount is applied."
* parameters:
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be retrieved in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsReq"
* x-codegen:
* method: create
* queryParams: AdminPostDiscountsParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* import { AllocationType, DiscountRuleType } from "@medusajs/medusa"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.create({
* code: "TEST",
* rule: {
* type: DiscountRuleType.FIXED,
* value: 10,
* allocation: AllocationType.ITEM
* },
* regions: ["reg_XXXXXXXX"],
* is_dynamic: false,
* is_disabled: false
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminCreateDiscount,
* } from "medusa-react"
* import {
* AllocationType,
* DiscountRuleType,
* } from "@medusajs/medusa"
*
* const CreateDiscount = () => {
* const createDiscount = useAdminCreateDiscount()
* // ...
*
* const handleCreate = (
* currencyCode: string,
* regionId: string
* ) => {
* // ...
* createDiscount.mutate({
* code: currencyCode,
* rule: {
* type: DiscountRuleType.FIXED,
* value: 10,
* allocation: AllocationType.ITEM,
* },
* regions: [
* regionId,
* ],
* is_dynamic: false,
* is_disabled: false,
* })
* }
*
* // ...
* }
*
* export default CreateDiscount
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "code": "TEST",
* "rule": {
* "type": "fixed",
* "value": 10,
* "allocation": "item"
* },
* "regions": ["reg_XXXXXXXX"]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
const created = await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.create(req.validatedBody as AdminPostDiscountsReq)
})
const discount = await discountService.retrieve(
created.id,
req.retrieveConfig
)
res.status(200).json({ discount })
}
/**
* Details of the discount rule to create.
*/
export class AdminPostDiscountsDiscountRule {
/**
* The discount rule's description.
*/
@IsString()
@IsOptional()
description?: string
/**
* The discount rule's type.
*/
@IsEnum(DiscountRuleType, {
message: `Invalid rule type, must be one of "fixed", "percentage" or "free_shipping"`,
})
type: DiscountRuleType
/**
* The discount rule's value.
*/
@IsNumber()
value: number
/**
* The discount rule's allocation.
*/
@IsEnum(AllocationType, {
message: `Invalid allocation type, must be one of "total" or "item"`,
})
allocation: AllocationType
/**
* The discount rule's conditions.
*/
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdminCreateCondition)
conditions?: AdminCreateCondition[]
}
/**
* @schema AdminPostDiscountsReq
* type: object
* description: "The details of the discount to create."
* required:
* - code
* - rule
* - regions
* properties:
* code:
* type: string
* description: A unique code that will be used to redeem the discount
* is_dynamic:
* type: boolean
* description: Whether the discount should have multiple instances of itself, each with a different code. This can be useful for automatically generated discount codes that all have to follow a common set of rules.
* default: false
* rule:
* description: The discount rule that defines how discounts are calculated
* type: object
* required:
* - type
* - value
* - allocation
* properties:
* description:
* type: string
* description: "A short description of the discount"
* type:
* type: string
* description: >-
* The type of the discount, can be `fixed` for discounts that reduce the price by a fixed amount, `percentage` for percentage reductions or `free_shipping` for shipping vouchers.
* enum: [fixed, percentage, free_shipping]
* value:
* type: number
* description: "The value that the discount represents. This will depend on the type of the discount."
* allocation:
* type: string
* description: >-
* The scope that the discount should apply to. `total` indicates that the discount should be applied on the cart total, and `item` indicates that the discount should be applied to each discountable item in the cart.
* enum: [total, item]
* conditions:
* type: array
* description: "A set of conditions that can be used to limit when the discount can be used. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided based on the discount condition's type."
* items:
* type: object
* required:
* - operator
* properties:
* operator:
* type: string
* description: >-
* Operator of the condition. `in` indicates that discountable resources are within the specified resources. `not_in` indicates that
* discountable resources are everything but the specified resources.
* enum: [in, not_in]
* products:
* type: array
* description: list of product IDs if the condition's type is `products`.
* items:
* type: string
* product_types:
* type: array
* description: list of product type IDs if the condition's type is `product_types`.
* items:
* type: string
* product_collections:
* type: array
* description: list of product collection IDs if the condition's type is `product_collections`.
* items:
* type: string
* product_tags:
* type: array
* description: list of product tag IDs if the condition's type is `product_tags`.
* items:
* type: string
* customer_groups:
* type: array
* description: list of customer group IDs if the condition's type is `customer_groups`.
* items:
* type: string
* is_disabled:
* type: boolean
* description: >-
* Whether the discount code is disabled on creation. If set to `true`, it will not be available for customers.
* default: false
* starts_at:
* type: string
* format: date-time
* description: The date and time at which the discount should be available.
* ends_at:
* type: string
* format: date-time
* description: The date and time at which the discount should no longer be available.
* valid_duration:
* type: string
* description: The duration the discount runs between
* example: P3Y6M4DT12H30M5S
* regions:
* description: A list of region IDs representing the Regions in which the Discount can be used.
* type: array
* items:
* type: string
* usage_limit:
* type: number
* description: Maximum number of times the discount can be used
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostDiscountsReq {
@IsString()
@IsNotEmpty()
code: string
@IsNotEmpty()
@ValidateNested()
@Type(() => AdminPostDiscountsDiscountRule)
rule: AdminPostDiscountsDiscountRule
@IsBoolean()
@IsOptional()
is_dynamic = false
@IsBoolean()
@IsOptional()
is_disabled = false
@IsDate()
@IsOptional()
@Type(() => Date)
starts_at?: Date
@IsDate()
@IsOptional()
@IsGreaterThan("starts_at")
@Type(() => Date)
ends_at?: Date
@IsISO8601Duration()
@IsOptional()
valid_duration?: string
@IsNumber()
@IsOptional()
@IsPositive()
usage_limit?: number
@IsArray()
@IsString({ each: true })
regions: string[]
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
/**
* Details of the discount condition to create.
*/
export class AdminCreateCondition extends AdminUpsertConditionsReq {
/**
* The operator of the discount condition.
*/
@IsString()
operator: DiscountConditionOperator
}
/**
* {@inheritDoc FindParams}
*/
export class AdminPostDiscountsParams extends FindParams {}
@@ -1,165 +0,0 @@
import { Request, Response } from "express"
import {
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
} from "class-validator"
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
import DiscountService from "../../../../services/discount"
import { EntityManager } from "typeorm"
/**
* @oas [post] /admin/discounts/{id}/dynamic-codes
* operationId: "PostDiscountsDiscountDynamicCodes"
* summary: "Create a Dynamic Code"
* description: "Create a dynamic unique code that can map to a parent Discount. This is useful if you want to automatically generate codes with the same rules and conditions."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount to create the dynamic code for."
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsDiscountDynamicCodesReq"
* x-codegen:
* method: createDynamicCode
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.createDynamicCode(discountId, {
* code: "TEST",
* usage_limit: 1
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateDynamicDiscountCode } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const createDynamicDiscount = useAdminCreateDynamicDiscountCode(discountId)
* // ...
*
* const handleCreate = (
* code: string,
* usageLimit: number
* ) => {
* createDynamicDiscount.mutate({
* code,
* usage_limit: usageLimit
* }, {
* onSuccess: ({ discount }) => {
* console.log(discount.is_dynamic)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}/dynamic-codes' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "code": "TEST"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
const created = await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.createDynamicCode(
discount_id,
req.validatedBody as AdminPostDiscountsDiscountDynamicCodesReq
)
})
const discount = await discountService.retrieve(created.id, {
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
})
res.status(200).json({ discount })
}
/**
* @schema AdminPostDiscountsDiscountDynamicCodesReq
* type: object
* description: "The details of the dynamic discount to create."
* required:
* - code
* properties:
* code:
* type: string
* description: A unique code that will be used to redeem the Discount
* usage_limit:
* type: number
* description: Maximum number of times the discount code can be used
* default: 1
* metadata:
* type: object
* description: An optional set of key-value pairs to hold additional information.
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostDiscountsDiscountDynamicCodesReq {
@IsString()
@IsNotEmpty()
code: string
@IsNumber()
@IsOptional()
usage_limit = 1
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -1,149 +0,0 @@
import DiscountConditionService from "../../../../services/discount-condition"
import { DiscountService } from "../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindParams } from "../../../../types/common"
/**
* @oas [delete] /admin/discounts/{discount_id}/conditions/{condition_id}
* operationId: "DeleteDiscountsDiscountConditionsCondition"
* summary: "Delete a Condition"
* description: "Delete a Discount Condition. This does not delete resources associated to the discount condition."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the Discount
* - (path) condition_id=* {string} The ID of the Discount Condition
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* x-codegen:
* method: deleteCondition
* queryParams: AdminDeleteDiscountsDiscountConditionsConditionParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.deleteCondition(discountId, conditionId)
* .then(({ id, object, deleted }) => {
* console.log(id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminDiscountRemoveCondition
* } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const deleteCondition = useAdminDiscountRemoveCondition(
* discountId
* )
* // ...
*
* const handleDelete = (
* conditionId: string
* ) => {
* deleteCondition.mutate(conditionId, {
* onSuccess: ({ id, object, deleted }) => {
* console.log(deleted)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/discounts/{id}/conditions/{condition_id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountConditionsDeleteRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { discount_id, condition_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const discountService: DiscountService = req.scope.resolve("discountService")
const condition = await conditionService
.retrieve(condition_id)
.catch(() => void 0)
if (!condition) {
const discount = await discountService.retrieve(
discount_id,
req.retrieveConfig
)
// resolves idempotently in case of non-existing condition
return res.json({
id: condition_id,
object: "discount-condition",
deleted: true,
discount,
})
}
let discount = await discountService.retrieve(discount_id, {
select: ["id", "rule_id"],
})
if (condition.discount_rule_id !== discount.rule_id) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Condition with id ${condition_id} does not belong to Discount with id ${discount_id}`
)
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await conditionService
.withTransaction(transactionManager)
.delete(condition_id)
})
discount = await discountService.retrieve(discount_id, req.retrieveConfig)
res.json({
id: condition_id,
object: "discount-condition",
deleted: true,
discount,
})
}
export class AdminDeleteDiscountsDiscountConditionsConditionParams extends FindParams {}
@@ -1,90 +0,0 @@
import DiscountService from "../../../../services/discount"
import { EntityManager } from "typeorm"
/**
* @oas [delete] /admin/discounts/{id}
* operationId: "DeleteDiscountsDiscount"
* summary: "Delete a Discount"
* description: "Delete a Discount. Deleting the discount will make it unavailable for customers to use."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount
* x-codegen:
* method: delete
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.delete(discountId)
* .then(({ id, object, deleted }) => {
* console.log(id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteDiscount } from "medusa-react"
*
* const Discount = () => {
* const deleteDiscount = useAdminDeleteDiscount(discount_id)
* // ...
*
* const handleDelete = () => {
* deleteDiscount.mutate()
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/discounts/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsDeleteRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { discount_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.delete(discount_id)
})
res.json({
id: discount_id,
object: "discount",
deleted: true,
})
}
@@ -1,102 +0,0 @@
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
import DiscountService from "../../../../services/discount"
import { EntityManager } from "typeorm"
/**
* @oas [delete] /admin/discounts/{id}/dynamic-codes/{code}
* operationId: "DeleteDiscountsDiscountDynamicCodesCode"
* summary: "Delete a Dynamic Code"
* description: "Delete a dynamic code from a Discount."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount
* - (path) code=* {string} The dynamic code to delete
* x-codegen:
* method: deleteDynamicCode
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.deleteDynamicCode(discountId, code)
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteDynamicDiscountCode } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const deleteDynamicDiscount = useAdminDeleteDynamicDiscountCode(discountId)
* // ...
*
* const handleDelete = (code: string) => {
* deleteDynamicDiscount.mutate(code, {
* onSuccess: ({ discount }) => {
* console.log(discount.is_dynamic)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/discounts/{id}/dynamic-codes/{code}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { discount_id, code } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.deleteDynamicCode(discount_id, code)
})
const discount = await discountService.retrieve(discount_id, {
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
})
res.status(200).json({ discount })
}
@@ -1,182 +0,0 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { DiscountConditionService, DiscountService } from "../../../../services"
import {
DiscountConditionInput,
DiscountConditionMapTypeToProperty,
} from "../../../../types/discount"
import { IsArray } from "class-validator"
import { FindParams } from "../../../../types/common"
/**
* @oas [delete] /admin/discounts/{discount_id}/conditions/{condition_id}/batch
* operationId: "DeleteDiscountsDiscountConditionsConditionBatch"
* summary: "Remove Batch Resources"
* description: "Remove a batch of resources from a discount condition. This will only remove the association between the resource and the discount condition, not the resource itself."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the discount.
* - (path) condition_id=* {string} The ID of the condition to remove the resources from.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDeleteDiscountsDiscountConditionsConditionBatchReq"
* x-codegen:
* method: deleteConditionResourceBatch
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.deleteConditionResourceBatch(discountId, conditionId, {
* resources: [{ id: itemId }]
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import {
* useAdminDeleteDiscountConditionResourceBatch
* } from "medusa-react"
*
* type Props = {
* discountId: string
* conditionId: string
* }
*
* const DiscountCondition = ({
* discountId,
* conditionId
* }: Props) => {
* const deleteConditionResource = useAdminDeleteDiscountConditionResourceBatch(
* discountId,
* conditionId,
* )
* // ...
*
* const handleDelete = (itemId: string) => {
* deleteConditionResource.mutate({
* resources: [
* {
* id: itemId
* }
* ]
* }, {
* onSuccess: ({ discount }) => {
* console.log(discount.id)
* }
* })
* }
*
* // ...
* }
*
* export default DiscountCondition
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/discounts/{id}/conditions/{condition_id}/batch' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "resources": [{ "id": "item_id" }]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id, condition_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const condition = await conditionService.retrieve(condition_id, {
select: ["id", "type", "discount_rule_id"],
})
const validatedBody =
req.validatedBody as AdminDeleteDiscountsDiscountConditionsConditionBatchReq
const data = {
id: condition_id,
rule_id: condition.discount_rule_id,
[DiscountConditionMapTypeToProperty[condition.type]]:
validatedBody.resources,
} as Omit<DiscountConditionInput, "id"> & { id: string }
await manager.transaction(async (transactionManager) => {
await conditionService
.withTransaction(transactionManager)
.removeResources(data)
})
const discountService: DiscountService = req.scope.resolve("discountService")
const discount = await discountService.retrieve(
discount_id,
req.retrieveConfig
)
res.status(200).json({ discount })
}
/**
* {@inheritDoc FindParams}
*/
// eslint-disable-next-line max-len
export class AdminDeleteDiscountsDiscountConditionsConditionBatchParams extends FindParams {}
/**
* @schema AdminDeleteDiscountsDiscountConditionsConditionBatchReq
* type: object
* description: "The resources to remove."
* required:
* - resources
* properties:
* resources:
* description: The resources to be removed from the discount condition
* type: array
* items:
* type: object
* required:
* - id
* properties:
* id:
* description: The id of the item
* type: string
*/
export class AdminDeleteDiscountsDiscountConditionsConditionBatchReq {
@IsArray()
resources: { id: string }[]
}
@@ -1,111 +0,0 @@
import { Request, Response } from "express"
import DiscountConditionService from "../../../../services/discount-condition"
import { FindParams } from "../../../../types/common"
/**
* @oas [get] /admin/discounts/{discount_id}/conditions/{condition_id}
* operationId: "GetDiscountsDiscountConditionsCondition"
* summary: "Get a Condition"
* description: "Retrieve a Discount Condition's details."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the Discount.
* - (path) condition_id=* {string} The ID of the Discount Condition.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount condition.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount condition.
* x-codegen:
* method: getCondition
* queryParams: AdminGetDiscountsDiscountConditionsConditionParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.getCondition(discountId, conditionId)
* .then(({ discount_condition }) => {
* console.log(discount_condition.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminGetDiscountCondition } from "medusa-react"
*
* type Props = {
* discountId: string
* discountConditionId: string
* }
*
* const DiscountCondition = ({
* discountId,
* discountConditionId
* }: Props) => {
* const {
* discount_condition,
* isLoading
* } = useAdminGetDiscountCondition(
* discountId,
* discountConditionId
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {discount_condition && (
* <span>{discount_condition.type}</span>
* )}
* </div>
* )
* }
*
* export default DiscountCondition
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/discounts/{id}/conditions/{condition_id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountConditionsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { condition_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const discountCondition = await conditionService.retrieve(
condition_id,
req.retrieveConfig
)
res.status(200).json({ discount_condition: discountCondition })
}
export class AdminGetDiscountsDiscountConditionsConditionParams extends FindParams {}
@@ -1,96 +0,0 @@
import { Request, Response } from "express"
import DiscountService from "../../../../services/discount"
import { FindParams } from "../../../../types/common"
/**
* @oas [get] /admin/discounts/code/{code}
* operationId: "GetDiscountsDiscountCode"
* summary: "Get Discount by Code"
* description: "Retrieve a Discount's details by its discount code"
* x-authenticated: true
* parameters:
* - (path) code=* {string} The code of the Discount
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* x-codegen:
* method: retrieveByCode
* queryParams: AdminGetDiscountsDiscountCodeParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.retrieveByCode(code)
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminGetDiscountByCode } from "medusa-react"
*
* type Props = {
* discountCode: string
* }
*
* const Discount = ({ discountCode }: Props) => {
* const { discount, isLoading } = useAdminGetDiscountByCode(
* discountCode
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {discount && <span>{discount.code}</span>}
* </div>
* )
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/discounts/code/{code}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { code } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const discount = await discountService.retrieveByCode(
code,
req.retrieveConfig
)
res.status(200).json({ discount })
}
export class AdminGetDiscountsDiscountCodeParams extends FindParams {}
@@ -1,93 +0,0 @@
import DiscountService from "../../../../services/discount"
import { Request, Response } from "express"
import { FindParams } from "../../../../types/common"
/**
* @oas [get] /admin/discounts/{id}
* operationId: "GetDiscountsDiscount"
* summary: "Get a Discount"
* description: "Retrieve a Discount."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* x-codegen:
* method: retrieve
* queryParams: AdminGetDiscountParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.retrieve(discountId)
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDiscount } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const { discount, isLoading } = useAdminDiscount(
* discountId
* )
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {discount && <span>{discount.code}</span>}
* </div>
* )
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/discounts/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const data = await discountService.retrieve(discount_id, req.retrieveConfig)
res.status(200).json({ discount: data })
}
export class AdminGetDiscountParams extends FindParams {}
@@ -1,366 +0,0 @@
import { Router } from "express"
import "reflect-metadata"
import { Discount } from "../../../.."
import { DiscountCondition } from "../../../../models"
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import middlewares, {
doesConditionBelongToDiscount,
transformBody,
transformQuery,
} from "../../../middlewares"
import {
AdminPostDiscountsDiscountConditionsConditionBatchParams,
AdminPostDiscountsDiscountConditionsConditionBatchReq,
} from "./add-resources-to-condition-batch"
import {
AdminPostDiscountsDiscountConditionsCondition,
AdminPostDiscountsDiscountConditionsConditionParams,
} from "./update-condition"
import {
AdminPostDiscountsDiscountConditions,
AdminPostDiscountsDiscountConditionsParams,
} from "./create-condition"
import { AdminPostDiscountsDiscountDynamicCodesReq } from "./create-dynamic-code"
import {
AdminPostDiscountsDiscountParams,
AdminPostDiscountsDiscountReq,
} from "./update-discount"
import {
AdminPostDiscountsParams,
AdminPostDiscountsReq,
} from "./create-discount"
import { AdminGetDiscountsParams } from "./list-discounts"
import { AdminGetDiscountsDiscountConditionsConditionParams } from "./get-condition"
import { AdminDeleteDiscountsDiscountConditionsConditionParams } from "./delete-condition"
import { AdminGetDiscountsDiscountCodeParams } from "./get-discount-by-code"
import { AdminGetDiscountParams } from "./get-discount"
import {
AdminDeleteDiscountsDiscountConditionsConditionBatchParams,
AdminDeleteDiscountsDiscountConditionsConditionBatchReq,
} from "./delete-resources-from-condition-batch"
const route = Router()
export default (app) => {
app.use("/discounts", route)
route.get(
"/",
transformQuery(AdminGetDiscountsParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: true,
}),
middlewares.wrap(require("./list-discounts").default)
)
route.post(
"/",
transformQuery(AdminPostDiscountsParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminPostDiscountsReq),
middlewares.wrap(require("./create-discount").default)
)
route.get(
"/:discount_id",
transformQuery(AdminGetDiscountParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
middlewares.wrap(require("./get-discount").default)
)
route.get(
"/code/:code",
transformQuery(AdminGetDiscountsDiscountCodeParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
middlewares.wrap(require("./get-discount-by-code").default)
)
route.post(
"/:discount_id",
transformQuery(AdminPostDiscountsDiscountParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminPostDiscountsDiscountReq),
middlewares.wrap(require("./update-discount").default)
)
route.delete(
"/:discount_id",
middlewares.wrap(require("./delete-discount").default)
)
// Dynamic codes
route.post(
"/:discount_id/dynamic-codes",
transformBody(AdminPostDiscountsDiscountDynamicCodesReq),
middlewares.wrap(require("./create-dynamic-code").default)
)
route.delete(
"/:discount_id/dynamic-codes/:code",
middlewares.wrap(require("./delete-dynamic-code").default)
)
// Discount region management
route.post(
"/:discount_id/regions/:region_id",
middlewares.wrap(require("./add-region").default)
)
route.delete(
"/:discount_id/regions/:region_id",
middlewares.wrap(require("./remove-region").default)
)
// Discount condition management
route.post(
"/:discount_id/conditions",
transformQuery(AdminPostDiscountsDiscountConditionsParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminPostDiscountsDiscountConditions),
middlewares.wrap(require("./create-condition").default)
)
route.delete(
"/:discount_id/conditions/:condition_id",
transformQuery(AdminDeleteDiscountsDiscountConditionsConditionParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
middlewares.wrap(require("./delete-condition").default)
)
const conditionRouter = Router({ mergeParams: true })
route.use(
"/:discount_id/conditions/:condition_id",
doesConditionBelongToDiscount,
conditionRouter
)
conditionRouter.get(
"/",
transformQuery(AdminGetDiscountsDiscountConditionsConditionParams, {
defaultFields: defaultAdminDiscountConditionFields,
defaultRelations: defaultAdminDiscountConditionRelations,
isList: false,
}),
middlewares.wrap(require("./get-condition").default)
)
conditionRouter.post(
"/",
transformQuery(AdminPostDiscountsDiscountConditionsConditionParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminPostDiscountsDiscountConditionsCondition),
middlewares.wrap(require("./update-condition").default)
)
conditionRouter.post(
"/batch",
transformQuery(AdminPostDiscountsDiscountConditionsConditionBatchParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminPostDiscountsDiscountConditionsConditionBatchReq),
middlewares.wrap(require("./add-resources-to-condition-batch").default)
)
conditionRouter.delete(
"/batch",
transformQuery(AdminDeleteDiscountsDiscountConditionsConditionBatchParams, {
defaultFields: defaultAdminDiscountsFields,
defaultRelations: defaultAdminDiscountsRelations,
isList: false,
}),
transformBody(AdminDeleteDiscountsDiscountConditionsConditionBatchReq),
middlewares.wrap(require("./delete-resources-from-condition-batch").default)
)
return app
}
export const defaultAdminDiscountsFields: (keyof Discount)[] = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
"updated_at",
"deleted_at",
"metadata",
"valid_duration",
]
export const defaultAdminDiscountsRelations = [
"parent_discount",
"regions",
"rule.conditions",
]
export const defaultAdminDiscountConditionFields: (keyof DiscountCondition)[] =
["id", "type", "operator", "discount_rule_id", "created_at", "updated_at"]
export const defaultAdminDiscountConditionRelations = ["discount_rule"]
/**
* @schema AdminDiscountsRes
* type: object
* description: "The discount's details."
* x-expanded-relations:
* field: discount
* relations:
* - parent_discount
* - regions
* - rule
* - rule.conditions
* eager:
* - regions.fulfillment_providers
* - regions.payment_providers
* required:
* - discount
* properties:
* discount:
* description: "Discount details."
* $ref: "#/components/schemas/Discount"
*/
export type AdminDiscountsRes = {
discount: Discount
}
/**
* @schema AdminDiscountConditionsRes
* type: object
* x-expanded-relations:
* field: discount_condition
* relations:
* - discount_rule
* required:
* - discount_condition
* properties:
* discount_condition:
* description: "Discount condition details."
* $ref: "#/components/schemas/DiscountCondition"
*/
export type AdminDiscountConditionsRes = {
discount_condition: DiscountCondition
}
/**
* @schema AdminDiscountsDeleteRes
* type: object
* required:
* - id
* - object
* - deleted
* properties:
* id:
* type: string
* description: The ID of the deleted Discount
* object:
* type: string
* description: The type of the object that was deleted.
* default: discount
* deleted:
* type: boolean
* description: Whether the discount was deleted successfully.
* default: true
*/
export type AdminDiscountsDeleteRes = DeleteResponse
/**
* @schema AdminDiscountConditionsDeleteRes
* type: object
* required:
* - id
* - object
* - deleted
* - discount
* properties:
* id:
* type: string
* description: The ID of the deleted Discount Condition
* object:
* type: string
* description: The type of the object that was deleted.
* default: discount-condition
* deleted:
* type: boolean
* description: Whether the discount condition was deleted successfully.
* default: true
* discount:
* description: The Discount to which the condition used to belong to.
* $ref: "#/components/schemas/Discount"
*/
export type AdminDiscountConditionsDeleteRes = DeleteResponse & {
discount: Discount
}
/**
* @schema AdminDiscountsListRes
* type: object
* description: The list of discounts with pagination fields.
* x-expanded-relations:
* field: discounts
* relations:
* - parent_discount
* - regions
* - rule
* - rule.conditions
* required:
* - discounts
* - count
* - offset
* - limit
* properties:
* discounts:
* type: array
* description: "The list of discounts."
* items:
* $ref: "#/components/schemas/Discount"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of discounts skipped when retrieving the discounts.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminDiscountsListRes = PaginatedResponse & {
discounts: Discount[]
}
export * from "./add-region"
export * from "./create-condition"
export * from "./create-discount"
export * from "./create-dynamic-code"
export * from "./delete-condition"
export * from "./delete-discount"
export * from "./delete-dynamic-code"
export * from "./get-condition"
export * from "./get-discount"
export * from "./get-discount-by-code"
export * from "./list-discounts"
export * from "./remove-region"
export * from "./update-condition"
export * from "./update-discount"
export * from "./add-resources-to-condition-batch"
export * from "./delete-resources-from-condition-batch"
@@ -1,241 +0,0 @@
import {
IsBoolean,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { Transform, Type } from "class-transformer"
import { AdminGetDiscountsDiscountRuleParams } from "../../../../types/discount"
import {
DateComparisonOperator,
extendedFindParamsMixin,
} from "../../../../types/common"
import { Request, Response } from "express"
import { DiscountService } from "../../../../services"
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
/**
* @oas [get] /admin/discounts
* operationId: "GetDiscounts"
* summary: "List Discounts"
* x-authenticated: true
* description: "Retrieve a list of Discounts. The discounts can be filtered by fields such as `rule` or `is_dynamic`. The discounts can also be paginated."
* parameters:
* - (query) q {string} term to search discounts' code field.
* - in: query
* name: rule
* description: Filter discounts by rule fields.
* schema:
* type: object
* properties:
* type:
* type: string
* enum: [fixed, percentage, free_shipping]
* description: "Filter discounts by type."
* allocation:
* type: string
* enum: [total, item]
* description: "Filter discounts by allocation type."
* - (query) is_dynamic {boolean} Filter discounts by whether they're dynamic or not.
* - (query) is_disabled {boolean} Filter discounts by whether they're disabled or not.
* - (query) limit=20 {number} The number of discounts to return
* - (query) offset=0 {number} The number of discounts to skip when retrieving the discounts.
* - (query) expand {string} Comma-separated relations that should be expanded in each returned discount.
* - (query) order {string} A discount field to sort-order the retrieved discounts by.
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* x-codegen:
* method: list
* queryParams: AdminGetDiscountsParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.list()
* .then(({ discounts, limit, offset, count }) => {
* console.log(discounts.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDiscounts } from "medusa-react"
*
* const Discounts = () => {
* const { discounts, isLoading } = useAdminDiscounts()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {discounts && !discounts.length && (
* <span>No customers</span>
* )}
* {discounts && discounts.length > 0 && (
* <ul>
* {discounts.map((discount) => (
* <li key={discount.id}>{discount.code}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default Discounts
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/discounts' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const discountService: DiscountService = req.scope.resolve("discountService")
const { filterableFields, listConfig } = req
const { skip, take } = listConfig
const [discounts, count] = await discountService.listAndCount(
filterableFields,
listConfig
)
res.status(200).json({
discounts,
count,
offset: skip,
limit: take,
})
}
/**
* Parameters used to filter and configure the pagination of the retrieved discounts.
*/
export class AdminGetDiscountsParams extends extendedFindParamsMixin({
limit: 20,
offset: 0,
}) {
/**
* Filter discounts by their associated rule.
*/
@ValidateNested()
@IsOptional()
@Type(() => AdminGetDiscountsDiscountRuleParams)
rule?: AdminGetDiscountsDiscountRuleParams
/**
* Search terms to search discounts' code fields.
*/
@IsString()
@IsOptional()
q?: string
/**
* Filter discounts by whether they're dynamic.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value))
is_dynamic?: boolean
/**
* Filter discounts by whether they're disabled.
*/
@IsBoolean()
@IsOptional()
@Transform(({ value }) => optionalBooleanMapper.get(value))
is_disabled?: boolean
/**
* Date filters to apply on the discounts' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
created_at?: DateComparisonOperator
/**
* Date filters to apply on the discounts' `updated_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
updated_at?: DateComparisonOperator
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
*/
@IsString()
@IsOptional()
order?: string
}
@@ -1,102 +0,0 @@
import { defaultAdminDiscountsFields, defaultAdminDiscountsRelations } from "."
import DiscountService from "../../../../services/discount"
import { EntityManager } from "typeorm"
/**
* @oas [delete] /admin/discounts/{id}/regions/{region_id}
* operationId: "DeleteDiscountsDiscountRegionsRegion"
* summary: "Remove Region"
* x-authenticated: true
* description: "Remove a Region from the list of Regions that a Discount can be used in. This does not delete a region, only the association between it and the discount."
* parameters:
* - (path) id=* {string} The ID of the Discount.
* - (path) region_id=* {string} The ID of the Region.
* x-codegen:
* method: removeRegion
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.removeRegion(discountId, regionId)
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDiscountRemoveRegion } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const deleteRegion = useAdminDiscountRemoveRegion(discountId)
* // ...
*
* const handleDelete = (regionId: string) => {
* deleteRegion.mutate(regionId, {
* onSuccess: ({ discount }) => {
* console.log(discount.regions)
* }
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/discounts/{id}/regions/{region_id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { discount_id, region_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.removeRegion(discount_id, region_id)
})
const discount = await discountService.retrieve(discount_id, {
select: defaultAdminDiscountsFields,
relations: defaultAdminDiscountsRelations,
})
res.status(200).json({ discount })
}
@@ -1,183 +0,0 @@
import { Request, Response } from "express"
import { AdminUpsertConditionsReq } from "../../../../types/discount"
import DiscountConditionService from "../../../../services/discount-condition"
import { DiscountService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
/**
* @oas [post] /admin/discounts/{discount_id}/conditions/{condition_id}
* operationId: "PostDiscountsDiscountConditionsCondition"
* summary: "Update a Condition"
* description: "Update a Discount Condition. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided, based on the type of discount condition.
* For example, if the discount condition's type is `products`, the `products` field should be provided in the request body."
* x-authenticated: true
* parameters:
* - (path) discount_id=* {string} The ID of the Discount.
* - (path) condition_id=* {string} The ID of the Discount Condition.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be included in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsDiscountConditionsCondition"
* x-codegen:
* method: updateCondition
* queryParams: AdminPostDiscountsDiscountConditionsConditionParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.updateCondition(discountId, conditionId, {
* products: [
* productId
* ]
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDiscountUpdateCondition } from "medusa-react"
*
* type Props = {
* discountId: string
* conditionId: string
* }
*
* const DiscountCondition = ({
* discountId,
* conditionId
* }: Props) => {
* const update = useAdminDiscountUpdateCondition(
* discountId,
* conditionId
* )
* // ...
*
* const handleUpdate = (
* products: string[]
* ) => {
* update.mutate({
* products
* }, {
* onSuccess: ({ discount }) => {
* console.log(discount.id)
* }
* })
* }
*
* // ...
* }
*
* export default DiscountCondition
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}/conditions/{condition}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "products": [
* "prod_01G1G5V2MBA328390B5AXJ610F"
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id, condition_id } = req.params
const conditionService: DiscountConditionService = req.scope.resolve(
"discountConditionService"
)
const condition = await conditionService.retrieve(condition_id)
const discountService: DiscountService = req.scope.resolve("discountService")
let discount = await discountService.retrieve(discount_id)
const updateObj = {
...(req.validatedBody as AdminPostDiscountsDiscountConditionsCondition),
rule_id: discount.rule_id,
id: condition.id,
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await conditionService
.withTransaction(transactionManager)
.upsertCondition(updateObj)
})
discount = await discountService.retrieve(discount.id, req.retrieveConfig)
res.status(200).json({ discount })
}
/**
* @schema AdminPostDiscountsDiscountConditionsCondition
* type: object
* properties:
* products:
* type: array
* description: list of product IDs if the condition's type is `products`.
* items:
* type: string
* product_types:
* type: array
* description: list of product type IDs if the condition's type is `product_types`.
* items:
* type: string
* product_collections:
* type: array
* description: list of product collection IDs if the condition's type is `product_collections`.
* items:
* type: string
* product_tags:
* type: array
* description: list of product tag IDs if the condition's type is `product_tags`
* items:
* type: string
* customer_groups:
* type: array
* description: list of customer group IDs if the condition's type is `customer_groups`.
* items:
* type: string
*/
// eslint-disable-next-line max-len
export class AdminPostDiscountsDiscountConditionsCondition extends AdminUpsertConditionsReq {}
// eslint-disable-next-line max-len
export class AdminPostDiscountsDiscountConditionsConditionParams extends FindParams {}
@@ -1,343 +0,0 @@
import {
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsPositive,
IsString,
ValidateNested,
} from "class-validator"
import { Request, Response } from "express"
import { AllocationType, DiscountConditionOperator } from "../../../../models"
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import DiscountService from "../../../../services/discount"
import { FindParams } from "../../../../types/common"
import { AdminUpsertConditionsReq } from "../../../../types/discount"
import { IsGreaterThan } from "../../../../utils/validators/greater-than"
import { IsISO8601Duration } from "../../../../utils/validators/iso8601-duration"
/**
* @oas [post] /admin/discounts/{id}
* operationId: "PostDiscountsDiscount"
* summary: "Update a Discount"
* description: "Update a Discount with a given set of rules that define how the Discount is applied."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Discount.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned discount.
* - (query) fields {string} Comma-separated fields that should be retrieved in the returned discount.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDiscountsDiscountReq"
* x-codegen:
* method: update
* queryParams: AdminPostDiscountsDiscountParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.discounts.update(discountId, {
* code: "TEST"
* })
* .then(({ discount }) => {
* console.log(discount.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateDiscount } from "medusa-react"
*
* type Props = {
* discountId: string
* }
*
* const Discount = ({ discountId }: Props) => {
* const updateDiscount = useAdminUpdateDiscount(discountId)
* // ...
*
* const handleUpdate = (isDisabled: boolean) => {
* updateDiscount.mutate({
* is_disabled: isDisabled,
* })
* }
*
* // ...
* }
*
* export default Discount
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/discounts/{id}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "code": "TEST"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Discounts
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDiscountsRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req: Request, res: Response) => {
const { discount_id } = req.params
const discountService: DiscountService = req.scope.resolve("discountService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await discountService
.withTransaction(transactionManager)
.update(discount_id, req.validatedBody as AdminPostDiscountsDiscountReq)
})
const discount = await discountService.retrieve(
discount_id,
req.retrieveConfig
)
res.status(200).json({ discount })
}
/**
* The attributes of the discount rule to update.
*/
export class AdminUpdateDiscountRule {
/**
* The discount rule's ID.
*/
@IsString()
@IsNotEmpty()
id: string
/**
* The discount rule's description.
*/
@IsString()
@IsOptional()
description?: string
/**
* The discount rule's value.
*/
@IsNumber()
@IsOptional()
value?: number
/**
* The discount rule's allocation.
*/
@IsOptional()
@IsEnum(AllocationType, {
message: `Invalid allocation type, must be one of "total" or "item"`,
})
allocation?: AllocationType
/**
* The discount rule's discount conditions.
*/
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdminUpsertCondition)
conditions?: AdminUpsertCondition[]
}
/**
* @schema AdminPostDiscountsDiscountReq
* type: object
* description: "The details of the discount to update."
* properties:
* code:
* type: string
* description: A unique code that will be used to redeem the discount
* rule:
* description: The discount rule that defines how discounts are calculated
* type: object
* required:
* - id
* properties:
* id:
* type: string
* description: "The ID of the Rule"
* description:
* type: string
* description: "A short description of the discount"
* value:
* type: number
* description: "The value that the discount represents. This will depend on the type of the discount."
* allocation:
* type: string
* description: >-
* The scope that the discount should apply to. `total` indicates that the discount should be applied on the cart total, and `item` indicates that the discount should be applied to each discountable item in the cart.
* enum: [total, item]
* conditions:
* type: array
* description: "A set of conditions that can be used to limit when the discount can be used. Only one of `products`, `product_types`, `product_collections`, `product_tags`, and `customer_groups` should be provided based on the discount condition's type."
* items:
* type: object
* required:
* - operator
* properties:
* id:
* type: string
* description: "The ID of the condition"
* operator:
* type: string
* description: >-
* Operator of the condition. `in` indicates that discountable resources are within the specified resources. `not_in` indicates that
* discountable resources are everything but the specified resources.
* enum: [in, not_in]
* products:
* type: array
* description: list of product IDs if the condition's type is `products`.
* items:
* type: string
* product_types:
* type: array
* description: list of product type IDs if the condition's type is `product_types`.
* items:
* type: string
* product_collections:
* type: array
* description: list of product collection IDs if the condition's type is `product_collections`.
* items:
* type: string
* product_tags:
* type: array
* description: list of product tag IDs if the condition's type is `product_tags`.
* items:
* type: string
* customer_groups:
* type: array
* description: list of customer group IDs if the condition's type is `customer_groups`.
* items:
* type: string
* is_disabled:
* type: boolean
* description: >-
* Whether the discount code is disabled on creation. If set to `true`, it will not be available for customers.
* starts_at:
* type: string
* format: date-time
* description: The date and time at which the discount should be available.
* ends_at:
* type: string
* format: date-time
* description: The date and time at which the discount should no longer be available.
* valid_duration:
* type: string
* description: The duration the discount runs between
* example: P3Y6M4DT12H30M5S
* usage_limit:
* type: number
* description: Maximum number of times the discount can be used
* regions:
* description: A list of region IDs representing the Regions in which the Discount can be used.
* type: array
* items:
* type: string
* metadata:
* description: An object containing metadata of the discount
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostDiscountsDiscountReq {
@IsString()
@IsOptional()
code?: string
@IsOptional()
@ValidateNested()
@Type(() => AdminUpdateDiscountRule)
rule?: AdminUpdateDiscountRule
@IsBoolean()
@IsOptional()
is_disabled?: boolean
@IsDate()
@IsOptional()
@Type(() => Date)
starts_at?: Date
@IsDate()
@IsOptional()
@IsGreaterThan("starts_at")
@Type(() => Date)
ends_at?: Date | null
@IsISO8601Duration()
@IsOptional()
valid_duration?: string | null
@IsNumber()
@IsOptional()
@IsPositive()
usage_limit?: number | null
@IsArray()
@IsOptional()
@IsString({ each: true })
regions?: string[]
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
/**
* The attributes to create or update in the discount condition.
*/
export class AdminUpsertCondition extends AdminUpsertConditionsReq {
/**
* The discount condition's ID.
*/
@IsString()
@IsOptional()
id?: string
/**
* The discount condition's operator.
*/
@IsString()
@IsOptional()
operator: DiscountConditionOperator
}
export class AdminPostDiscountsDiscountParams extends FindParams {}
@@ -1,388 +0,0 @@
import {
IsArray,
IsBoolean,
IsEmail,
IsEnum,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
defaultAdminDraftOrdersFields,
defaultAdminDraftOrdersRelations,
} from "."
import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { DraftOrder } from "../../../.."
import { CartService, DraftOrderService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { DraftOrderCreateProps } from "../../../../types/draft-orders"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /admin/draft-orders
* operationId: "PostDraftOrders"
* summary: "Create a Draft Order"
* description: "Create a Draft Order. A draft order is not transformed into an order until payment is captured."
* x-authenticated: true
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDraftOrdersReq"
* x-codegen:
* method: create
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.create({
* email: "user@example.com",
* region_id,
* items: [
* {
* quantity: 1
* }
* ],
* shipping_methods: [
* {
* option_id
* }
* ],
* })
* .then(({ draft_order }) => {
* console.log(draft_order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminCreateDraftOrder } from "medusa-react"
*
* type DraftOrderData = {
* email: string
* region_id: string
* items: {
* quantity: number,
* variant_id: string
* }[]
* shipping_methods: {
* option_id: string
* price: number
* }[]
* }
*
* const CreateDraftOrder = () => {
* const createDraftOrder = useAdminCreateDraftOrder()
* // ...
*
* const handleCreate = (data: DraftOrderData) => {
* createDraftOrder.mutate(data, {
* onSuccess: ({ draft_order }) => {
* console.log(draft_order.id)
* }
* })
* }
*
* // ...
* }
*
* export default CreateDraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/draft-orders' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "email": "user@example.com",
* "region_id": "{region_id}"
* "items": [
* {
* "quantity": 1
* }
* ],
* "shipping_methods": [
* {
* "option_id": "{option_id}"
* }
* ]
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(AdminPostDraftOrdersReq, req.body)
const { shipping_address, billing_address, ...rest } = validated
const draftOrderDataToCreate: DraftOrderCreateProps = { ...rest }
if (typeof shipping_address === "string") {
draftOrderDataToCreate.shipping_address_id = shipping_address
} else {
draftOrderDataToCreate.shipping_address = shipping_address
}
if (typeof billing_address === "string") {
draftOrderDataToCreate.billing_address_id = billing_address
} else {
draftOrderDataToCreate.billing_address = billing_address
}
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const manager: EntityManager = req.scope.resolve("manager")
let draftOrder: DraftOrder = await manager.transaction(
async (transactionManager) => {
return await draftOrderService
.withTransaction(transactionManager)
.create(draftOrderDataToCreate)
}
)
draftOrder = await draftOrderService.retrieve(draftOrder.id, {
relations: defaultAdminDraftOrdersRelations,
select: defaultAdminDraftOrdersFields,
})
const cartService: CartService = req.scope.resolve("cartService")
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieveWithTotals(draftOrder.cart_id, {
relations: defaultAdminDraftOrdersCartRelations,
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: cleanResponseData(draftOrder, []) })
}
enum Status {
open = "open",
completed = "completed",
}
/**
* @schema AdminPostDraftOrdersReq
* type: object
* description: "The details of the draft order to create."
* required:
* - email
* - region_id
* - shipping_methods
* properties:
* status:
* description: >-
* The status of the draft order. The draft order's default status is `open`. It's changed to `completed` when its payment is marked as paid.
* type: string
* enum: [open, completed]
* email:
* description: "The email of the customer of the draft order"
* type: string
* format: email
* billing_address:
* description: "The Address to be used for billing purposes."
* anyOf:
* - $ref: "#/components/schemas/AddressPayload"
* - type: string
* shipping_address:
* description: "The Address to be used for shipping purposes."
* anyOf:
* - $ref: "#/components/schemas/AddressPayload"
* - type: string
* items:
* description: The draft order's line items.
* type: array
* items:
* type: object
* required:
* - quantity
* properties:
* variant_id:
* description: The ID of the Product Variant associated with the line item. If the line item is custom, the `variant_id` should be omitted.
* type: string
* unit_price:
* description: The custom price of the line item. If a `variant_id` is supplied, the price provided here will override the variant's price.
* type: integer
* title:
* description: The title of the line item if `variant_id` is not provided.
* type: string
* quantity:
* description: The quantity of the line item.
* type: integer
* metadata:
* description: The optional key-value map with additional details about the line item.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
* region_id:
* description: The ID of the region for the draft order
* type: string
* discounts:
* description: The discounts to add to the draft order
* type: array
* items:
* type: object
* required:
* - code
* properties:
* code:
* description: The code of the discount to apply
* type: string
* customer_id:
* description: The ID of the customer this draft order is associated with.
* type: string
* no_notification_order:
* description: An optional flag passed to the resulting order that indicates whether the customer should receive notifications about order updates.
* type: boolean
* shipping_methods:
* description: The shipping methods for the draft order
* type: array
* items:
* type: object
* required:
* - option_id
* properties:
* option_id:
* description: The ID of the shipping option in use
* type: string
* data:
* description: The optional additional data needed for the shipping method
* type: object
* price:
* description: The price of the shipping method.
* type: integer
* metadata:
* description: The optional key-value map with additional details about the Draft Order.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostDraftOrdersReq {
@IsEnum(Status)
@IsOptional()
status?: string
@IsEmail()
email: string
@IsOptional()
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsArray()
@Type(() => Item)
@IsNotEmpty()
@ValidateNested({ each: true })
@IsOptional()
items?: Item[]
@IsString()
region_id: string
@IsArray()
@IsOptional()
@Type(() => Discount)
@ValidateNested({ each: true })
discounts?: Discount[]
@IsString()
@IsOptional()
customer_id?: string
@IsBoolean()
@IsOptional()
no_notification_order?: boolean
@IsArray()
@Type(() => ShippingMethod)
@IsNotEmpty()
@ValidateNested({ each: true })
shipping_methods: ShippingMethod[]
@IsObject()
@IsOptional()
metadata?: Record<string, unknown> = {}
}
class ShippingMethod {
@IsString()
option_id: string
@IsObject()
@IsOptional()
data?: Record<string, unknown> = {}
@IsNumber()
@IsOptional()
price?: number
}
class Discount {
@IsString()
code: string
}
class Item {
@IsString()
@IsOptional()
title?: string
@IsNumber()
@IsOptional()
unit_price?: number
@IsString()
@IsOptional()
variant_id?: string
@IsNumber()
quantity: number
@IsObject()
@IsOptional()
metadata?: Record<string, unknown> = {}
}
@@ -1,237 +0,0 @@
import {
IsBoolean,
IsInt,
IsObject,
IsOptional,
IsString,
} from "class-validator"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
defaultAdminDraftOrdersFields,
} from "."
import {
CartService,
DraftOrderService,
LineItemService,
} from "../../../../services"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import { validator } from "../../../../utils/validator"
/**
* @oas [post] /admin/draft-orders/{id}/line-items
* operationId: "PostDraftOrdersDraftOrderLineItems"
* summary: "Create a Line Item"
* description: "Create a Line Item in the Draft Order."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Draft Order.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDraftOrdersDraftOrderLineItemsReq"
* x-codegen:
* method: addLineItem
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.addLineItem(draftOrderId, {
* quantity: 1
* })
* .then(({ draft_order }) => {
* console.log(draft_order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDraftOrderAddLineItem } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const addLineItem = useAdminDraftOrderAddLineItem(
* draftOrderId
* )
* // ...
*
* const handleAdd = (quantity: number) => {
* addLineItem.mutate({
* quantity,
* }, {
* onSuccess: ({ draft_order }) => {
* console.log(draft_order.cart)
* }
* })
* }
*
* // ...
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/draft-orders/{id}/line-items' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "quantity": 1
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const validated = await validator(
AdminPostDraftOrdersDraftOrderLineItemsReq,
req.body
)
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const cartService: CartService = req.scope.resolve("cartService")
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
const entityManager: EntityManager = req.scope.resolve("manager")
await entityManager.transaction(async (manager) => {
const draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id, {
select: defaultAdminDraftOrdersFields,
relations: ["cart"],
})
if (draftOrder.status === "completed") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
if (validated.variant_id) {
const line = await lineItemService
.withTransaction(manager)
.generate(
validated.variant_id,
draftOrder.cart.region_id,
validated.quantity,
{
metadata: validated.metadata,
unit_price: validated.unit_price,
}
)
await cartService
.withTransaction(manager)
.addOrUpdateLineItems(draftOrder.cart_id, line, {
validateSalesChannels: false,
})
} else {
// custom line items can be added to a draft order
await lineItemService.withTransaction(manager).create({
cart_id: draftOrder.cart_id,
has_shipping: true,
title: validated.title,
allow_discounts: false,
unit_price: validated.unit_price || 0,
quantity: validated.quantity,
metadata: validated.metadata,
})
}
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieveWithTotals(draftOrder.cart_id, {
relations: defaultAdminDraftOrdersCartRelations,
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({
draft_order: cleanResponseData(draftOrder, []),
})
})
}
/**
* @schema AdminPostDraftOrdersDraftOrderLineItemsReq
* type: object
* description: "The details of the line item to create."
* required:
* - quantity
* properties:
* variant_id:
* description: The ID of the Product Variant associated with the line item. If the line item is custom, the `variant_id` should be omitted.
* type: string
* unit_price:
* description: The custom price of the line item. If a `variant_id` is supplied, the price provided here will override the variant's price.
* type: integer
* title:
* description: The title of the line item if `variant_id` is not provided.
* type: string
* default: "Custom item"
* quantity:
* description: The quantity of the line item.
* type: integer
* metadata:
* description: The optional key-value map with additional details about the Line Item.
* type: object
* externalDocs:
* description: "Learn about the metadata attribute, and how to delete and update it."
* url: "https://docs.medusajs.com/development/entities/overview#metadata-attribute"
*/
export class AdminPostDraftOrdersDraftOrderLineItemsReq {
@IsString()
@IsOptional()
title?: string = "Custom item"
@IsInt()
@IsOptional()
unit_price?: number
@IsString()
@IsOptional()
variant_id?: string
@IsInt()
quantity: number
@IsObject()
@IsOptional()
metadata?: Record<string, unknown> = {}
}
@@ -1,102 +0,0 @@
import { DraftOrderService } from "../../../../services"
import { EntityManager } from "typeorm"
/**
* @oas [delete] /admin/draft-orders/{id}
* operationId: DeleteDraftOrdersDraftOrder
* summary: Delete a Draft Order
* description: "Delete a Draft Order."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Draft Order.
* x-codegen:
* method: delete
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.delete(draftOrderId)
* .then(({ id, object, deleted }) => {
* console.log(id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDeleteDraftOrder } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const deleteDraftOrder = useAdminDeleteDraftOrder(
* draftOrderId
* )
* // ...
*
* const handleDelete = () => {
* deleteDraftOrder.mutate(void 0, {
* onSuccess: ({ id, object, deleted }) => {
* console.log(id)
* }
* })
* }
*
* // ...
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/draft-orders/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersDeleteRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
return await draftOrderService
.withTransaction(transactionManager)
.delete(id)
})
res.json({
id,
object: "draft-order",
deleted: true,
})
}
@@ -1,130 +0,0 @@
import { CartService, DraftOrderService } from "../../../../services"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
defaultAdminDraftOrdersFields,
} from "."
import { DraftOrder } from "../../../.."
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [delete] /admin/draft-orders/{id}/line-items/{line_id}
* operationId: DeleteDraftOrdersDraftOrderLineItemsItem
* summary: Delete a Line Item
* description: "Delete a Line Item from a Draft Order."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Draft Order.
* - (path) line_id=* {string} The ID of the line item.
* x-codegen:
* method: removeLineItem
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.removeLineItem(draftOrderId, itemId)
* .then(({ draft_order }) => {
* console.log(draft_order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDraftOrderRemoveLineItem } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const deleteLineItem = useAdminDraftOrderRemoveLineItem(
* draftOrderId
* )
* // ...
*
* const handleDelete = (itemId: string) => {
* deleteLineItem.mutate(itemId, {
* onSuccess: ({ draft_order }) => {
* console.log(draft_order.cart)
* }
* })
* }
*
* // ...
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X DELETE '{backend_url}/admin/draft-orders/{id}/line-items/{line_id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id, line_id } = req.params
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const cartService: CartService = req.scope.resolve("cartService")
const entityManager: EntityManager = req.scope.resolve("manager")
await entityManager.transaction(async (manager) => {
const draftOrder: DraftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id, { select: defaultAdminDraftOrdersFields })
if (draftOrder.status === "completed") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
await cartService
.withTransaction(manager)
.removeLineItem(draftOrder.cart_id, line_id)
draftOrder.cart = await cartService
.withTransaction(manager)
.retrieveWithTotals(draftOrder.cart_id, {
relations: defaultAdminDraftOrdersCartRelations,
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({
draft_order: cleanResponseData(draftOrder, []),
})
})
}
@@ -1,111 +0,0 @@
import { CartService, DraftOrderService } from "../../../../services"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
defaultAdminDraftOrdersFields,
defaultAdminDraftOrdersRelations,
} from "."
import { DraftOrder } from "../../../.."
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /admin/draft-orders/{id}
* operationId: "GetDraftOrdersDraftOrder"
* summary: "Get a Draft Order"
* description: "Retrieve a Draft Order's details."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Draft Order.
* x-codegen:
* method: retrieve
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.retrieve(draftOrderId)
* .then(({ draft_order }) => {
* console.log(draft_order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDraftOrder } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const {
* draft_order,
* isLoading,
* } = useAdminDraftOrder(draftOrderId)
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {draft_order && <span>{draft_order.display_id}</span>}
*
* </div>
* )
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/draft-orders/{id}' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const cartService: CartService = req.scope.resolve("cartService")
const draftOrder: DraftOrder = await draftOrderService.retrieve(id, {
select: defaultAdminDraftOrdersFields,
relations: defaultAdminDraftOrdersRelations,
})
draftOrder.cart = await cartService.retrieveWithTotals(draftOrder.cart_id, {
relations: defaultAdminDraftOrdersCartRelations,
select: defaultAdminDraftOrdersCartFields,
})
res.json({
draft_order: cleanResponseData(draftOrder, []),
})
}
@@ -1,259 +0,0 @@
import { Router } from "express"
import { Cart, DraftOrder, Order } from "../../../.."
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import { AdminGetDraftOrdersParams } from "./list-draft-orders"
const route = Router()
export default (app) => {
app.use("/draft-orders", route)
route.get(
"/",
transformQuery(AdminGetDraftOrdersParams, {
defaultFields: defaultAdminDraftOrdersFields,
defaultRelations: defaultAdminDraftOrdersRelations,
isList: true,
}),
middlewares.wrap(require("./list-draft-orders").default)
)
route.get("/:id", middlewares.wrap(require("./get-draft-order").default))
route.post("/", middlewares.wrap(require("./create-draft-order").default))
route.post("/:id", middlewares.wrap(require("./update-draft-order").default))
route.delete(
"/:id",
middlewares.wrap(require("./delete-draft-order").default)
)
route.delete(
"/:id/line-items/:line_id",
middlewares.wrap(require("./delete-line-item").default)
)
route.post(
"/:id/line-items",
middlewares.wrap(require("./create-line-item").default)
)
route.post(
"/:id/line-items/:line_id",
middlewares.wrap(require("./update-line-item").default)
)
route.post("/", middlewares.wrap(require("./create-draft-order").default))
route.post(
"/:id/pay",
middlewares.wrap(require("./register-payment").default)
)
return app
}
export const defaultAdminDraftOrdersRelations = [
"order",
"cart",
"cart.items",
"cart.items.adjustments",
]
export const defaultAdminDraftOrdersCartRelations = [
"region",
"items",
"items.adjustments",
"payment",
"shipping_address",
"shipping_address.country",
"billing_address",
"billing_address.country",
"region.payment_providers",
"shipping_methods",
"payment_sessions",
"shipping_methods.shipping_option",
"discounts",
"customer",
"discounts.rule",
]
export const defaultAdminDraftOrdersCartFields: (keyof Cart)[] = [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"gift_card_total",
"total",
]
export const defaultAdminDraftOrdersFields: (keyof DraftOrder)[] = [
"id",
"status",
"display_id",
"cart_id",
"order_id",
"canceled_at",
"created_at",
"updated_at",
"metadata",
"no_notification_order",
]
/**
* @schema AdminPostDraftOrdersDraftOrderRegisterPaymentRes
* type: object
* description: "The order's details."
* required:
* - order
* properties:
* order:
* description: Order's details.
* $ref: "#/components/schemas/Order"
*/
export type AdminPostDraftOrdersDraftOrderRegisterPaymentRes = {
order: Order
}
/**
* @schema AdminDraftOrdersRes
* type: object
* description: "The list of draft orders."
* x-expanded-relations:
* field: draft_order
* relations:
* - order
* - cart
* - cart.items
* - cart.items.adjustments
* - cart.billing_address
* - cart.customer
* - cart.discounts
* - cart.discounts.rule
* - cart.items
* - cart.items.adjustments
* - cart.payment
* - cart.payment_sessions
* - cart.region
* - cart.region.payment_providers
* - cart.shipping_address
* - cart.shipping_methods
* - cart.shipping_methods.shipping_option
* eager:
* - cart.region.fulfillment_providers
* - cart.region.payment_providers
* - cart.shipping_methods.shipping_option
* implicit:
* - cart.discounts
* - cart.discounts.rule
* - cart.gift_cards
* - cart.items
* - cart.items.adjustments
* - cart.items.tax_lines
* - cart.items.variant
* - cart.items.variant.product
* - cart.items.variant.product.profiles
* - cart.region
* - cart.region.tax_rates
* - cart.shipping_address
* - cart.shipping_methods
* - cart.shipping_methods.tax_lines
* totals:
* - cart.discount_total
* - cart.gift_card_tax_total
* - cart.gift_card_total
* - cart.item_tax_total
* - cart.refundable_amount
* - cart.refunded_total
* - cart.shipping_tax_total
* - cart.shipping_total
* - cart.subtotal
* - cart.tax_total
* - cart.total
* - cart.items.discount_total
* - cart.items.gift_card_total
* - cart.items.original_tax_total
* - cart.items.original_total
* - cart.items.refundable
* - cart.items.subtotal
* - cart.items.tax_total
* - cart.items.total
* required:
* - draft_order
* properties:
* draft_order:
* description: Draft order's details.
* $ref: "#/components/schemas/DraftOrder"
*/
export type AdminDraftOrdersRes = {
draft_order: DraftOrder
}
/**
* @schema AdminDraftOrdersDeleteRes
* type: object
* required:
* - id
* - object
* - deleted
* properties:
* id:
* type: string
* description: The ID of the deleted Draft Order.
* object:
* type: string
* description: The type of the object that was deleted.
* default: draft-order
* deleted:
* type: boolean
* description: Whether the draft order was deleted successfully.
* default: true
*/
export type AdminDraftOrdersDeleteRes = DeleteResponse
/**
* @schema AdminDraftOrdersListRes
* description: "The list of draft orders with pagination fields."
* type: object
* x-expanded-relations:
* field: draft_orders
* relations:
* - order
* - cart
* - cart.items
* - cart.items.adjustments
* required:
* - draft_orders
* - count
* - offset
* - limit
* properties:
* draft_orders:
* type: array
* description: An array of draft order's details.
* items:
* $ref: "#/components/schemas/DraftOrder"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of draft orders skipped when retrieving the draft orders.
* limit:
* type: integer
* description: The number of items per page
*/
export type AdminDraftOrdersListRes = PaginatedResponse & {
draft_orders: DraftOrder[]
}
export * from "./create-draft-order"
export * from "./create-line-item"
export * from "./delete-draft-order"
export * from "./delete-line-item"
export * from "./get-draft-order"
export * from "./list-draft-orders"
export * from "./register-payment"
export * from "./update-draft-order"
export * from "./update-line-item"
@@ -1,228 +0,0 @@
import { Type } from "class-transformer"
import {
IsArray,
IsEnum,
IsNumber,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { DraftOrderStatus } from "../../../../models"
import { DraftOrderService } from "../../../../services"
import {
DateComparisonOperator,
extendedFindParamsMixin,
} from "../../../../types/common"
import { DraftOrderStatusValue } from "../../../../types/draft-orders"
/**
* @oas [get] /admin/draft-orders
* operationId: "GetDraftOrders"
* summary: "List Draft Orders"
* description: "Retrieve an list of Draft Orders. The draft orders can be filtered by fields such as `q`. The draft orders can also paginated."
* x-authenticated: true
* parameters:
* - (query) offset=0 {number} The number of draft orders to skip when retrieving the draft orders.
* - (query) limit=50 {number} Limit the number of draft orders returned.
* - (query) q {string} a term to search draft orders' display IDs and emails in the draft order's cart
* - (query) order {string} Field to sort retrieved draft orders by.
* - (query) expand {string} A comma-separated list of fields to expand.
* - (query) fields {string} A comma-separated list of fields to include in the response.
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: status
* style: form
* explode: false
* description: Filter by status
* schema:
* type: array
* items:
* type: string
* enum: [open, completed]
* x-codegen:
* method: list
* queryParams: AdminGetDraftOrdersParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.list()
* .then(({ draft_orders, limit, offset, count }) => {
* console.log(draft_orders.length);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDraftOrders } from "medusa-react"
*
* const DraftOrders = () => {
* const { draft_orders, isLoading } = useAdminDraftOrders()
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {draft_orders && !draft_orders.length && (
* <span>No Draft Orders</span>
* )}
* {draft_orders && draft_orders.length > 0 && (
* <ul>
* {draft_orders.map((order) => (
* <li key={order.id}>{order.display_id}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default DraftOrders
* - lang: Shell
* label: cURL
* source: |
* curl '{backend_url}/admin/draft-orders' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersListRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const { skip, take } = req.listConfig
const [draftOrders, count] = await draftOrderService.listAndCount(
req.filterableFields,
req.listConfig
)
res.json({
draft_orders: draftOrders,
count,
offset: skip,
limit: take,
})
}
/**
* Parameters used to filter and configure the pagination of the retrieved draft orders.
*/
export class AdminGetDraftOrdersParams extends extendedFindParamsMixin({
limit: 50,
}) {
/**
* Search term to search draft orders by their display IDs and emails.
*/
@IsString()
@IsOptional()
q?: string
/**
* {@inheritDoc FindParams.expand}
*/
@IsOptional()
@IsString()
expand?: string
/**
* {@inheritDoc FindPaginationParams.limit}
* @defaultValue 50
*/
@IsOptional()
@IsNumber()
fields?: string
/**
* Statuses to filter draft orders by.
*/
@IsArray()
@IsEnum(DraftOrderStatus, { each: true })
@IsOptional()
status?: DraftOrderStatusValue[]
/**
* Date filters to apply on the draft orders' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
created_at?: DateComparisonOperator
/**
* Date filters to apply on the draft orders' `updated_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
updated_at?: DateComparisonOperator
}
@@ -1,191 +0,0 @@
import {
CartService,
DraftOrderService,
OrderService,
PaymentProviderService,
ProductVariantInventoryService,
} from "../../../../services"
import {
defaultAdminOrdersFields as defaultOrderFields,
defaultAdminOrdersRelations as defaultOrderRelations,
} from "../../../../types/orders"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Order } from "../../../../models"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import { promiseAll } from "@medusajs/utils"
/**
* @oas [post] /admin/draft-orders/{id}/pay
* summary: "Mark Paid"
* operationId: "PostDraftOrdersDraftOrderRegisterPayment"
* description: "Capture the draft order's payment. This will also set the draft order's status to `completed` and create an Order from the draft order. The payment is captured through Medusa's system payment,
* which is manual payment that isn't integrated with any third-party payment provider. It is assumed that the payment capturing is handled manually by the admin."
* x-authenticated: true
* parameters:
* - (path) id=* {String} The Draft Order ID.
* x-codegen:
* method: markPaid
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.markPaid(draftOrderId)
* .then(({ order }) => {
* console.log(order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminDraftOrderRegisterPayment } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const registerPayment = useAdminDraftOrderRegisterPayment(
* draftOrderId
* )
* // ...
*
* const handlePayment = () => {
* registerPayment.mutate(void 0, {
* onSuccess: ({ order }) => {
* console.log(order.id)
* }
* })
* }
*
* // ...
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/draft-orders/{id}/pay' \
* -H 'x-medusa-access-token: {api_token}'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDraftOrdersDraftOrderRegisterPaymentRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const paymentProviderService: PaymentProviderService = req.scope.resolve(
"paymentProviderService"
)
const orderService: OrderService = req.scope.resolve("orderService")
const inventoryService: OrderService = req.scope.resolve("inventoryService")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const entityManager: EntityManager = req.scope.resolve("manager")
const order = await entityManager.transaction(async (manager) => {
const draftOrderServiceTx = draftOrderService.withTransaction(manager)
const orderServiceTx = orderService.withTransaction(manager)
const cartServiceTx = cartService.withTransaction(manager)
const draftOrder = await draftOrderServiceTx.retrieve(id)
const cart = await cartServiceTx.retrieveWithTotals(draftOrder.cart_id)
await paymentProviderService
.withTransaction(manager)
.createSession("system", cart)
await cartServiceTx.setPaymentSession(cart.id, "system")
await cartServiceTx.createTaxLines(cart.id)
await cartServiceTx.authorizePayment(cart.id)
let order = await orderServiceTx.createFromCart(cart.id)
await draftOrderServiceTx.registerCartCompletion(draftOrder.id, order.id)
await orderServiceTx.capturePayment(order.id)
order = await orderService
.withTransaction(manager)
.retrieveWithTotals(order.id, {
relations: defaultOrderRelations,
select: defaultOrderFields,
})
return order
})
res.status(200).json({ order: cleanResponseData(order, []) })
}
export const reserveQuantityForDraftOrder = async (
order: Order,
context: {
productVariantInventoryService: ProductVariantInventoryService
locationId?: string
}
) => {
const { productVariantInventoryService, locationId } = context
await promiseAll(
order.items.map(async (item) => {
if (item.variant_id) {
const inventoryConfirmed =
await productVariantInventoryService.confirmInventory(
item.variant_id,
item.quantity,
{ salesChannelId: order.sales_channel_id }
)
if (!inventoryConfirmed) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Variant with id: ${item.variant_id} does not have the required inventory`,
MedusaError.Codes.INSUFFICIENT_INVENTORY
)
}
await productVariantInventoryService.reserveQuantity(
item.variant_id,
item.quantity,
{
lineItemId: item.id,
salesChannelId: order.sales_channel_id,
}
)
}
})
)
}
@@ -1,260 +0,0 @@
import { Type } from "class-transformer"
import {
IsArray,
IsBoolean,
IsEmail,
IsOptional,
IsString,
ValidateNested,
} from "class-validator"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import {
defaultAdminDraftOrdersCartFields,
defaultAdminDraftOrdersCartRelations,
} from "."
import { DraftOrderStatus } from "../../../../models"
import { CartService, DraftOrderService } from "../../../../services"
import { CartUpdateProps } from "../../../../types/cart"
import { AddressPayload } from "../../../../types/common"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /admin/draft-orders/{id}
* operationId: PostDraftOrdersDraftOrder
* summary: Update a Draft Order
* description: "Update a Draft Order's details."
* x-authenticated: true
* parameters:
* - (path) id=* {string} The ID of the Draft Order.
* requestBody:
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminPostDraftOrdersDraftOrderReq"
* x-codegen:
* method: update
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.draftOrders.update(draftOrderId, {
* email: "user@example.com"
* })
* .then(({ draft_order }) => {
* console.log(draft_order.id);
* })
* - lang: tsx
* label: Medusa React
* source: |
* import React from "react"
* import { useAdminUpdateDraftOrder } from "medusa-react"
*
* type Props = {
* draftOrderId: string
* }
*
* const DraftOrder = ({ draftOrderId }: Props) => {
* const updateDraftOrder = useAdminUpdateDraftOrder(
* draftOrderId
* )
* // ...
*
* const handleUpdate = (email: string) => {
* updateDraftOrder.mutate({
* email,
* }, {
* onSuccess: ({ draft_order }) => {
* console.log(draft_order.id)
* }
* })
* }
*
* // ...
* }
*
* export default DraftOrder
* - lang: Shell
* label: cURL
* source: |
* curl -X POST '{backend_url}/admin/draft-orders/{id}' \
* -H 'x-medusa-access-token: {api_token}' \
* -H 'Content-Type: application/json' \
* --data-raw '{
* "email": "user@example.com"
* }'
* security:
* - api_token: []
* - cookie_auth: []
* - jwt_token: []
* tags:
* - Draft Orders
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/AdminDraftOrdersRes"
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { id } = req.params
const validated = await validator(AdminPostDraftOrdersDraftOrderReq, req.body)
const draftOrderService: DraftOrderService =
req.scope.resolve("draftOrderService")
const cartService: CartService = req.scope.resolve("cartService")
const draftOrder = await draftOrderService.retrieve(id)
if (draftOrder.status === DraftOrderStatus.COMPLETED) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You are only allowed to update open draft orders"
)
}
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
if (validated.no_notification_order !== undefined) {
await draftOrderService
.withTransaction(transactionManager)
.update(draftOrder.id, {
no_notification_order: validated.no_notification_order,
})
delete validated.no_notification_order
}
const { shipping_address, billing_address, ...rest } = validated
const cartDataToUpdate: CartUpdateProps = { ...rest }
if (typeof shipping_address === "string") {
cartDataToUpdate.shipping_address_id = shipping_address
} else {
cartDataToUpdate.shipping_address = shipping_address
}
if (typeof billing_address === "string") {
cartDataToUpdate.billing_address_id = billing_address
} else {
cartDataToUpdate.billing_address = billing_address
}
await cartService.update(draftOrder.cart_id, cartDataToUpdate)
})
draftOrder.cart = await cartService.retrieveWithTotals(draftOrder.cart_id, {
relations: defaultAdminDraftOrdersCartRelations,
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: cleanResponseData(draftOrder, []) })
}
/**
* @schema AdminPostDraftOrdersDraftOrderReq
* type: object
* description: "The details of the draft order to update."
* properties:
* region_id:
* type: string
* description: The ID of the Region to create the Draft Order in.
* country_code:
* type: string
* description: "The 2 character ISO code for the Country."
* externalDocs:
* url: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
* description: See a list of codes.
* email:
* type: string
* description: "An email to be used in the Draft Order."
* format: email
* billing_address:
* description: "The Address to be used for billing purposes."
* anyOf:
* - $ref: "#/components/schemas/AddressPayload"
* - type: string
* shipping_address:
* description: "The Address to be used for shipping purposes."
* anyOf:
* - $ref: "#/components/schemas/AddressPayload"
* - type: string
* discounts:
* description: "An array of Discount codes to add to the Draft Order."
* type: array
* items:
* type: object
* required:
* - code
* properties:
* code:
* description: "The code that a Discount is identifed by."
* type: string
* no_notification_order:
* description: "An optional flag passed to the resulting order that indicates whether the customer should receive notifications about order updates."
* type: boolean
* customer_id:
* description: "The ID of the customer this draft order is associated with."
* type: string
*/
export class AdminPostDraftOrdersDraftOrderReq {
@IsString()
@IsOptional()
region_id?: string
@IsString()
@IsOptional()
country_code?: string
@IsEmail()
@IsOptional()
email?: string
@IsOptional()
@IsType([AddressPayload, String])
billing_address?: AddressPayload | string
@IsOptional()
@IsType([AddressPayload, String])
shipping_address?: AddressPayload | string
@IsArray()
@IsOptional()
@Type(() => Discount)
@ValidateNested({ each: true })
discounts?: Discount[]
@IsString()
@IsOptional()
customer_id?: string
@IsBoolean()
@IsOptional()
no_notification_order?: boolean
}
class Discount {
@IsString()
code: string
}
Loaded 100 of 1452 files, more files were not shown because too many files have changed in this diff. Show more