chore(medusa): cleanup medusa package (#7206)
This commit is contained in:
1 parent
8b61dccd0f
commit
71f4f46cb9
1452 files changed
+4737
-234780
No files matched your search
@@ -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 }
|
||||
-51
@@ -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 }
|
||||
-46
@@ -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 }
|
||||
-54
@@ -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
|
||||
}
|
||||
-65
@@ -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 })
|
||||
}
|
||||
-182
@@ -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
Reference in new issue
Block a user