feat: new tax api (#979)
* feat: add tax calculation strategy (#885) * feat: add tax calculation strategy * fix: adds strategy loader * fix: eslint ignore * chore: cleanup * fix: allow plugin overwrites * fix: allow plugin overwrites * fix: fake region * Update packages/medusa/src/loaders/strategies.ts Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> * feat: adds tax related db entities + tax provider (#896) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: pr comments * fix: unit test * feat: totals service to ts (#911) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: adds TotalsServiceProps * feat: adds integration tests for automatic tax calculation + shipping tax rates (#945) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: integration test helpers * fix: adds factories + tests automatic tax rates * fix: remove verbose * fix: adds TotalsServiceProps * fix: add shipping tax lines * fix: add migration for shipping taxes * fix: integration tests for shipping taxes * fix: integration tests for shipping taxes * fix: jsdoc types * Feat/manual taxes (#950) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: integration test helpers * fix: adds factories + tests automatic tax rates * fix: remove verbose * fix: adds TotalsServiceProps * fix: add shipping tax lines * fix: add migration for shipping taxes * fix: integration tests for shipping taxes * fix: integration tests for shipping taxes * fix: add integration tests for manual taxes * fix: cart service - cleanup jsdoc * feat: add /carts/id/taxes to manually calculate taxes * feat: add integration tests for order tax calculations * fix: unit tests * fix: merge * fix: rm verbose * fix: unit tests * fix: object -> cartOrOrder * fix: rounding * Feat/complete order w tax lines (#951) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: integration test helpers * fix: adds factories + tests automatic tax rates * fix: remove verbose * fix: adds TotalsServiceProps * fix: add shipping tax lines * fix: add migration for shipping taxes * fix: integration tests for shipping taxes * fix: integration tests for shipping taxes * fix: add integration tests for manual taxes * fix: cart service - cleanup jsdoc * feat: add /carts/id/taxes to manually calculate taxes * feat: add integration tests for order tax calculations * feat: adds cart completion strategy + create order w. tax lines * fix: unit tests * fix: merge * fix: rm verbose * fix: unit tests * fix: unit tests * fix: unit tests * fix: ensure calculation for list orders * fix: unit tests * fix: integration tests * fix: adds cart order type gaurds * Docs/tax api (#954) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: integration test helpers * fix: adds factories + tests automatic tax rates * fix: remove verbose * fix: adds TotalsServiceProps * fix: add shipping tax lines * fix: add migration for shipping taxes * fix: integration tests for shipping taxes * fix: integration tests for shipping taxes * fix: add integration tests for manual taxes * fix: cart service - cleanup jsdoc * feat: add /carts/id/taxes to manually calculate taxes * feat: add integration tests for order tax calculations * feat: adds cart completion strategy + create order w. tax lines * fix: unit tests * fix: merge * fix: rm verbose * fix: unit tests * fix: unit tests * fix: unit tests * fix: ensure calculation for list orders * fix: unit tests * fix: integration tests * docs: documents tax related methods and types * fix: require either item_id or shipping_method_id * feat: product type tax rate (#969) * feat: adds tax related db entities + tax provider * fix: add tax provider tests * fix: add tax service unit tests * fix: tests + migrations * feat: totals service to ts * fix: remove totals.js * fix: add shipping methods * fix: add inherited tax lines * chore: rm tax-line repo * fix: test * fix: tests * fix: tests * fix: unit test * fix: integration test helpers * fix: adds factories + tests automatic tax rates * fix: remove verbose * fix: adds TotalsServiceProps * fix: add shipping tax lines * fix: add migration for shipping taxes * fix: integration tests for shipping taxes * fix: integration tests for shipping taxes * fix: add integration tests for manual taxes * fix: cart service - cleanup jsdoc * feat: add /carts/id/taxes to manually calculate taxes * feat: add integration tests for order tax calculations * feat: adds cart completion strategy + create order w. tax lines * fix: unit tests * fix: merge * fix: rm verbose * fix: unit tests * fix: unit tests * fix: unit tests * fix: ensure calculation for list orders * fix: unit tests * fix: integration tests * docs: documents tax related methods and types * fix: require either item_id or shipping_method_id * feat: adds returns tests for new tax system * feat: adds return lines + integration tests for swaps * feat: return integration tests * feat: adds product type tax rates * feat: add tax management endpoints * fix: create single migration * fix: adds tax rates to js client * fix: strats * Fix/plugin tests (#998) * plugin testing setup * fix: test sendgrid plugin * fix: test sendgrid plugin * chore: clean * chore: clean * fix: clean up tests * fix: remove dirty import * fix: sendgrid + brightpearl * fix: plugin integration tests * fix: klarna * fix: shipping method tax * fix: remove taxrates * fix: unit tests * fix: integration * fix: integration * fix: plugins tests * fix: ignore plugins * fix: tests * fix: taxes (#1017) * fix: taxes * fix: taxes * fix: faulty ref * fix: create tax-lines with claim items * fix: snapshot tax-liens * fix: allows integration test teardown to force deleting tables * fix: tests * fix: merge * fix: adds tax-rates to client * fix: adds tax-rates to medusa-react * fix: tests * fix: tests * fix: add product types * fix: adds tax provider endpoint + cascaded deletes on tax rate relations * fix: move errors to service layer * fix: cleanup api * fix: unit tests * fix: error handler in base-service * fix: Add order region to swap on createFulfillment (#1110) Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oliver Windall Juhl
parent
694e2df20f
commit
c56660fca9
@@ -35,9 +35,11 @@ export * from "./routes/admin/return-reasons"
|
||||
export * from "./routes/admin/swaps"
|
||||
export * from "./routes/admin/uploads"
|
||||
export * from "./routes/admin/returns"
|
||||
export * from "./routes/admin/tax-rates"
|
||||
export * from "./routes/admin/shipping-options"
|
||||
export * from "./routes/admin/regions"
|
||||
export * from "./routes/admin/product-tags"
|
||||
export * from "./routes/admin/product-types"
|
||||
|
||||
// Store
|
||||
export * from "./routes/store/auth"
|
||||
|
||||
@@ -25,6 +25,8 @@ import collectionRoutes from "./collections"
|
||||
import productTagRoutes from "./product-tags"
|
||||
import notificationRoutes from "./notifications"
|
||||
import noteRoutes from "./notes"
|
||||
import taxRateRoutes from "./tax-rates"
|
||||
import productTypesRoutes from "./product-types"
|
||||
import customerGroupRoutes from "./customer-groups"
|
||||
|
||||
const route = Router()
|
||||
@@ -79,8 +81,10 @@ export default (app, container, config) => {
|
||||
notificationRoutes(route)
|
||||
returnReasonRoutes(route)
|
||||
productTagRoutes(route)
|
||||
productTypesRoutes(route)
|
||||
noteRoutes(route)
|
||||
inviteRoutes(route)
|
||||
taxRateRoutes(route)
|
||||
customerGroupRoutes(route)
|
||||
|
||||
return app
|
||||
|
||||
@@ -15,6 +15,8 @@ const defaultRelations = [
|
||||
"fulfillments.tracking_links",
|
||||
"fulfillments.items",
|
||||
"returns",
|
||||
"returns.shipping_method",
|
||||
"returns.shipping_method.tax_lines",
|
||||
"returns.items",
|
||||
"returns.items.reason",
|
||||
"gift_cards",
|
||||
|
||||
@@ -169,7 +169,15 @@ export default async (req, res) => {
|
||||
const order = await orderService
|
||||
.withTransaction(manager)
|
||||
.retrieve(id, {
|
||||
relations: ["items", "discounts", "discounts.rule"],
|
||||
relations: [
|
||||
"customer",
|
||||
"shipping_address",
|
||||
"region",
|
||||
"items",
|
||||
"items.tax_lines",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
],
|
||||
})
|
||||
|
||||
await claimService.withTransaction(manager).create({
|
||||
|
||||
@@ -81,7 +81,7 @@ export class AdminPostOrdersOrderSwapsSwapShipmentsReq {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
tracking_numbers?: string[]
|
||||
tracking_numbers?: string[] = []
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -150,7 +150,13 @@ export default async (req, res) => {
|
||||
.withTransaction(manager)
|
||||
.retrieve(id, {
|
||||
select: ["refunded_total", "total"],
|
||||
relations: ["items", "swaps", "swaps.additional_items"],
|
||||
relations: [
|
||||
"items",
|
||||
"items.tax_lines",
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
"swaps.additional_items.tax_lines",
|
||||
],
|
||||
})
|
||||
|
||||
const swap = await swapService
|
||||
@@ -170,6 +176,7 @@ export default async (req, res) => {
|
||||
await swapService
|
||||
.withTransaction(manager)
|
||||
.createCart(swap.id, validated.custom_shipping_options)
|
||||
|
||||
const returnOrder = await returnService
|
||||
.withTransaction(manager)
|
||||
.retrieveBySwap(swap.id)
|
||||
|
||||
@@ -143,14 +143,6 @@ export default (app) => {
|
||||
middlewares.wrap(require("./cancel-swap").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Receives the inventory to return from a swap
|
||||
*/
|
||||
route.post(
|
||||
"/:id/swaps/:swap_id/receive",
|
||||
middlewares.wrap(require("./receive-swap").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Fulfills a swap.
|
||||
*/
|
||||
@@ -246,6 +238,8 @@ export const defaultAdminOrdersRelations = [
|
||||
"fulfillments.tracking_links",
|
||||
"fulfillments.items",
|
||||
"returns",
|
||||
"returns.shipping_method",
|
||||
"returns.shipping_method.tax_lines",
|
||||
"returns.items",
|
||||
"returns.items.reason",
|
||||
"gift_cards",
|
||||
@@ -390,7 +384,6 @@ export * from "./fulfill-swap"
|
||||
export * from "./get-order"
|
||||
export * from "./list-orders"
|
||||
export * from "./process-swap-payment"
|
||||
export * from "./receive-swap"
|
||||
export * from "./refund-payment"
|
||||
export * from "./request-return"
|
||||
export * from "./update-claim"
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import { Type } from "class-transformer"
|
||||
import {
|
||||
IsArray,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "."
|
||||
import { OrderService, SwapService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
/**
|
||||
* @oas [post] /orders/{id}/swaps/{swap_id}/receive
|
||||
* operationId: "PostOrdersOrderSwapsSwapReceive"
|
||||
* summary: "Receive a Swap"
|
||||
* description: "Registers a Swap as received."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Order.
|
||||
* - (path) swap_id=* {string} The id of the Swap.
|
||||
* requestBody:
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* items:
|
||||
* description: The Line Items that have been received.
|
||||
* type: array
|
||||
* items:
|
||||
* properties:
|
||||
* item_id:
|
||||
* description: The id of the Line Item.
|
||||
* type: string
|
||||
* quantity:
|
||||
* description: The quantity of the Line Item.
|
||||
* type: integer
|
||||
* tags:
|
||||
* - Order
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* order:
|
||||
* $ref: "#/components/schemas/order"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id, swap_id } = req.params
|
||||
|
||||
const validated = await validator(
|
||||
AdminPostOrdersOrderSwapsSwapReceiveReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const orderService: OrderService = req.scope.resolve("orderService")
|
||||
const swapService: SwapService = req.scope.resolve("swapService")
|
||||
const entityManager: EntityManager = req.scope.resolve("manager")
|
||||
|
||||
await entityManager.transaction(async (manager) => {
|
||||
await swapService
|
||||
.withTransaction(manager)
|
||||
.receiveReturn(swap_id, validated.items)
|
||||
|
||||
await orderService
|
||||
.withTransaction(manager)
|
||||
.registerSwapReceived(id, swap_id)
|
||||
})
|
||||
|
||||
const order = await orderService.retrieve(id, {
|
||||
select: defaultAdminOrdersFields,
|
||||
relations: defaultAdminOrdersRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ order })
|
||||
}
|
||||
|
||||
export class AdminPostOrdersOrderSwapsSwapReceiveReq {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Item)
|
||||
items: Item[]
|
||||
}
|
||||
|
||||
class Item {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
item_id: string
|
||||
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
quantity: number
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Router } from "express"
|
||||
import { ProductType } from "../../../.."
|
||||
import { PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
import "reflect-metadata"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/product-types", route)
|
||||
|
||||
route.get("/", middlewares.wrap(require("./list-product-types").default))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const allowedAdminProductTypeFields = [
|
||||
"id",
|
||||
"value",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
export const defaultAdminProductTypeFields = [
|
||||
"id",
|
||||
"value",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
export const defaultAdminProductTypeRelations = []
|
||||
|
||||
export type AdminProductTypesListRes = PaginatedResponse & {
|
||||
product_types: ProductType[]
|
||||
}
|
||||
|
||||
export type AdminProductTypesRes = {
|
||||
product_type: ProductType
|
||||
}
|
||||
|
||||
export * from "./list-product-types"
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Type } from "class-transformer"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { IsNumber, IsString, IsOptional, ValidateNested } from "class-validator"
|
||||
import { omit, pickBy, identity } from "lodash"
|
||||
import {
|
||||
allowedAdminProductTypeFields,
|
||||
defaultAdminProductTypeFields,
|
||||
defaultAdminProductTypeRelations,
|
||||
} from "."
|
||||
import { ProductType } from "../../../../models/product-type"
|
||||
import ProductTypeService from "../../../../services/product-type"
|
||||
import {
|
||||
StringComparisonOperator,
|
||||
DateComparisonOperator,
|
||||
FindConfig,
|
||||
} from "../../../../types/common"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
|
||||
/**
|
||||
* @oas [get] /product-types
|
||||
* operationId: "GetProductTypes"
|
||||
* summary: "List Product Types"
|
||||
* description: "Retrieve a list of Product Types."
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) limit {string} The number of types to return.
|
||||
* - (query) offset {string} The offset of types to return.
|
||||
* - (query) value {string} The value of types to return.
|
||||
* - (query) id {string} The id of types to return.
|
||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting tas was created, i.e. less than, greater than etc.
|
||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting tas was updated, i.e. less than, greater than etc.
|
||||
* tags:
|
||||
* - Product Tag
|
||||
* responses:
|
||||
* "200":
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* types:
|
||||
* $ref: "#/components/schemas/product_tag"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const validated = await validator(AdminGetProductTypesParams, req.query)
|
||||
|
||||
const typeService: ProductTypeService =
|
||||
req.scope.resolve("productTypeService")
|
||||
|
||||
const listConfig: FindConfig<ProductType> = {
|
||||
select: defaultAdminProductTypeFields as (keyof ProductType)[],
|
||||
relations: defaultAdminProductTypeRelations,
|
||||
skip: validated.offset,
|
||||
take: validated.limit,
|
||||
}
|
||||
|
||||
if (typeof validated.order !== "undefined") {
|
||||
let orderField = validated.order
|
||||
if (validated.order.startsWith("-")) {
|
||||
const [, field] = validated.order.split("-")
|
||||
orderField = field
|
||||
listConfig.order = { [field]: "DESC" }
|
||||
} else {
|
||||
listConfig.order = { [validated.order]: "ASC" }
|
||||
}
|
||||
|
||||
if (!allowedAdminProductTypeFields.includes(orderField)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Order field must be a valid product type field"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const filterableFields = omit(validated, ["limit", "offset"])
|
||||
|
||||
const [types, count] = await typeService.listAndCount(
|
||||
pickBy(filterableFields, identity),
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.status(200).json({
|
||||
product_types: types,
|
||||
count,
|
||||
offset: validated.offset,
|
||||
limit: validated.limit,
|
||||
})
|
||||
}
|
||||
|
||||
export class AdminGetProductTypesPaginationParams {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit? = 10
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset? = 0
|
||||
}
|
||||
|
||||
export class AdminGetProductTypesParams extends AdminGetProductTypesPaginationParams {
|
||||
@ValidateNested()
|
||||
@IsType([String, [String], StringComparisonOperator])
|
||||
@IsOptional()
|
||||
id?: string | string[] | StringComparisonOperator
|
||||
|
||||
@ValidateNested()
|
||||
@IsType([String, [String], StringComparisonOperator])
|
||||
@IsOptional()
|
||||
value?: string | string[] | StringComparisonOperator
|
||||
|
||||
@IsType([DateComparisonOperator])
|
||||
@IsOptional()
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
@IsType([DateComparisonOperator])
|
||||
@IsOptional()
|
||||
updated_at?: DateComparisonOperator
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
order?: string
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import { RegionServiceMock } from "../../../../../services/__mocks__/region"
|
||||
const defaultFields = [
|
||||
"id",
|
||||
"name",
|
||||
"automatic_taxes",
|
||||
"gift_cards_taxable",
|
||||
"tax_provider_id",
|
||||
"currency_code",
|
||||
"tax_rate",
|
||||
"tax_code",
|
||||
|
||||
@@ -5,6 +5,9 @@ import { RegionServiceMock } from "../../../../../services/__mocks__/region"
|
||||
const defaultFields = [
|
||||
"id",
|
||||
"name",
|
||||
"automatic_taxes",
|
||||
"gift_cards_taxable",
|
||||
"tax_provider_id",
|
||||
"currency_code",
|
||||
"tax_rate",
|
||||
"tax_code",
|
||||
|
||||
@@ -77,6 +77,9 @@ export default (app) => {
|
||||
export const defaultAdminRegionFields = [
|
||||
"id",
|
||||
"name",
|
||||
"automatic_taxes",
|
||||
"gift_cards_taxable",
|
||||
"tax_provider_id",
|
||||
"currency_code",
|
||||
"tax_rate",
|
||||
"tax_code",
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { IsArray, IsNumber, IsOptional, IsString } from "class-validator"
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
import { defaultAdminRegionRelations, defaultAdminRegionFields } from "."
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import RegionService from "../../../../services/region"
|
||||
@@ -22,6 +28,15 @@ import RegionService from "../../../../services/region"
|
||||
* currency_code:
|
||||
* description: "The 3 character ISO currency code to use for the Region."
|
||||
* type: string
|
||||
* automatic_taxes:
|
||||
* description: "If true Medusa will automatically calculate taxes for carts in this region. If false you have to manually call POST /carts/:id/taxes."
|
||||
* type: boolean
|
||||
* gift_cards_taxable:
|
||||
* description: "Whether gift cards in this region should be applied sales tax when purchasing a gift card"
|
||||
* type: boolean
|
||||
* tax_provider_id:
|
||||
* description: "The id of the tax provider to use; if null the system tax provider is used"
|
||||
* type: string
|
||||
* tax_code:
|
||||
* description: "An optional tax code the Region."
|
||||
* type: string
|
||||
@@ -86,6 +101,18 @@ export class AdminPostRegionsRegionReq {
|
||||
@IsOptional()
|
||||
tax_rate?: number
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
gift_cards_taxable?: boolean
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
automatic_taxes?: boolean
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
tax_provider_id?: string | null
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
|
||||
+2
-2
@@ -40,8 +40,8 @@ describe("GET /admin/shipping-options", () => {
|
||||
})
|
||||
|
||||
it("calls service retrieve", () => {
|
||||
expect(ShippingOptionServiceMock.list).toHaveBeenCalledTimes(1)
|
||||
expect(ShippingOptionServiceMock.list).toHaveBeenCalledWith(
|
||||
expect(ShippingOptionServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||
expect(ShippingOptionServiceMock.listAndCount).toHaveBeenCalledWith(
|
||||
{},
|
||||
{
|
||||
select: defaultFields,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from "express"
|
||||
import { ShippingOption } from "../../../.."
|
||||
import { DeleteResponse } from "../../../../types/common"
|
||||
import { PaginatedResponse, DeleteResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
@@ -46,7 +46,7 @@ export const defaultFields = [
|
||||
|
||||
export const defaultRelations = ["region", "profile", "requirements"]
|
||||
|
||||
export type AdminShippingOptionsListRes = {
|
||||
export type AdminShippingOptionsListRes = PaginatedResponse & {
|
||||
shipping_options: ShippingOption[]
|
||||
}
|
||||
|
||||
|
||||
@@ -50,12 +50,12 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
const optionService = req.scope.resolve("shippingOptionService")
|
||||
const data = await optionService.list(validatedParams, {
|
||||
const [data, count] = await optionService.listAndCount(validatedParams, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ shipping_options: data })
|
||||
res.status(200).json({ shipping_options: data, count })
|
||||
}
|
||||
|
||||
export class AdminGetShippingOptionsParams {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from "express"
|
||||
import { Store, PaymentProvider } from "./../../../../"
|
||||
import { Store, PaymentProvider, TaxProvider } from "./../../../../"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
@@ -12,6 +12,10 @@ export default (app) => {
|
||||
"/payment-providers",
|
||||
middlewares.wrap(require("./list-payment-providers").default)
|
||||
)
|
||||
route.get(
|
||||
"/tax-providers",
|
||||
middlewares.wrap(require("./list-tax-providers").default)
|
||||
)
|
||||
route.post("/", middlewares.wrap(require("./update-store").default))
|
||||
route.post(
|
||||
"/currencies/:currency_code",
|
||||
@@ -29,6 +33,10 @@ export type AdminStoresRes = {
|
||||
store: Store
|
||||
}
|
||||
|
||||
export type AdminTaxProvidersList = {
|
||||
tax_providers: TaxProvider[]
|
||||
}
|
||||
|
||||
export type AdminPaymentProvidersList = {
|
||||
payment_providers: PaymentProvider[]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { TaxProviderService } from "../../../../services"
|
||||
|
||||
/**
|
||||
* @oas [get] /store/tax-providers
|
||||
* operationId: "GetStoreTaxProviders"
|
||||
* summary: "Retrieve configured Tax Providers"
|
||||
* description: "Retrieves the configured Tax Providers"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Store
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_providers:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/store"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const taxProviderService: TaxProviderService =
|
||||
req.scope.resolve("taxProviderService")
|
||||
const taxProviders = await taxProviderService.list()
|
||||
res.status(200).json({ tax_providers: taxProviders })
|
||||
}
|
||||
@@ -11,12 +11,6 @@ const defaultRelations = [
|
||||
"shipping_address",
|
||||
"shipping_methods",
|
||||
"cart",
|
||||
"cart.items",
|
||||
"cart.region",
|
||||
"cart.shipping_methods",
|
||||
"cart.gift_cards",
|
||||
"cart.discounts",
|
||||
"cart.payment",
|
||||
]
|
||||
|
||||
const defaultFields = [
|
||||
|
||||
@@ -30,12 +30,6 @@ export const defaultAdminSwapRelations = [
|
||||
"shipping_address",
|
||||
"shipping_methods",
|
||||
"cart",
|
||||
"cart.items",
|
||||
"cart.region",
|
||||
"cart.shipping_methods",
|
||||
"cart.gift_cards",
|
||||
"cart.discounts",
|
||||
"cart.payment",
|
||||
]
|
||||
|
||||
export const defaultAdminSwapFields = [
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /tax-rates/:id/product-types/batch
|
||||
* operationId: "PostTaxRatesTaxRateProductTypes"
|
||||
* summary: "Add Tax Rate to Product Types"
|
||||
* description: "Associates a Tax Rate with a list of Product Types"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(
|
||||
AdminPostTaxRatesTaxRateProductTypesReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const query = await validator(
|
||||
AdminPostTaxRatesTaxRateProductTypesParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await rateService.addToProductType(req.params.id, value.product_types)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateProductTypesReq {
|
||||
@IsArray()
|
||||
product_types: string[]
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateProductTypesParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /tax-rates/:id/products/batch
|
||||
* operationId: "PostTaxRatesTaxRateProducts"
|
||||
* summary: "Add Tax Rate to Products"
|
||||
* description: "Associates a Tax Rate with a list of Products"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminPostTaxRatesTaxRateProductsReq, req.body)
|
||||
|
||||
const query = await validator(
|
||||
AdminPostTaxRatesTaxRateProductsParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await rateService.addToProduct(req.params.id, value.products)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateProductsReq {
|
||||
@IsArray()
|
||||
products: string[]
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateProductsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [post] /tax-rates/:id/shipping-options/batch
|
||||
* operationId: "PostTaxRatesTaxRateShippingOptions"
|
||||
* summary: "Add Tax Rate to Product Types"
|
||||
* description: "Associates a Tax Rate with a list of Product Types"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(
|
||||
AdminPostTaxRatesTaxRateShippingOptionsReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const query = await validator(
|
||||
AdminPostTaxRatesTaxRateShippingOptionsParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
await rateService.addToShippingOption(req.params.id, value.shipping_options)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateShippingOptionsReq {
|
||||
@IsArray()
|
||||
shipping_options: string[]
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateShippingOptionsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsString, IsArray, IsOptional } from "class-validator"
|
||||
import { omit } from "lodash"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
|
||||
/**
|
||||
* @oas [post] /tax-rates
|
||||
* operationId: "PostTaxRates"
|
||||
* summary: "Create a Tax Rate"
|
||||
* description: "Creates a Tax Rate"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminPostTaxRatesReq, req.body)
|
||||
|
||||
const query = await validator(AdminPostTaxRatesParams, req.query)
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
let id: string | undefined
|
||||
await manager.transaction(async (tx) => {
|
||||
const txRateService = rateService.withTransaction(tx)
|
||||
const created = await txRateService.create(
|
||||
omit(value, ["products", "product_types", "shipping_options"])
|
||||
)
|
||||
id = created.id
|
||||
|
||||
if (typeof value.products !== "undefined") {
|
||||
await txRateService.addToProduct(id, value.products)
|
||||
}
|
||||
|
||||
if (typeof value.product_types !== "undefined") {
|
||||
await txRateService.addToProductType(id, value.product_types)
|
||||
}
|
||||
|
||||
if (typeof value.shipping_options !== "undefined") {
|
||||
await txRateService.addToShippingOption(id, value.shipping_options)
|
||||
}
|
||||
})
|
||||
|
||||
if (typeof id === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Rate was not created"
|
||||
)
|
||||
}
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
|
||||
const rate = await rateService.retrieve(id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesReq {
|
||||
@IsString()
|
||||
code: string
|
||||
|
||||
@IsString()
|
||||
name: string
|
||||
|
||||
@IsString()
|
||||
region_id: string
|
||||
|
||||
@IsOptional()
|
||||
@IsType([Number, null])
|
||||
rate?: number | null
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
products?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
shipping_options?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
product_types?: string[]
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { TaxRateService } from "../../../../services"
|
||||
|
||||
/**
|
||||
* @oas [delete] /tax-rates/{id}
|
||||
* operationId: "DeleteTaxRatesTaxRate"
|
||||
* summary: "Delete a Tax Rate"
|
||||
* description: "Deletes a Tax Rate"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (path) id=* {string} The id of the Shipping Option.
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* id:
|
||||
* type: string
|
||||
* description: The id of the deleted Shipping Option.
|
||||
* object:
|
||||
* type: string
|
||||
* description: The type of the object that was deleted.
|
||||
* deleted:
|
||||
* type: boolean
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
const taxRateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await taxRateService.delete(id)
|
||||
|
||||
res.json({
|
||||
id: id,
|
||||
object: "tax-rate",
|
||||
deleted: true,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getRetrieveConfig, pickByConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /tax-rates/:id
|
||||
* operationId: "GetTaxRatesTaxRate"
|
||||
* summary: "Get Tax Rate"
|
||||
* description: "Retrieves a TaxRate"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminGetTaxRatesTaxRateParams, req.query)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
const config = getRetrieveConfig(
|
||||
value.fields as (keyof TaxRate)[],
|
||||
value.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig<TaxRate>(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminGetTaxRatesTaxRateParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Router } from "express"
|
||||
import { TaxRate } from "../../../.."
|
||||
import { DeleteResponse, PaginatedResponse } from "../../../../types/common"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/tax-rates", route)
|
||||
|
||||
/**
|
||||
* List tax rates
|
||||
*/
|
||||
route.get("/", middlewares.wrap(require("./list-tax-rates").default))
|
||||
|
||||
/**
|
||||
* Get a tax rate
|
||||
*/
|
||||
route.get("/:id", middlewares.wrap(require("./get-tax-rate").default))
|
||||
|
||||
/**
|
||||
* Create a tax rate
|
||||
*/
|
||||
route.post("/", middlewares.wrap(require("./create-tax-rate").default))
|
||||
|
||||
/**
|
||||
* Update a tax rate
|
||||
*/
|
||||
route.post("/:id", middlewares.wrap(require("./update-tax-rate").default))
|
||||
|
||||
/**
|
||||
* Remove products from tax rate
|
||||
*/
|
||||
route.delete(
|
||||
"/:id/products/batch",
|
||||
middlewares.wrap(require("./remove-from-products").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Remove product types from tax rate
|
||||
*/
|
||||
route.delete(
|
||||
"/:id/product-types/batch",
|
||||
middlewares.wrap(require("./remove-from-product-types").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Remove shipping options from tax rate
|
||||
*/
|
||||
route.delete(
|
||||
"/:id/shipping-options/batch",
|
||||
middlewares.wrap(require("./remove-from-shipping-options").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Add products to tax rate
|
||||
*/
|
||||
route.post(
|
||||
"/:id/products/batch",
|
||||
middlewares.wrap(require("./add-to-products").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Add product types to tax rate
|
||||
*/
|
||||
route.post(
|
||||
"/:id/product-types/batch",
|
||||
middlewares.wrap(require("./add-to-product-types").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Add to shipping options
|
||||
*/
|
||||
route.post(
|
||||
"/:id/shipping-options/batch",
|
||||
middlewares.wrap(require("./add-to-shipping-options").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete a tax rate
|
||||
*/
|
||||
route.delete("/:id", middlewares.wrap(require("./delete-tax-rate").default))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultAdminTaxRatesRelations = []
|
||||
|
||||
export const defaultAdminTaxRatesFields: (keyof TaxRate)[] = [
|
||||
"id",
|
||||
"rate",
|
||||
"code",
|
||||
"name",
|
||||
"region_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
export type AdminTaxRatesDeleteRes = DeleteResponse
|
||||
|
||||
export type AdminTaxRatesListRes = PaginatedResponse & {
|
||||
tax_rates: TaxRate[]
|
||||
}
|
||||
|
||||
export type AdminTaxRatesRes = {
|
||||
tax_rate: TaxRate
|
||||
}
|
||||
|
||||
export * from "./list-tax-rates"
|
||||
export * from "./get-tax-rate"
|
||||
export * from "./remove-from-product-types"
|
||||
export * from "./remove-from-products"
|
||||
export * from "./remove-from-shipping-options"
|
||||
export * from "./add-to-product-types"
|
||||
export * from "./add-to-products"
|
||||
export * from "./add-to-shipping-options"
|
||||
export * from "./create-tax-rate"
|
||||
export * from "./delete-tax-rate"
|
||||
export * from "./update-tax-rate"
|
||||
@@ -0,0 +1,111 @@
|
||||
import { getListConfig, pickByConfig } from "./utils/get-query-config"
|
||||
import {
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
} from "class-validator"
|
||||
import { Type } from "class-transformer"
|
||||
import { omit, pickBy, pick, identity } from "lodash"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import {
|
||||
NumericalComparisonOperator,
|
||||
FindConfig,
|
||||
} from "../../../../types/common"
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [get] /tax-rates
|
||||
* operationId: "GetTaxRates"
|
||||
* summary: "List Tax Rates"
|
||||
* description: "Retrieves a list of TaxRates"
|
||||
* x-authenticated: true
|
||||
* parameters:
|
||||
* - (query) q {string} Query used for searching orders.
|
||||
* - (query) id {string} Id of the order to search for.
|
||||
* - (query) region_id {string} to search for.
|
||||
* - (query) code {string} to search for.
|
||||
* - (query) rate {string} to search for.
|
||||
* - (query) created_at {DateComparisonOperator} Date comparison for when resulting orders was created, i.e. less than, greater than etc.
|
||||
* - (query) updated_at {DateComparisonOperator} Date comparison for when resulting orders was updated, i.e. less than, greater than etc.
|
||||
* - (query) offset {string} How many orders to skip in the result.
|
||||
* - (query) limit {string} Limit the number of orders returned.
|
||||
* - (query) fields {string} (Comma separated) Which fields should be included in each order of the result.
|
||||
* tags:
|
||||
* - Order
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* orders:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/order"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminGetTaxRatesParams, req.query)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
const listConfig = getListConfig()
|
||||
|
||||
const filterableFields = omit(value, [
|
||||
"limit",
|
||||
"offset",
|
||||
"expand",
|
||||
"fields",
|
||||
"order",
|
||||
])
|
||||
|
||||
const [rates, count] = await rateService.listAndCount(
|
||||
pickBy(filterableFields, identity),
|
||||
listConfig
|
||||
)
|
||||
|
||||
const data = pickByConfig<TaxRate>(rates, listConfig)
|
||||
|
||||
res.json({ tax_rates: data, count, offset: value.offset, limit: value.limit })
|
||||
}
|
||||
|
||||
export class AdminGetTaxRatesParams {
|
||||
@IsOptional()
|
||||
@IsType([String, [String]])
|
||||
region_id?: string | string[]
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
code?: string
|
||||
|
||||
@IsType([NumericalComparisonOperator, Number])
|
||||
@IsOptional()
|
||||
rate?: number | NumericalComparisonOperator
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
offset? = 0
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
limit? = 50
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [delete] /tax-rates/:id/product-types/batch
|
||||
* operationId: "DeleteTaxRatesTaxRateProductTypes"
|
||||
* summary: "Remove Tax Rate from Product Types"
|
||||
* description: "Removes a Tax Rate from a list of Product Types"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(
|
||||
AdminDeleteTaxRatesTaxRateProductTypesReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const query = await validator(
|
||||
AdminDeleteTaxRatesTaxRateProductTypesParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await rateService.removeFromProductType(req.params.id, value.product_types)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateProductTypesReq {
|
||||
@IsArray()
|
||||
product_types: string[]
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateProductTypesParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [delete] /tax-rates/:id/products/batch
|
||||
* operationId: "DeleteTaxRatesTaxRateProducts"
|
||||
* summary: "Removes Tax Rate from Products"
|
||||
* description: "Removes a Tax Rate from a list of Products"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminDeleteTaxRatesTaxRateProductsReq, req.body)
|
||||
|
||||
const query = await validator(
|
||||
AdminDeleteTaxRatesTaxRateProductsParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await rateService.removeFromProduct(req.params.id, value.products)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateProductsReq {
|
||||
@IsArray()
|
||||
products: string[]
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateProductsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { IsArray, IsOptional } from "class-validator"
|
||||
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
|
||||
/**
|
||||
* @oas [delete] /tax-rates/:id/shipping-options/batch
|
||||
* operationId: "DeleteTaxRatesTaxRateShippingOptions"
|
||||
* summary: "Removes a Tax Rate from Product Types"
|
||||
* description: "Removes a Tax Rate from a list of Product Types"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(
|
||||
AdminDeleteTaxRatesTaxRateShippingOptionsReq,
|
||||
req.body
|
||||
)
|
||||
|
||||
const query = await validator(
|
||||
AdminDeleteTaxRatesTaxRateShippingOptionsParams,
|
||||
req.query
|
||||
)
|
||||
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await rateService.removeFromShippingOption(
|
||||
req.params.id,
|
||||
value.shipping_options
|
||||
)
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateShippingOptionsReq {
|
||||
@IsArray()
|
||||
shipping_options: string[]
|
||||
}
|
||||
|
||||
export class AdminDeleteTaxRatesTaxRateShippingOptionsParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IsString, IsArray, IsOptional } from "class-validator"
|
||||
import { omit } from "lodash"
|
||||
|
||||
import { pickByConfig, getRetrieveConfig } from "./utils/get-query-config"
|
||||
import { TaxRate } from "../../../.."
|
||||
import { TaxRateService } from "../../../../services"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import { IsType } from "../../../../utils/validators/is-type"
|
||||
|
||||
/**
|
||||
* @oas [post] /tax-rates/:id
|
||||
* operationId: "PostTaxRatesTaxRate"
|
||||
* summary: "Update a Tax Rate"
|
||||
* description: "Updates a Tax Rate"
|
||||
* x-authenticated: true
|
||||
* tags:
|
||||
* - Tax Rates
|
||||
* responses:
|
||||
* 200:
|
||||
* description: OK
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* properties:
|
||||
* tax_rate:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: "#/components/schemas/tax_rate"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const value = await validator(AdminPostTaxRatesTaxRateReq, req.body)
|
||||
|
||||
const query = await validator(AdminPostTaxRatesTaxRateParams, req.query)
|
||||
|
||||
const manager: EntityManager = req.scope.resolve("manager")
|
||||
const rateService: TaxRateService = req.scope.resolve("taxRateService")
|
||||
|
||||
await manager.transaction(async (tx) => {
|
||||
const txRateService = rateService.withTransaction(tx)
|
||||
await txRateService.update(
|
||||
req.params.id,
|
||||
omit(value, ["products", "product_types", "shipping_options"])
|
||||
)
|
||||
|
||||
if (typeof value.products !== "undefined") {
|
||||
await txRateService.addToProduct(req.params.id, value.products, true)
|
||||
}
|
||||
|
||||
if (typeof value.product_types !== "undefined") {
|
||||
await txRateService.addToProductType(
|
||||
req.params.id,
|
||||
value.product_types,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof value.shipping_options !== "undefined") {
|
||||
await txRateService.addToShippingOption(
|
||||
req.params.id,
|
||||
value.shipping_options,
|
||||
true
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const config = getRetrieveConfig(
|
||||
query.fields as (keyof TaxRate)[],
|
||||
query.expand
|
||||
)
|
||||
|
||||
const rate = await rateService.retrieve(req.params.id, config)
|
||||
const data = pickByConfig(rate, config)
|
||||
|
||||
res.json({ tax_rate: data })
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateReq {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
code?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
region_id?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsType([Number, null])
|
||||
rate?: number | null
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
products?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
shipping_options?: string[]
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
product_types?: string[]
|
||||
}
|
||||
|
||||
export class AdminPostTaxRatesTaxRateParams {
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
expand?: string[]
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
fields?: string[]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { defaultAdminTaxRatesFields, defaultAdminTaxRatesRelations } from "../"
|
||||
import { pick } from "lodash"
|
||||
import { FindConfig } from "../../../../../types/common"
|
||||
import { TaxRate } from "../../../../.."
|
||||
|
||||
export function pickByConfig<T>(
|
||||
obj: T | T[],
|
||||
config: FindConfig<T>
|
||||
): Partial<T> | Partial<T>[] {
|
||||
const fields = [...(config.select ?? []), ...(config.relations ?? [])]
|
||||
|
||||
if (fields.length) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((o) => pick(o, fields))
|
||||
} else {
|
||||
return pick(obj, fields)
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
export function getRetrieveConfig(
|
||||
fields?: (keyof TaxRate)[],
|
||||
expand?: string[]
|
||||
): FindConfig<TaxRate> {
|
||||
let includeFields: (keyof TaxRate)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
const fieldSet = new Set(fields)
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TaxRate)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultAdminTaxRatesFields,
|
||||
relations: expandFields.length
|
||||
? expandFields
|
||||
: defaultAdminTaxRatesRelations,
|
||||
}
|
||||
}
|
||||
|
||||
export function getListConfig(
|
||||
fields?: (keyof TaxRate)[],
|
||||
expand?: string[],
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
order?: { [k: symbol]: "DESC" | "ASC" }
|
||||
): FindConfig<TaxRate> {
|
||||
let includeFields: (keyof TaxRate)[] = []
|
||||
if (typeof fields !== "undefined") {
|
||||
const fieldSet = new Set(fields)
|
||||
// Ensure created_at is included, since we are sorting on this
|
||||
fieldSet.add("created_at")
|
||||
fieldSet.add("id")
|
||||
includeFields = Array.from(fieldSet) as (keyof TaxRate)[]
|
||||
}
|
||||
|
||||
let expandFields: string[] = []
|
||||
if (typeof expand !== "undefined") {
|
||||
expandFields = expand
|
||||
}
|
||||
|
||||
const orderBy = order ?? { created_at: "DESC" }
|
||||
|
||||
return {
|
||||
select: includeFields.length ? includeFields : defaultAdminTaxRatesFields,
|
||||
relations: expandFields.length
|
||||
? expandFields
|
||||
: defaultAdminTaxRatesRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: orderBy,
|
||||
}
|
||||
}
|
||||
@@ -1,119 +1,56 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { CartServiceMock } from "../../../../../services/__mocks__/cart"
|
||||
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
|
||||
import { SwapServiceMock } from "../../../../../services/__mocks__/swap"
|
||||
import { CompletionStrategyMock } from "../../../../../strategies/__mocks__/cart-completion"
|
||||
|
||||
describe("POST /store/carts/:id", () => {
|
||||
describe("successfully completes a normal cart", () => {
|
||||
describe("POST /store/carts/:id/complete", () => {
|
||||
describe("successfully calls completion strategy - Legacy Endpoint", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/carts/${IdMap.getId("test-cart")}/complete-cart`
|
||||
)
|
||||
subject = await request("POST", `/store/carts/test-cart/complete-cart`)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Call CartService authorizePayment", () => {
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1)
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-cart"),
|
||||
{ idempotency_key: "testkey" }
|
||||
it("calls completion strat", () => {
|
||||
expect(CompletionStrategyMock.complete).toHaveBeenCalledTimes(1)
|
||||
expect(CompletionStrategyMock.complete).toHaveBeenCalledWith(
|
||||
"test-cart",
|
||||
{ idempotency_key: "testkey", recovery_point: "started" },
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("Call OrderService createFromCart", () => {
|
||||
expect(OrderServiceMock.createFromCart).toHaveBeenCalledTimes(1)
|
||||
expect(OrderServiceMock.createFromCart).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-cart")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
it("responds correctly", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns the created order", () => {
|
||||
expect(subject.body.data.id).toEqual(IdMap.getId("test-order"))
|
||||
expect(subject.body).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe("successfully completes a swap cart", () => {
|
||||
describe("successfully calls completion strategy", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/carts/${IdMap.getId("swap-cart")}/complete-cart`
|
||||
)
|
||||
subject = await request("POST", `/store/carts/test-cart/complete`)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Call CartService authorizePayment", () => {
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1)
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith(
|
||||
IdMap.getId("swap-cart"),
|
||||
{ idempotency_key: "testkey" }
|
||||
it("calls completion strat", () => {
|
||||
expect(CompletionStrategyMock.complete).toHaveBeenCalledTimes(1)
|
||||
expect(CompletionStrategyMock.complete).toHaveBeenCalledWith(
|
||||
"test-cart",
|
||||
{ idempotency_key: "testkey", recovery_point: "started" },
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("Call SwapService registerCartCompletion", () => {
|
||||
expect(SwapServiceMock.registerCartCompletion).toHaveBeenCalledTimes(1)
|
||||
expect(SwapServiceMock.registerCartCompletion).toHaveBeenCalledWith(
|
||||
"test-swap"
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
it("responds correctly", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns the created order", () => {
|
||||
expect(subject.body.data.id).toEqual("test-swap")
|
||||
})
|
||||
})
|
||||
|
||||
describe("returns early if payment requires more work", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/store/carts/${IdMap.getId("test-cart2")}/complete-cart`
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Call CartService authorizePayment", () => {
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledTimes(1)
|
||||
expect(CartServiceMock.authorizePayment).toHaveBeenCalledWith(
|
||||
IdMap.getId("test-cart2"),
|
||||
{ idempotency_key: "testkey" }
|
||||
)
|
||||
})
|
||||
|
||||
it("Call CartService retrieve 1 time", () => {
|
||||
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns the created order", () => {
|
||||
expect(subject.body.data.id).toEqual(IdMap.getId("test-cart2"))
|
||||
expect(subject.body.data.payment_session.status).toEqual("requires_more")
|
||||
expect(subject.body).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { IdempotencyKey } from "../../../../models/idempotency-key"
|
||||
import { CartService, IdempotencyKeyService } from "../../../../services"
|
||||
|
||||
/**
|
||||
* @oas [post] /carts/{id}/taxes
|
||||
* summary: "Calculate Cart Taxes"
|
||||
* operationId: "PostCartsCartTaxes"
|
||||
* description: "Calculates taxes for a cart. Depending on the cart's region
|
||||
* this may involve making 3rd party API calls to a Tax Provider service."
|
||||
* parameters:
|
||||
* - (path) id=* {String} The Cart id.
|
||||
* tags:
|
||||
* - Cart
|
||||
* responses:
|
||||
* 200:
|
||||
* description: "A cart object with the tax_total field populated"
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* oneOf:
|
||||
* - type: object
|
||||
* properties:
|
||||
* cart:
|
||||
* $ref: "#/components/schemas/cart"
|
||||
*/
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
|
||||
"idempotencyKeyService"
|
||||
)
|
||||
|
||||
const headerKey = req.get("Idempotency-Key") || ""
|
||||
|
||||
let idempotencyKey: IdempotencyKey
|
||||
try {
|
||||
idempotencyKey = await idempotencyKeyService.initializeRequest(
|
||||
headerKey,
|
||||
req.method,
|
||||
req.params,
|
||||
req.path
|
||||
)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
res.status(409).send("Failed to create idempotency key")
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
|
||||
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
|
||||
|
||||
const cartService: CartService = req.scope.resolve("cartService")
|
||||
|
||||
let inProgress = true
|
||||
let err = false
|
||||
|
||||
while (inProgress) {
|
||||
switch (idempotencyKey.recovery_point) {
|
||||
case "started": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async (manager: EntityManager) => {
|
||||
const cart = await cartService.withTransaction(manager).retrieve(
|
||||
id,
|
||||
{
|
||||
select: [
|
||||
"total",
|
||||
"subtotal",
|
||||
"tax_total",
|
||||
"discount_total",
|
||||
"shipping_total",
|
||||
"gift_card_total",
|
||||
],
|
||||
},
|
||||
{ force_taxes: true }
|
||||
)
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { cart },
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "finished": {
|
||||
inProgress = false
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
idempotencyKey = await idempotencyKeyService.update(
|
||||
idempotencyKey.idempotency_key,
|
||||
{
|
||||
recovery_point: "finished",
|
||||
response_code: 500,
|
||||
response_body: { message: "Unknown recovery point" },
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import {
|
||||
CartService,
|
||||
IdempotencyKeyService,
|
||||
OrderService,
|
||||
SwapService,
|
||||
} from "../../../../services"
|
||||
|
||||
import { Order } from "../../../../models/order"
|
||||
import { ICartCompletionStrategy } from "../../../../interfaces"
|
||||
import { IdempotencyKeyService } from "../../../../services"
|
||||
import { IdempotencyKey } from "../../../../models/idempotency-key"
|
||||
|
||||
/**
|
||||
* @oas [post] /carts/{id}/complete
|
||||
@@ -54,7 +48,7 @@ export default async (req, res) => {
|
||||
|
||||
const headerKey = req.get("Idempotency-Key") || ""
|
||||
|
||||
let idempotencyKey
|
||||
let idempotencyKey: IdempotencyKey
|
||||
try {
|
||||
idempotencyKey = await idempotencyKeyService.initializeRequest(
|
||||
headerKey,
|
||||
@@ -71,231 +65,15 @@ export default async (req, res) => {
|
||||
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
|
||||
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
|
||||
|
||||
const cartService: CartService = req.scope.resolve("cartService")
|
||||
const orderService: OrderService = req.scope.resolve("orderService")
|
||||
const swapService: SwapService = req.scope.resolve("swapService")
|
||||
const completionStrat: ICartCompletionStrategy = req.scope.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
|
||||
let inProgress = true
|
||||
let err = false
|
||||
const { response_code, response_body } = await completionStrat.complete(
|
||||
id,
|
||||
idempotencyKey,
|
||||
req.request_context
|
||||
)
|
||||
|
||||
while (inProgress) {
|
||||
switch (idempotencyKey.recovery_point) {
|
||||
case "started": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async (manager) => {
|
||||
let cart = await cartService.withTransaction(manager).retrieve(id)
|
||||
|
||||
if (cart.completed_at) {
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
code: MedusaError.Codes.CART_INCOMPATIBLE_STATE,
|
||||
message: "Cart has already been completed",
|
||||
type: MedusaError.Types.NOT_ALLOWED,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
cart = await cartService
|
||||
.withTransaction(manager)
|
||||
.authorizePayment(id, {
|
||||
...req.request_context,
|
||||
idempotency_key: idempotencyKey.idempotency_key,
|
||||
})
|
||||
|
||||
if (cart.payment_session) {
|
||||
if (
|
||||
cart.payment_session.status === "requires_more" ||
|
||||
cart.payment_session.status === "pending"
|
||||
) {
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: {
|
||||
data: cart,
|
||||
payment_status: cart.payment_session.status,
|
||||
type: "cart",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
recovery_point: "payment_authorized",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "payment_authorized": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async (manager) => {
|
||||
const cart = await cartService
|
||||
.withTransaction(manager)
|
||||
.retrieve(id, {
|
||||
select: ["total"],
|
||||
relations: ["payment", "payment_sessions"],
|
||||
})
|
||||
|
||||
let order: Order
|
||||
|
||||
// If cart is part of swap, we register swap as complete
|
||||
switch (cart.type) {
|
||||
case "swap": {
|
||||
try {
|
||||
const swapId = cart.metadata?.swap_id
|
||||
let swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.registerCartCompletion(swapId)
|
||||
|
||||
swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.retrieve(swap.id, { relations: ["shipping_address"] })
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: swap, type: "swap" },
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
) {
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
// case "payment_link":
|
||||
default: {
|
||||
if (typeof cart.total === "undefined") {
|
||||
return {
|
||||
response_code: 500,
|
||||
response_body: {
|
||||
message: "Unexpected state",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (!cart.payment && cart.total > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cart payment not authorized`
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
order = await orderService
|
||||
.withTransaction(manager)
|
||||
.createFromCart(cart.id)
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
error.message === "Order from cart already exists"
|
||||
) {
|
||||
order = await orderService
|
||||
.withTransaction(manager)
|
||||
.retrieveByCartId(id, {
|
||||
select: [
|
||||
"subtotal",
|
||||
"tax_total",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"total",
|
||||
],
|
||||
relations: ["shipping_address", "items", "payments"],
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: order, type: "order" },
|
||||
}
|
||||
} else if (
|
||||
error &&
|
||||
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
) {
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
order = await orderService
|
||||
.withTransaction(manager)
|
||||
.retrieve(order.id, {
|
||||
select: [
|
||||
"subtotal",
|
||||
"tax_total",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"total",
|
||||
],
|
||||
relations: ["shipping_address", "items", "payments"],
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: order, type: "order" },
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "finished": {
|
||||
inProgress = false
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
idempotencyKey = await idempotencyKeyService.update(
|
||||
idempotencyKey.idempotency_key,
|
||||
{
|
||||
recovery_point: "finished",
|
||||
response_code: 500,
|
||||
response_body: { message: "Unknown recovery point" },
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
|
||||
res.status(response_code).json(response_body)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,12 @@ export default (app, container) => {
|
||||
middlewares.wrap(require("./add-shipping-method").default)
|
||||
)
|
||||
|
||||
// Taxes
|
||||
route.post(
|
||||
"/:id/taxes",
|
||||
middlewares.wrap(require("./calculate-taxes").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,13 @@ export default async (req, res) => {
|
||||
.withTransaction(manager)
|
||||
.retrieve(swapDto.order_id, {
|
||||
select: ["refunded_total", "total"],
|
||||
relations: ["items", "swaps", "swaps.additional_items"],
|
||||
relations: [
|
||||
"items",
|
||||
"items.tax_lines",
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
"swaps.additional_items.tax_lines",
|
||||
],
|
||||
})
|
||||
|
||||
let returnShipping
|
||||
|
||||
Reference in New Issue
Block a user