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:
1 parent
d80eaa172d
commit
47588e7a8d
223 files changed
+28546
-2229
No files matched your search
@@ -21,6 +21,7 @@
|
||||
"@babel/core": "^7.14.3",
|
||||
"@babel/preset-typescript": "^7.13.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"babel-preset-medusa-package": "^1.1.19",
|
||||
"cross-env": "^5.2.1",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,7 @@ import config from "../config"
|
||||
import apiLoader from "../loaders/api"
|
||||
import passportLoader from "../loaders/passport"
|
||||
import servicesLoader from "../loaders/services"
|
||||
import strategiesLoader from "../loaders/strategies"
|
||||
|
||||
const adminSessionOpts = {
|
||||
cookieName: "session",
|
||||
@@ -45,6 +46,7 @@ testApp.use((req, res, next) => {
|
||||
})
|
||||
|
||||
servicesLoader({ container })
|
||||
strategiesLoader({ container })
|
||||
passportLoader({ app: testApp, container })
|
||||
|
||||
testApp.use((req, res, next) => {
|
||||
|
||||
@@ -1,55 +1,65 @@
|
||||
// API payloads
|
||||
export * from "./api"
|
||||
|
||||
// Interfaces
|
||||
export * from "./interfaces"
|
||||
|
||||
// Models
|
||||
export { Address } from "./models/address"
|
||||
export { Cart } from "./models/cart"
|
||||
export { ClaimImage } from "./models/claim-image"
|
||||
export { ClaimItem } from "./models/claim-item"
|
||||
export { ClaimOrder } from "./models/claim-order"
|
||||
export { ClaimTag } from "./models/claim-tag"
|
||||
export { Country } from "./models/country"
|
||||
export { Currency } from "./models/currency"
|
||||
export { CustomShippingOption } from "./models/custom-shipping-option"
|
||||
export { Customer } from "./models/customer"
|
||||
export { CustomerGroup } from "./models/customer-group"
|
||||
export { Discount } from "./models/discount"
|
||||
export { DiscountRule } from "./models/discount-rule"
|
||||
export { DraftOrder } from "./models/draft-order"
|
||||
export { Fulfillment } from "./models/fulfillment"
|
||||
export { FulfillmentItem } from "./models/fulfillment-item"
|
||||
export { FulfillmentProvider } from "./models/fulfillment-provider"
|
||||
export { GiftCard } from "./models/gift-card"
|
||||
export { GiftCardTransaction } from "./models/gift-card-transaction"
|
||||
export { IdempotencyKey } from "./models/idempotency-key"
|
||||
export { Image } from "./models/image"
|
||||
export { Invite } from "./models/invite"
|
||||
export { LineItem } from "./models/line-item"
|
||||
export { MoneyAmount } from "./models/money-amount"
|
||||
export { Note } from "./models/note"
|
||||
export { Notification } from "./models/notification"
|
||||
export { Oauth } from "./models/oauth"
|
||||
export { Order } from "./models/order"
|
||||
export { Payment } from "./models/payment"
|
||||
export { PaymentProvider } from "./models/payment-provider"
|
||||
export { PaymentSession } from "./models/payment-session"
|
||||
export { Product } from "./models/product"
|
||||
export { ProductCollection } from "./models/product-collection"
|
||||
export { ProductOption } from "./models/product-option"
|
||||
export { ProductOptionValue } from "./models/product-option-value"
|
||||
export { ProductTag } from "./models/product-tag"
|
||||
export { ProductType } from "./models/product-type"
|
||||
export { ProductVariant } from "./models/product-variant"
|
||||
export { Refund } from "./models/refund"
|
||||
export { Region } from "./models/region"
|
||||
export { Return } from "./models/return"
|
||||
export { ReturnItem } from "./models/return-item"
|
||||
export { ReturnReason } from "./models/return-reason"
|
||||
export { ShippingMethod } from "./models/shipping-method"
|
||||
export { ShippingOption } from "./models/shipping-option"
|
||||
export { ShippingOptionRequirement } from "./models/shipping-option-requirement"
|
||||
export { ShippingProfile } from "./models/shipping-profile"
|
||||
export { StagedJob } from "./models/staged-job"
|
||||
export { Store } from "./models/store"
|
||||
export { Swap } from "./models/swap"
|
||||
export { User } from "./models/user"
|
||||
export * from "./models/shipping-tax-rate"
|
||||
export * from "./models/product-tax-rate"
|
||||
export * from "./models/product-type-tax-rate"
|
||||
export * from "./models/tax-rate"
|
||||
export * from "./models/shipping-method-tax-line"
|
||||
export * from "./models/line-item-tax-line"
|
||||
export * from "./models/address"
|
||||
export * from "./models/cart"
|
||||
export * from "./models/claim-image"
|
||||
export * from "./models/claim-item"
|
||||
export * from "./models/claim-order"
|
||||
export * from "./models/claim-tag"
|
||||
export * from "./models/country"
|
||||
export * from "./models/currency"
|
||||
export * from "./models/custom-shipping-option"
|
||||
export * from "./models/customer"
|
||||
export * from "./models/customer-group"
|
||||
export * from "./models/discount"
|
||||
export * from "./models/discount-rule"
|
||||
export * from "./models/draft-order"
|
||||
export * from "./models/fulfillment"
|
||||
export * from "./models/fulfillment-item"
|
||||
export * from "./models/fulfillment-provider"
|
||||
export * from "./models/gift-card"
|
||||
export * from "./models/gift-card-transaction"
|
||||
export * from "./models/idempotency-key"
|
||||
export * from "./models/image"
|
||||
export * from "./models/invite"
|
||||
export * from "./models/line-item"
|
||||
export * from "./models/money-amount"
|
||||
export * from "./models/note"
|
||||
export * from "./models/notification"
|
||||
export * from "./models/oauth"
|
||||
export * from "./models/order"
|
||||
export * from "./models/payment"
|
||||
export * from "./models/payment-provider"
|
||||
export * from "./models/payment-session"
|
||||
export * from "./models/product"
|
||||
export * from "./models/product-collection"
|
||||
export * from "./models/product-option"
|
||||
export * from "./models/product-option-value"
|
||||
export * from "./models/product-tag"
|
||||
export * from "./models/product-type"
|
||||
export * from "./models/product-variant"
|
||||
export * from "./models/refund"
|
||||
export * from "./models/region"
|
||||
export * from "./models/return"
|
||||
export * from "./models/return-item"
|
||||
export * from "./models/return-reason"
|
||||
export * from "./models/shipping-method"
|
||||
export * from "./models/shipping-option"
|
||||
export * from "./models/shipping-option-requirement"
|
||||
export * from "./models/shipping-profile"
|
||||
export * from "./models/staged-job"
|
||||
export * from "./models/store"
|
||||
export * from "./models/swap"
|
||||
export * from "./models/user"
|
||||
export * from "./models/tax-provider"
|
||||
@@ -0,0 +1,33 @@
|
||||
import { IdempotencyKey } from "../models/idempotency-key"
|
||||
import { RequestContext } from "../types/request"
|
||||
|
||||
export type CartCompletionResponse = {
|
||||
/** The response code for the completion request */
|
||||
response_code: number
|
||||
|
||||
/** The response body for the completion request */
|
||||
response_body: object
|
||||
}
|
||||
|
||||
export interface ICartCompletionStrategy {
|
||||
/**
|
||||
* Takes a cart id and completes the cart. This for example takes place when
|
||||
* creating an order or confirming a swap.
|
||||
* @param cartId - the id of the Cart to complete.
|
||||
* @param idempotencyKey - the idempotency key for the request
|
||||
* @param context - the request context for the completion request
|
||||
* @return the response for the completion request
|
||||
*/
|
||||
complete(
|
||||
cartId: string,
|
||||
idempotencyKey: IdempotencyKey,
|
||||
context: RequestContext
|
||||
): Promise<CartCompletionResponse>
|
||||
}
|
||||
|
||||
export function isCartCompletionStrategy(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: any
|
||||
): object is ICartCompletionStrategy {
|
||||
return typeof object.complete === "function"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./tax-calculation-strategy"
|
||||
export * from "./cart-completion-strategy"
|
||||
export * from "./tax-service"
|
||||
@@ -0,0 +1,27 @@
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { TaxCalculationContext } from "./tax-service"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
|
||||
export interface ITaxCalculationStrategy {
|
||||
/**
|
||||
* Calculates the tax amount for a given set of line items under applicable
|
||||
* tax conditions and calculation contexts.
|
||||
* @param items - the line items to calculate the tax total for
|
||||
* @param taxLines - the tax lines that applies to the calculation
|
||||
* @param calculationContext - other details relevant for the calculation
|
||||
* @return the tax total
|
||||
*/
|
||||
calculate(
|
||||
items: LineItem[],
|
||||
taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[],
|
||||
calculationContext: TaxCalculationContext
|
||||
): Promise<number>
|
||||
}
|
||||
|
||||
export function isTaxCalculationStrategy(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
object: any
|
||||
): object is ITaxCalculationStrategy {
|
||||
return typeof object.calculate === "function"
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { Region } from "../models/region"
|
||||
import { Address } from "../models/address"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { Customer } from "../models/customer"
|
||||
import { ProviderTaxLine, TaxServiceRate } from "../types/tax-service"
|
||||
import { LineAllocationsMap } from "../types/totals"
|
||||
|
||||
/**
|
||||
* A shipping method and the tax rates that have been configured to apply to the
|
||||
* shipping method.
|
||||
*/
|
||||
export type ShippingTaxCalculationLine = {
|
||||
shipping_method: ShippingMethod
|
||||
rates: TaxServiceRate[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A line item and the tax rates that have been configured to apply to the
|
||||
* product contained in the line item.
|
||||
*/
|
||||
export type ItemTaxCalculationLine = {
|
||||
item: LineItem
|
||||
rates: TaxServiceRate[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Information relevant to a tax calculation, such as the shipping address where
|
||||
* the items are going.
|
||||
*/
|
||||
export type TaxCalculationContext = {
|
||||
shipping_address: Address | null
|
||||
customer: Customer
|
||||
region: Region
|
||||
is_return: boolean
|
||||
shipping_methods: ShippingMethod[]
|
||||
allocation_map: LineAllocationsMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to be implemented by tax provider plugins. The interface defines a
|
||||
* single method `getTaxLines` that returns numerical rates to apply to line
|
||||
* items and shipping methods.
|
||||
*/
|
||||
export interface ITaxService {
|
||||
/**
|
||||
* Retrieves the numerical tax lines for a calculation context.
|
||||
* @param itemLines - the line item calculation lines
|
||||
* @param itemLines - the shipping calculation lines
|
||||
* @param context - other details relevant to the tax determination
|
||||
* @return numerical tax rates that should apply to the provided calculation
|
||||
* lines
|
||||
*/
|
||||
getTaxLines(
|
||||
itemLines: ItemTaxCalculationLine[],
|
||||
shippingLines: ShippingTaxCalculationLine[],
|
||||
context: TaxCalculationContext
|
||||
): Promise<ProviderTaxLine[]>
|
||||
}
|
||||
@@ -16,9 +16,10 @@ import redisLoader from "./redis"
|
||||
import repositoriesLoader from "./repositories"
|
||||
import searchIndexLoader from "./search-index"
|
||||
import servicesLoader from "./services"
|
||||
import strategiesLoader from "./strategies"
|
||||
import subscribersLoader from "./subscribers"
|
||||
|
||||
export default async ({ directory: rootDirectory, expressApp }) => {
|
||||
export default async ({ directory: rootDirectory, expressApp, isTest }) => {
|
||||
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||
|
||||
const container = createContainer()
|
||||
@@ -58,7 +59,7 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
||||
|
||||
const modelsActivity = Logger.activity("Initializing models")
|
||||
track("MODELS_INIT_STARTED")
|
||||
modelsLoader({ container, activityId: modelsActivity })
|
||||
modelsLoader({ container, activityId: modelsActivity, isTest })
|
||||
const mAct = Logger.success(modelsActivity, "Models initialized") || {}
|
||||
track("MODELS_INIT_COMPLETED", { duration: mAct.duration })
|
||||
|
||||
@@ -74,7 +75,7 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
||||
|
||||
const repoActivity = Logger.activity("Initializing repositories")
|
||||
track("REPOSITORIES_INIT_STARTED")
|
||||
repositoriesLoader({ container, activityId: repoActivity }) || {}
|
||||
repositoriesLoader({ container, activityId: repoActivity, isTest }) || {}
|
||||
const rAct = Logger.success(repoActivity, "Repositories initialized") || {}
|
||||
track("REPOSITORIES_INIT_COMPLETED", { duration: rAct.duration })
|
||||
|
||||
@@ -84,6 +85,7 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
||||
container,
|
||||
configModule,
|
||||
activityId: dbActivity,
|
||||
isTest,
|
||||
})
|
||||
const dbAct = Logger.success(dbActivity, "Database initialized") || {}
|
||||
track("DATABASE_INIT_COMPLETED", { duration: dbAct.duration })
|
||||
@@ -92,12 +94,24 @@ export default async ({ directory: rootDirectory, expressApp }) => {
|
||||
manager: asValue(dbConnection.manager),
|
||||
})
|
||||
|
||||
const stratActivity = Logger.activity("Initializing strategies")
|
||||
track("STRATEGIES_INIT_STARTED")
|
||||
strategiesLoader({
|
||||
container,
|
||||
configModule,
|
||||
activityId: stratActivity,
|
||||
isTest,
|
||||
})
|
||||
const stratAct = Logger.success(stratActivity, "Strategies initialized") || {}
|
||||
track("STRATEGIES_INIT_COMPLETED", { duration: stratAct.duration })
|
||||
|
||||
const servicesActivity = Logger.activity("Initializing services")
|
||||
track("SERVICES_INIT_STARTED")
|
||||
servicesLoader({
|
||||
container,
|
||||
configModule,
|
||||
activityId: servicesActivity,
|
||||
isTest,
|
||||
})
|
||||
const servAct = Logger.success(servicesActivity, "Services initialized") || {}
|
||||
track("SERVICES_INIT_COMPLETED", { duration: servAct.duration })
|
||||
|
||||
@@ -9,13 +9,13 @@ import formatRegistrationName from "../utils/format-registration-name"
|
||||
* Registers all models in the model directory
|
||||
*/
|
||||
export default ({ container }, config = { register: true }) => {
|
||||
let corePath = "../models/*.js"
|
||||
const corePath = "../models/*.js"
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
|
||||
const toReturn = []
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach(fn => {
|
||||
core.forEach((fn) => {
|
||||
const loaded = require(fn)
|
||||
|
||||
Object.entries(loaded).map(([key, val]) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async ({ app, container }) => {
|
||||
passport.use(
|
||||
new JWTStrategy(
|
||||
{
|
||||
jwtFromRequest: req => req.session.jwt,
|
||||
jwtFromRequest: (req) => req.session.jwt,
|
||||
secretOrKey: config.jwtSecret,
|
||||
},
|
||||
async (jwtPayload, done) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import glob from "glob"
|
||||
import { EntitySchema } from "typeorm"
|
||||
import {
|
||||
BaseModel,
|
||||
BaseService,
|
||||
PaymentService,
|
||||
FulfillmentService,
|
||||
@@ -17,6 +16,7 @@ import fs from "fs"
|
||||
import { asValue, asClass, asFunction, aliasTo } from "awilix"
|
||||
import { sync as existsSync } from "fs-exists-cached"
|
||||
|
||||
import { isTaxCalculationStrategy } from "../interfaces/tax-calculation-strategy"
|
||||
import formatRegistrationName from "../utils/format-registration-name"
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ export default async ({ rootDirectory, container, app, activityId }) => {
|
||||
const resolved = getResolvedPlugins(rootDirectory)
|
||||
|
||||
await Promise.all(
|
||||
resolved.map(async pluginDetails => {
|
||||
resolved.map(async (pluginDetails) => {
|
||||
registerRepositories(pluginDetails, container)
|
||||
await registerServices(pluginDetails, container)
|
||||
registerMedusaApi(pluginDetails, container)
|
||||
@@ -37,7 +37,7 @@ export default async ({ rootDirectory, container, app, activityId }) => {
|
||||
)
|
||||
|
||||
await Promise.all(
|
||||
resolved.map(async pluginDetails => runLoaders(pluginDetails, container))
|
||||
resolved.map(async (pluginDetails) => runLoaders(pluginDetails, container))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ function getResolvedPlugins(rootDirectory) {
|
||||
|
||||
const { plugins } = configModule
|
||||
|
||||
const resolved = plugins.map(plugin => {
|
||||
const resolved = plugins.map((plugin) => {
|
||||
if (_.isString(plugin)) {
|
||||
return resolvePlugin(plugin)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ function getResolvedPlugins(rootDirectory) {
|
||||
export async function registerPluginModels({ rootDirectory, container }) {
|
||||
const resolved = getResolvedPlugins(rootDirectory)
|
||||
await Promise.all(
|
||||
resolved.map(async pluginDetails => {
|
||||
resolved.map(async (pluginDetails) => {
|
||||
registerModels(pluginDetails, container)
|
||||
})
|
||||
)
|
||||
@@ -87,7 +87,7 @@ async function runLoaders(pluginDetails, container) {
|
||||
{}
|
||||
)
|
||||
await Promise.all(
|
||||
loaderFiles.map(async loader => {
|
||||
loaderFiles.map(async (loader) => {
|
||||
try {
|
||||
const module = require(loader).default
|
||||
if (typeof module === "function") {
|
||||
@@ -104,6 +104,34 @@ async function runLoaders(pluginDetails, container) {
|
||||
|
||||
function registerMedusaApi(pluginDetails, container) {
|
||||
registerMedusaMiddleware(pluginDetails, container)
|
||||
registerStrategies(pluginDetails, container)
|
||||
}
|
||||
|
||||
function registerStrategies(pluginDetails, container) {
|
||||
let module
|
||||
try {
|
||||
const path = `${pluginDetails.resolve}/strategies/tax-calculation`
|
||||
if (existsSync(path)) {
|
||||
module = require(path).default
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isTaxCalculationStrategy(module.prototype)) {
|
||||
container.register({
|
||||
taxCalculationStrategy: asFunction(
|
||||
(cradle) => new module(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
})
|
||||
} else {
|
||||
const logger = container.resolve("logger")
|
||||
logger.warn(
|
||||
`${pluginDetails.resolve}/strategies/tax-calculation did not export a class that implements ITaxCalculationStrategy. Your Medusa server will still work, but if you have written custom tax calculation logic it will not be used. Make sure to implement the ITaxCalculationStrategy interface.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function registerMedusaMiddleware(pluginDetails, container) {
|
||||
@@ -140,7 +168,7 @@ function registerCoreRouters(pluginDetails, container) {
|
||||
const adminFiles = glob.sync(`${resolve}/api/admin/[!__]*.js`, {})
|
||||
const storeFiles = glob.sync(`${resolve}/api/store/[!__]*.js`, {})
|
||||
|
||||
adminFiles.forEach(fn => {
|
||||
adminFiles.forEach((fn) => {
|
||||
const descriptor = fn.split(".")[0]
|
||||
const splat = descriptor.split("/")
|
||||
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
||||
@@ -148,7 +176,7 @@ function registerCoreRouters(pluginDetails, container) {
|
||||
middlewareService.addRouter(path, loaded())
|
||||
})
|
||||
|
||||
storeFiles.forEach(fn => {
|
||||
storeFiles.forEach((fn) => {
|
||||
const descriptor = fn.split(".")[0]
|
||||
const splat = descriptor.split("/")
|
||||
const path = `${splat[splat.length - 2]}/${splat[splat.length - 1]}`
|
||||
@@ -204,7 +232,7 @@ function registerApi(
|
||||
async function registerServices(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/services/[!__]*`, {})
|
||||
await Promise.all(
|
||||
files.map(async fn => {
|
||||
files.map(async (fn) => {
|
||||
const loaded = require(fn).default
|
||||
const name = formatRegistrationName(fn)
|
||||
|
||||
@@ -219,14 +247,14 @@ async function registerServices(pluginDetails, container) {
|
||||
// Register our payment providers to paymentProviders
|
||||
container.registerAdd(
|
||||
"paymentProviders",
|
||||
asFunction(cradle => new loaded(cradle, pluginDetails.options))
|
||||
asFunction((cradle) => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
[`pp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
@@ -239,35 +267,35 @@ async function registerServices(pluginDetails, container) {
|
||||
const name = appDetails.application_name
|
||||
container.register({
|
||||
[`${name}Oauth`]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
})
|
||||
} else if (loaded.prototype instanceof FulfillmentService) {
|
||||
// Register our payment providers to paymentProviders
|
||||
container.registerAdd(
|
||||
"fulfillmentProviders",
|
||||
asFunction(cradle => new loaded(cradle, pluginDetails.options))
|
||||
asFunction((cradle) => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which fulfillment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof NotificationService) {
|
||||
container.registerAdd(
|
||||
"notificationProviders",
|
||||
asFunction(cradle => new loaded(cradle, pluginDetails.options))
|
||||
asFunction((cradle) => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which notification provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
[`noti_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
@@ -276,7 +304,7 @@ async function registerServices(pluginDetails, container) {
|
||||
// resolution if we already know which file storage provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
[`fileService`]: aliasTo(name),
|
||||
})
|
||||
@@ -285,14 +313,14 @@ async function registerServices(pluginDetails, container) {
|
||||
// resolution if we already know which search provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
[`searchService`]: aliasTo(name),
|
||||
})
|
||||
} else {
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -311,13 +339,13 @@ async function registerServices(pluginDetails, container) {
|
||||
*/
|
||||
function registerSubscribers(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/subscribers/*.js`, {})
|
||||
files.forEach(fn => {
|
||||
files.forEach((fn) => {
|
||||
const loaded = require(fn).default
|
||||
|
||||
const name = formatRegistrationName(fn)
|
||||
container.build(
|
||||
asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
(cradle) => new loaded(cradle, pluginDetails.options)
|
||||
).singleton()
|
||||
)
|
||||
})
|
||||
@@ -334,7 +362,7 @@ function registerSubscribers(pluginDetails, container) {
|
||||
*/
|
||||
function registerRepositories(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/repositories/*.js`, {})
|
||||
files.forEach(fn => {
|
||||
files.forEach((fn) => {
|
||||
const loaded = require(fn)
|
||||
|
||||
Object.entries(loaded).map(([key, val]) => {
|
||||
@@ -361,7 +389,7 @@ function registerRepositories(pluginDetails, container) {
|
||||
*/
|
||||
function registerModels(pluginDetails, container) {
|
||||
const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {})
|
||||
files.forEach(fn => {
|
||||
files.forEach((fn) => {
|
||||
const loaded = require(fn)
|
||||
|
||||
Object.entries(loaded).map(([key, val]) => {
|
||||
|
||||
@@ -7,20 +7,21 @@ import formatRegistrationName from "../utils/format-registration-name"
|
||||
/**
|
||||
* Registers all services in the services directory
|
||||
*/
|
||||
export default ({ container, configModule }) => {
|
||||
const isTest = process.env.NODE_ENV === "test"
|
||||
export default ({ container, configModule, isTest }) => {
|
||||
const useMock =
|
||||
typeof isTest !== "undefined" ? isTest : process.env.NODE_ENV === "test"
|
||||
|
||||
const corePath = isTest ? "../services/__mocks__/*.js" : "../services/*.js"
|
||||
const corePath = useMock ? "../services/__mocks__/*.js" : "../services/*.js"
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach(fn => {
|
||||
core.forEach((fn) => {
|
||||
const loaded = require(fn).default
|
||||
if (loaded) {
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, configModule),
|
||||
(cradle) => new loaded(cradle, configModule)
|
||||
).singleton(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import glob from "glob"
|
||||
import path from "path"
|
||||
import { AwilixContainer, asFunction } from "awilix"
|
||||
|
||||
import formatRegistrationName from "../utils/format-registration-name"
|
||||
|
||||
type LoaderOptions = {
|
||||
container: AwilixContainer
|
||||
configModule: object
|
||||
isTest: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all strategies in the strategies directory
|
||||
* @returns void
|
||||
*/
|
||||
export default ({ container, configModule, isTest }: LoaderOptions): void => {
|
||||
const useMock =
|
||||
typeof isTest !== "undefined" ? isTest : process.env.NODE_ENV === "test"
|
||||
|
||||
const corePath = useMock
|
||||
? "../strategies/__mocks__/*.js"
|
||||
: "../strategies/*.js"
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach((fn) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const loaded = require(fn).default
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
(cradle) => new loaded(cradle, configModule)
|
||||
).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class newTaxSystem1641636508055 implements MigrationInterface {
|
||||
name = "newTaxSystem1641636508055"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tax_rate" ("id" character varying NOT NULL, "rate" real, "code" character varying, "name" character varying NOT NULL, "region_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_23b71b53f650c0b39e99ccef4fd" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "tax_provider" ("id" character varying NOT NULL, "is_installed" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_b198bf82ba6a317c11763d99b99" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "line_item_tax_line" ("id" character varying NOT NULL, "rate" real NOT NULL, "name" character varying NOT NULL, "code" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "item_id" character varying NOT NULL, CONSTRAINT "PK_4a0f4322fcd5ce4af85727f89a8" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_5077fa54b0d037e984385dfe8a" ON "line_item_tax_line" ("item_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "shipping_method_tax_line" ("id" character varying NOT NULL, "rate" real NOT NULL, "name" character varying NOT NULL, "code" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, "shipping_method_id" character varying NOT NULL, CONSTRAINT "PK_54c94f5908aacbd51cf0a73edb1" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_926ca9f29014af8091722dede0" ON "shipping_method_tax_line" ("shipping_method_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_tax_rate" ("product_id" character varying NOT NULL, "rate_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_326257ce468df46cd5c8c5922e9" PRIMARY KEY ("product_id", "rate_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_type_tax_rate" ("product_type_id" character varying NOT NULL, "rate_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_ddc9242de1d99bc7674969289f0" PRIMARY KEY ("product_type_id", "rate_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "shipping_tax_rate" ("shipping_option_id" character varying NOT NULL, "rate_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "metadata" jsonb, CONSTRAINT "PK_bcd93b14d7e2695365d383f5eae" PRIMARY KEY ("shipping_option_id", "rate_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" ADD "gift_cards_taxable" boolean NOT NULL DEFAULT true`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" ADD "automatic_taxes" boolean NOT NULL DEFAULT true`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" ADD "tax_provider_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" ADD "is_return" boolean NOT NULL DEFAULT false`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`UPDATE "line_item" SET "is_return" = true WHERE "id" IN (SELECT "id" from "line_item" WHERE "metadata"->>'is_return_line' = 'true')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "order" ALTER COLUMN "tax_rate" DROP NOT NULL`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_2484cf14c437a04586b07e7ddd" ON "product_tax_rate" ("rate_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_1d04aebeabb6a89f87e536a124" ON "product_tax_rate" ("product_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ece65a774192b34253abc4cd67" ON "product_type_tax_rate" ("rate_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_25a3138bb236f63d9bb6c8ff11" ON "product_type_tax_rate" ("product_type_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_346e0016cf045b998074774764" ON "shipping_tax_rate" ("rate_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_f672727ab020df6c50fb64c1a7" ON "shipping_tax_rate" ("shipping_option_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tax_rate" ADD CONSTRAINT "FK_b95a1e03b051993d208366cb960" FOREIGN KEY ("region_id") REFERENCES "region"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" ADD CONSTRAINT "FK_91f88052197680f9790272aaf5b" FOREIGN KEY ("tax_provider_id") REFERENCES "tax_provider"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item_tax_line" ADD CONSTRAINT "FK_5077fa54b0d037e984385dfe8ad" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method_tax_line" ADD CONSTRAINT "FK_926ca9f29014af8091722dede08" FOREIGN KEY ("shipping_method_id") REFERENCES "shipping_method"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tax_rate" ADD CONSTRAINT "FK_1d04aebeabb6a89f87e536a124d" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tax_rate" ADD CONSTRAINT "FK_2484cf14c437a04586b07e7dddb" FOREIGN KEY ("rate_id") REFERENCES "tax_rate"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_type_tax_rate" ADD CONSTRAINT "FK_25a3138bb236f63d9bb6c8ff111" FOREIGN KEY ("product_type_id") REFERENCES "product_type"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_type_tax_rate" ADD CONSTRAINT "FK_ece65a774192b34253abc4cd672" FOREIGN KEY ("rate_id") REFERENCES "tax_rate"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_tax_rate" ADD CONSTRAINT "FK_f672727ab020df6c50fb64c1a70" FOREIGN KEY ("shipping_option_id") REFERENCES "shipping_option"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_tax_rate" ADD CONSTRAINT "FK_346e0016cf045b9980747747645" FOREIGN KEY ("rate_id") REFERENCES "tax_rate"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_tax_rate" DROP CONSTRAINT "FK_346e0016cf045b9980747747645"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_tax_rate" DROP CONSTRAINT "FK_f672727ab020df6c50fb64c1a70"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_type_tax_rate" DROP CONSTRAINT "FK_ece65a774192b34253abc4cd672"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_type_tax_rate" DROP CONSTRAINT "FK_25a3138bb236f63d9bb6c8ff111"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tax_rate" DROP CONSTRAINT "FK_2484cf14c437a04586b07e7dddb"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tax_rate" DROP CONSTRAINT "FK_1d04aebeabb6a89f87e536a124d"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method_tax_line" DROP CONSTRAINT "FK_926ca9f29014af8091722dede08"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item_tax_line" DROP CONSTRAINT "FK_5077fa54b0d037e984385dfe8ad"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" DROP CONSTRAINT "FK_91f88052197680f9790272aaf5b"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "tax_rate" DROP CONSTRAINT "FK_b95a1e03b051993d208366cb960"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_f672727ab020df6c50fb64c1a7"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_346e0016cf045b998074774764"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_25a3138bb236f63d9bb6c8ff11"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_ece65a774192b34253abc4cd67"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_1d04aebeabb6a89f87e536a124"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_2484cf14c437a04586b07e7ddd"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "order" ALTER COLUMN "tax_rate" SET NOT NULL`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "line_item" DROP COLUMN "is_return"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" DROP COLUMN "tax_provider_id"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" DROP COLUMN "automatic_taxes"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "region" DROP COLUMN "gift_cards_taxable"`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "shipping_tax_rate"`)
|
||||
await queryRunner.query(`DROP TABLE "product_type_tax_rate"`)
|
||||
await queryRunner.query(`DROP TABLE "product_tax_rate"`)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_926ca9f29014af8091722dede0"`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "shipping_method_tax_line"`)
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_5077fa54b0d037e984385dfe8a"`
|
||||
)
|
||||
await queryRunner.query(`DROP TABLE "line_item_tax_line"`)
|
||||
await queryRunner.query(`DROP TABLE "tax_provider"`)
|
||||
await queryRunner.query(`DROP TABLE "tax_rate"`)
|
||||
}
|
||||
}
|
||||
@@ -124,6 +124,8 @@ export class Cart {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
readonly object = "cart"
|
||||
|
||||
@Column({ nullable: true })
|
||||
email: string
|
||||
|
||||
@@ -245,7 +247,7 @@ export class Cart {
|
||||
|
||||
shipping_total?: number
|
||||
discount_total?: number
|
||||
tax_total?: number
|
||||
tax_total?: number | null
|
||||
refunded_total?: number
|
||||
total?: number
|
||||
subtotal?: number
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
Index,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { TaxLine } from "./tax-line"
|
||||
import { LineItem } from "./line-item"
|
||||
|
||||
@Entity()
|
||||
export class LineItemTaxLine extends TaxLine {
|
||||
@Index()
|
||||
@Column()
|
||||
item_id: string
|
||||
|
||||
@ManyToOne(() => LineItem, (li) => li.tax_lines)
|
||||
@JoinColumn({ name: "item_id" })
|
||||
item: LineItem
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `litl_${id}`
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Entity,
|
||||
OneToMany,
|
||||
BeforeInsert,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { LineItemTaxLine } from "./line-item-tax-line"
|
||||
import { Swap } from "./swap"
|
||||
import { Cart } from "./cart"
|
||||
import { Order } from "./order"
|
||||
@@ -32,10 +34,7 @@ export class LineItem {
|
||||
@Column({ nullable: true })
|
||||
cart_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Cart,
|
||||
cart => cart.items
|
||||
)
|
||||
@ManyToOne(() => Cart, (cart) => cart.items)
|
||||
@JoinColumn({ name: "cart_id" })
|
||||
cart: Cart
|
||||
|
||||
@@ -43,10 +42,7 @@ export class LineItem {
|
||||
@Column({ nullable: true })
|
||||
order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Order,
|
||||
order => order.items
|
||||
)
|
||||
@ManyToOne(() => Order, (order) => order.items)
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@@ -54,10 +50,7 @@ export class LineItem {
|
||||
@Column({ nullable: true })
|
||||
swap_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Swap,
|
||||
swap => swap.additional_items
|
||||
)
|
||||
@ManyToOne(() => Swap, (swap) => swap.additional_items)
|
||||
@JoinColumn({ name: "swap_id" })
|
||||
swap: Swap
|
||||
|
||||
@@ -65,13 +58,13 @@ export class LineItem {
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => ClaimOrder,
|
||||
co => co.additional_items
|
||||
)
|
||||
@ManyToOne(() => ClaimOrder, (co) => co.additional_items)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@OneToMany(() => LineItemTaxLine, (tl) => tl.item, { cascade: ["insert"] })
|
||||
tax_lines: LineItemTaxLine[]
|
||||
|
||||
@Column()
|
||||
title: string
|
||||
|
||||
@@ -81,6 +74,9 @@ export class LineItem {
|
||||
@Column({ nullable: true })
|
||||
thumbnail: string
|
||||
|
||||
@Column({ default: false })
|
||||
is_return: boolean
|
||||
|
||||
@Column({ default: false })
|
||||
is_giftcard: boolean
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ export class Order {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
readonly object = "order"
|
||||
|
||||
@DbAwareColumn({ type: "enum", enum: OrderStatus, default: "pending" })
|
||||
status: OrderStatus
|
||||
|
||||
@@ -142,8 +144,8 @@ export class Order {
|
||||
@JoinColumn({ name: "currency_code", referencedColumnName: "code" })
|
||||
currency: Currency
|
||||
|
||||
@Column({ type: "real" })
|
||||
tax_rate: number
|
||||
@Column({ type: "real", nullable: true })
|
||||
tax_rate: number | null
|
||||
|
||||
@ManyToMany(() => Discount, { cascade: ["insert"] })
|
||||
@JoinTable({
|
||||
@@ -173,57 +175,29 @@ export class Order {
|
||||
})
|
||||
gift_cards: GiftCard[]
|
||||
|
||||
@OneToMany(
|
||||
() => ShippingMethod,
|
||||
(method) => method.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
)
|
||||
@OneToMany(() => ShippingMethod, (method) => method.order, {
|
||||
cascade: ["insert"],
|
||||
})
|
||||
shipping_methods: ShippingMethod[]
|
||||
|
||||
@OneToMany(
|
||||
() => Payment,
|
||||
(payment) => payment.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
@OneToMany(() => Payment, (payment) => payment.order, { cascade: ["insert"] })
|
||||
payments: Payment[]
|
||||
|
||||
@OneToMany(
|
||||
() => Fulfillment,
|
||||
(fulfillment) => fulfillment.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
)
|
||||
@OneToMany(() => Fulfillment, (fulfillment) => fulfillment.order, {
|
||||
cascade: ["insert"],
|
||||
})
|
||||
fulfillments: Fulfillment[]
|
||||
|
||||
@OneToMany(
|
||||
() => Return,
|
||||
(ret) => ret.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
@OneToMany(() => Return, (ret) => ret.order, { cascade: ["insert"] })
|
||||
returns: Return[]
|
||||
|
||||
@OneToMany(
|
||||
() => ClaimOrder,
|
||||
(co) => co.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
@OneToMany(() => ClaimOrder, (co) => co.order, { cascade: ["insert"] })
|
||||
claims: ClaimOrder[]
|
||||
|
||||
@OneToMany(
|
||||
() => Refund,
|
||||
(ref) => ref.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
@OneToMany(() => Refund, (ref) => ref.order, { cascade: ["insert"] })
|
||||
refunds: Refund[]
|
||||
|
||||
@OneToMany(
|
||||
() => Swap,
|
||||
(swap) => swap.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
@OneToMany(() => Swap, (swap) => swap.order, { cascade: ["insert"] })
|
||||
swaps: Swap[]
|
||||
|
||||
@Column({ nullable: true })
|
||||
@@ -233,19 +207,12 @@ export class Order {
|
||||
@JoinColumn({ name: "draft_order_id" })
|
||||
draft_order: DraftOrder
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
(lineItem) => lineItem.order,
|
||||
{
|
||||
cascade: ["insert"],
|
||||
}
|
||||
)
|
||||
@OneToMany(() => LineItem, (lineItem) => lineItem.order, {
|
||||
cascade: ["insert"],
|
||||
})
|
||||
items: LineItem[]
|
||||
|
||||
@OneToMany(
|
||||
() => GiftCardTransaction,
|
||||
(gc) => gc.order
|
||||
)
|
||||
@OneToMany(() => GiftCardTransaction, (gc) => gc.order)
|
||||
gift_card_transactions: GiftCardTransaction[]
|
||||
|
||||
@Column({ nullable: true, type: resolveDbType("timestamptz") })
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Entity,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { Product } from "./product"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
|
||||
@Entity()
|
||||
export class ProductTaxRate {
|
||||
@PrimaryColumn()
|
||||
product_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
rate_id: string
|
||||
|
||||
@ManyToOne(() => Product, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_id" })
|
||||
product?: Product
|
||||
|
||||
// Note the onDelete config here
|
||||
@ManyToOne(() => TaxRate, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "rate_id" })
|
||||
tax_rate?: TaxRate
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema product_tax_rate
|
||||
* title: "Product Tax Rate"
|
||||
* description: "Associates a tax rate with a product to indicate that the product is taxed in a certain way"
|
||||
* x-resourceId: product_tax_rate
|
||||
* properties:
|
||||
* product_id:
|
||||
* description: "The id of the Product"
|
||||
* type: string
|
||||
* rate_id:
|
||||
* description: "The id of the Tax Rate"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Entity,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { ProductType } from "./product-type"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
|
||||
@Entity()
|
||||
export class ProductTypeTaxRate {
|
||||
@PrimaryColumn()
|
||||
product_type_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
rate_id: string
|
||||
|
||||
@ManyToOne(() => ProductType, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "product_type_id" })
|
||||
product_type?: ProductType
|
||||
|
||||
@ManyToOne(() => TaxRate, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "rate_id" })
|
||||
tax_rate?: TaxRate
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema product_type_tax_rate
|
||||
* title: "Product Type Tax Rate"
|
||||
* description: "Associates a tax rate with a product type to indicate that the product type is taxed in a certain way"
|
||||
* x-resourceId: product_type_tax_rate
|
||||
* properties:
|
||||
* product_type_id:
|
||||
* description: "The id of the Product type"
|
||||
* type: string
|
||||
* rate_id:
|
||||
* description: "The id of the Tax Rate"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -16,9 +16,11 @@ import { ulid } from "ulid"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { Currency } from "./currency"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
import { Country } from "./country"
|
||||
import { PaymentProvider } from "./payment-provider"
|
||||
import { FulfillmentProvider } from "./fulfillment-provider"
|
||||
import { TaxProvider } from "./tax-provider"
|
||||
|
||||
@Entity()
|
||||
export class Region {
|
||||
@@ -38,16 +40,32 @@ export class Region {
|
||||
@Column({ type: "real" })
|
||||
tax_rate: number
|
||||
|
||||
@OneToMany(() => TaxRate, (tr) => tr.region)
|
||||
tax_rates: TaxRate[] | null
|
||||
|
||||
@Column({ nullable: true })
|
||||
tax_code: string
|
||||
|
||||
@OneToMany(
|
||||
() => Country,
|
||||
c => c.region
|
||||
)
|
||||
@Column({ default: true })
|
||||
gift_cards_taxable: boolean
|
||||
|
||||
@Column({ default: true })
|
||||
automatic_taxes: boolean
|
||||
|
||||
@OneToMany(() => Country, (c) => c.region)
|
||||
countries: Country[]
|
||||
|
||||
@ManyToMany(() => PaymentProvider, { eager: true, cascade: ['insert', 'update'] })
|
||||
@Column({ type: "text", nullable: true })
|
||||
tax_provider_id: string | null
|
||||
|
||||
@ManyToOne(() => TaxProvider)
|
||||
@JoinColumn({ name: "tax_provider_id" })
|
||||
tax_provider: TaxProvider
|
||||
|
||||
@ManyToMany(() => PaymentProvider, {
|
||||
eager: true,
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
@JoinTable({
|
||||
name: "region_payment_providers",
|
||||
joinColumn: {
|
||||
@@ -61,7 +79,10 @@ export class Region {
|
||||
})
|
||||
payment_providers: PaymentProvider[]
|
||||
|
||||
@ManyToMany(() => FulfillmentProvider, { eager: true, cascade: ['insert', 'update'] })
|
||||
@ManyToMany(() => FulfillmentProvider, {
|
||||
eager: true,
|
||||
cascade: ["insert", "update"],
|
||||
})
|
||||
@JoinTable({
|
||||
name: "region_fulfillment_providers",
|
||||
joinColumn: {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
Index,
|
||||
Column,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { TaxLine } from "./tax-line"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
|
||||
@Entity()
|
||||
export class ShippingMethodTaxLine extends TaxLine {
|
||||
@Index()
|
||||
@Column()
|
||||
shipping_method_id: string
|
||||
|
||||
@ManyToOne(() => ShippingMethod, (sm) => sm.tax_lines)
|
||||
@JoinColumn({ name: "shipping_method_id" })
|
||||
shipping_method: ShippingMethod
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `smtl_${id}`
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PrimaryColumn,
|
||||
ManyToOne,
|
||||
OneToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
Index,
|
||||
} from "typeorm"
|
||||
@@ -18,6 +19,7 @@ import { Cart } from "./cart"
|
||||
import { Swap } from "./swap"
|
||||
import { Return } from "./return"
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
import { ShippingMethodTaxLine } from "./shipping-method-tax-line"
|
||||
|
||||
@Check(
|
||||
`"claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
|
||||
@@ -68,10 +70,7 @@ export class ShippingMethod {
|
||||
@Column({ nullable: true })
|
||||
return_id: string
|
||||
|
||||
@OneToOne(
|
||||
() => Return,
|
||||
ret => ret.shipping_method
|
||||
)
|
||||
@OneToOne(() => Return, (ret) => ret.shipping_method)
|
||||
@JoinColumn({ name: "return_id" })
|
||||
return_order: Return
|
||||
|
||||
@@ -79,6 +78,11 @@ export class ShippingMethod {
|
||||
@JoinColumn({ name: "shipping_option_id" })
|
||||
shipping_option: ShippingOption
|
||||
|
||||
@OneToMany(() => ShippingMethodTaxLine, (tl) => tl.shipping_method, {
|
||||
cascade: ["insert"],
|
||||
})
|
||||
tax_lines: ShippingMethodTaxLine[]
|
||||
|
||||
@Column({ type: "int" })
|
||||
price: number
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
Entity,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
import { TaxRate } from "./tax-rate"
|
||||
|
||||
@Entity()
|
||||
export class ShippingTaxRate {
|
||||
@PrimaryColumn()
|
||||
shipping_option_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
rate_id: string
|
||||
|
||||
@ManyToOne(() => ShippingOption, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "shipping_option_id" })
|
||||
shipping_option?: ShippingOption
|
||||
|
||||
@ManyToOne(() => TaxRate, { onDelete: "CASCADE" })
|
||||
@JoinColumn({ name: "rate_id" })
|
||||
tax_rate?: TaxRate
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema shipping_tax_rate
|
||||
* title: "Shipping Tax Rate"
|
||||
* description: "Associates a tax rate with a shipping option to indicate that the shipping option is taxed in a certain way"
|
||||
* x-resourceId: shipping_tax_rate
|
||||
* properties:
|
||||
* shipping_option_id:
|
||||
* description: "The id of the Shipping Option"
|
||||
* type: string
|
||||
* rate_id:
|
||||
* description: "The id of the Tax Rate"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* deleted_at:
|
||||
* description: "The date with timezone at which the resource was deleted."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -63,35 +63,35 @@ export class Swap {
|
||||
|
||||
@ManyToOne(
|
||||
() => Order,
|
||||
o => o.swaps
|
||||
(o) => o.swaps
|
||||
)
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
item => item.swap,
|
||||
(item) => item.swap,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
additional_items: LineItem
|
||||
additional_items: LineItem[]
|
||||
|
||||
@OneToOne(
|
||||
() => Return,
|
||||
ret => ret.swap,
|
||||
(ret) => ret.swap,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
return_order: Return
|
||||
|
||||
@OneToMany(
|
||||
() => Fulfillment,
|
||||
fulfillment => fulfillment.swap,
|
||||
(fulfillment) => fulfillment.swap,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
fulfillments: Fulfillment[]
|
||||
|
||||
@OneToOne(
|
||||
() => Payment,
|
||||
p => p.swap,
|
||||
(p) => p.swap,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
payment: Payment
|
||||
@@ -108,7 +108,7 @@ export class Swap {
|
||||
|
||||
@OneToMany(
|
||||
() => ShippingMethod,
|
||||
method => method.swap,
|
||||
(method) => method.swap,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
shipping_methods: ShippingMethod[]
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
export class TaxLine {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ type: "real" })
|
||||
rate: number
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
code: string | null
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema tax_line
|
||||
* title: "Tax Line"
|
||||
* description: "Line item that specifies an amount of tax to add to a line item."
|
||||
* x-resourceId: tax_line
|
||||
* properties:
|
||||
* id:
|
||||
* description: "The id of the Tax Line. This value will be prefixed by `tl_`."
|
||||
* type: string
|
||||
* code:
|
||||
* description: "A code to identify the tax type by"
|
||||
* type: string
|
||||
* name:
|
||||
* description: "A human friendly name for the tax"
|
||||
* type: string
|
||||
* rate:
|
||||
* description: "The numeric rate to charge tax by"
|
||||
* type: number
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
*/
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm"
|
||||
|
||||
@Entity()
|
||||
export class TaxProvider {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ default: true })
|
||||
is_installed: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema tax_provider
|
||||
* title: "Tax Provider"
|
||||
* description: "The tax service used to calculate taxes"
|
||||
* x-resourceId: tax_provider
|
||||
* properties:
|
||||
* id:
|
||||
* description: "The id of the tax provider as given by the plugin."
|
||||
* type: string
|
||||
* is_installed:
|
||||
* description: "Whether the plugin is installed in the current version. Plugins that are no longer installed are not deleted by will have this field set to `false`."
|
||||
* type: boolean
|
||||
*/
|
||||
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
ManyToOne,
|
||||
ManyToMany,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
|
||||
|
||||
import { Region } from "./region"
|
||||
import { Product } from "./product"
|
||||
import { ProductType } from "./product-type"
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
|
||||
@Entity()
|
||||
export class TaxRate {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ type: "real", nullable: true })
|
||||
rate: number | null
|
||||
|
||||
@Column({ type: "varchar", nullable: true })
|
||||
code: string | null
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
region_id: string
|
||||
|
||||
@ManyToOne(() => Region)
|
||||
@JoinColumn({ name: "region_id" })
|
||||
region: Region
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
updated_at: Date
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@ManyToMany(() => Product)
|
||||
@JoinTable({
|
||||
name: "product_tax_rate",
|
||||
joinColumn: {
|
||||
name: "rate_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
products: Product[]
|
||||
|
||||
@ManyToMany(() => ProductType)
|
||||
@JoinTable({
|
||||
name: "product_type_tax_rate",
|
||||
joinColumn: {
|
||||
name: "rate_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_type_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
product_types: ProductType[]
|
||||
|
||||
@ManyToMany(() => ShippingOption)
|
||||
@JoinTable({
|
||||
name: "shipping_tax_rate",
|
||||
joinColumn: {
|
||||
name: "rate_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "shipping_option_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
shipping_options: ShippingOption[]
|
||||
|
||||
// TODO: consider custom DTO instead
|
||||
product_count?: number
|
||||
product_type_count?: number
|
||||
shipping_option_count?: number
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `txr_${id}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @schema tax_rate
|
||||
* title: "Tax Rate"
|
||||
* description: "A Tax Rate can be used to associate a certain rate to charge on products within a given Region"
|
||||
* x-resourceId: line_item
|
||||
* properties:
|
||||
* id:
|
||||
* description: "The id of the Tax Rate. This value will be prefixed by `txr_`."
|
||||
* type: string
|
||||
* rate:
|
||||
* description: "The numeric rate to charge"
|
||||
* type: number
|
||||
* code:
|
||||
* description: "A code to identify the tax type by"
|
||||
* type: string
|
||||
* name:
|
||||
* description: "A human friendly name for the tax"
|
||||
* type: string
|
||||
* region_id:
|
||||
* description: "The id of the Region that the rate belongs to"
|
||||
* type: string
|
||||
* created_at:
|
||||
* description: "The date with timezone at which the resource was created."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* updated_at:
|
||||
* description: "The date with timezone at which the resource was last updated."
|
||||
* type: string
|
||||
* format: date-time
|
||||
* metadata:
|
||||
* description: "An optional key-value map with additional information."
|
||||
* type: object
|
||||
* refundable:
|
||||
* description: "The amount that can be refunded from the given Line Item. Takes taxes and discounts into consideration."
|
||||
* type: integer
|
||||
*/
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
|
||||
@EntityRepository(LineItemTaxLine)
|
||||
export class LineItemTaxLineRepository extends Repository<LineItemTaxLine> {}
|
||||
@@ -1,5 +1,30 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { ReturnItem } from "../models/return-item"
|
||||
|
||||
@EntityRepository(LineItem)
|
||||
export class LineItemRepository extends Repository<LineItem> {}
|
||||
export class LineItemRepository extends Repository<LineItem> {
|
||||
/**
|
||||
* Finds line items that are to be returned as part of the Return represented
|
||||
* by `returnId`. The function joins the associated ReturnItem and the
|
||||
* LineItem's TaxLines.
|
||||
* @param returnId - the id of the Return to get LineItems by
|
||||
* @return the LineItems associated with the Return with its ReturnItem joined
|
||||
* and mapped to `return_item`.
|
||||
*/
|
||||
async findByReturn(
|
||||
returnId: string
|
||||
): Promise<(LineItem & { return_item: ReturnItem })[]> {
|
||||
const qb = this.createQueryBuilder("li")
|
||||
.leftJoinAndSelect(`li.tax_lines`, "tax_lines")
|
||||
.leftJoinAndMapOne(
|
||||
`li.return_item`,
|
||||
ReturnItem,
|
||||
`ri`,
|
||||
`ri.item_id = li.id`
|
||||
)
|
||||
.where(`ri.return_id = :returnId`, { returnId })
|
||||
|
||||
return (await qb.getMany()) as (LineItem & { return_item: ReturnItem })[]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ProductTaxRate } from "../models/product-tax-rate"
|
||||
|
||||
@EntityRepository(ProductTaxRate)
|
||||
export class ProductTaxRateRepository extends Repository<ProductTaxRate> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
|
||||
@EntityRepository(ShippingMethodTaxLine)
|
||||
export class ShippingMethodTaxLineRepository extends Repository<ShippingMethodTaxLine> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
|
||||
@EntityRepository(ShippingTaxRate)
|
||||
export class ShippingTaxRateRepository extends Repository<ShippingTaxRate> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { TaxProvider } from "../models/tax-provider"
|
||||
|
||||
@EntityRepository(TaxProvider)
|
||||
export class TaxProviderRepository extends Repository<TaxProvider> {}
|
||||
@@ -0,0 +1,274 @@
|
||||
import { unionBy } from "lodash"
|
||||
import {
|
||||
In,
|
||||
Not,
|
||||
DeleteResult,
|
||||
SelectQueryBuilder,
|
||||
EntityRepository,
|
||||
FindManyOptions,
|
||||
FindOptionsUtils,
|
||||
Repository,
|
||||
} from "typeorm"
|
||||
import { TaxRate } from "../models/tax-rate"
|
||||
import { ProductTaxRate } from "../models/product-tax-rate"
|
||||
import { ProductTypeTaxRate } from "../models/product-type-tax-rate"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
import { Product } from "../models/product"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { TaxRateListByConfig } from "../types/tax-rate"
|
||||
|
||||
const resolveableFields = [
|
||||
"product_count",
|
||||
"product_type_count",
|
||||
"shipping_option_count",
|
||||
]
|
||||
|
||||
@EntityRepository(TaxRate)
|
||||
export class TaxRateRepository extends Repository<TaxRate> {
|
||||
getFindQueryBuilder(findOptions: FindManyOptions<TaxRate>) {
|
||||
let qb = this.createQueryBuilder("tr")
|
||||
const cleanOptions = findOptions
|
||||
|
||||
const resolverFields: string[] = []
|
||||
if (typeof findOptions.select !== "undefined") {
|
||||
let selectableCols: (keyof TaxRate)[] = []
|
||||
for (const k of findOptions.select) {
|
||||
if (!resolveableFields.includes(k)) {
|
||||
selectableCols.push(k)
|
||||
} else {
|
||||
resolverFields.push(k)
|
||||
}
|
||||
}
|
||||
cleanOptions.select = selectableCols
|
||||
}
|
||||
|
||||
FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(
|
||||
qb,
|
||||
cleanOptions
|
||||
)
|
||||
|
||||
if (resolverFields.length > 0) {
|
||||
this.applyResolutionsToQueryBuilder(qb, resolverFields)
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
async findWithResolution(findOptions: FindManyOptions<TaxRate>) {
|
||||
const qb = this.getFindQueryBuilder(findOptions)
|
||||
return await qb.getMany()
|
||||
}
|
||||
|
||||
async findOneWithResolution(findOptions: FindManyOptions<TaxRate>) {
|
||||
const qb = this.getFindQueryBuilder(findOptions)
|
||||
return await qb.getOne()
|
||||
}
|
||||
|
||||
async findAndCountWithResolution(findOptions: FindManyOptions<TaxRate>) {
|
||||
const qb = this.getFindQueryBuilder(findOptions)
|
||||
return await qb.getManyAndCount()
|
||||
}
|
||||
|
||||
applyResolutionsToQueryBuilder(
|
||||
qb: SelectQueryBuilder<TaxRate>,
|
||||
resolverFields: string[]
|
||||
): SelectQueryBuilder<TaxRate> {
|
||||
for (const k of resolverFields) {
|
||||
switch (k) {
|
||||
case "product_count":
|
||||
qb.loadRelationCountAndMap(
|
||||
`${qb.alias}.product_count`,
|
||||
`${qb.alias}.products`
|
||||
)
|
||||
break
|
||||
case "product_type_count":
|
||||
qb.loadRelationCountAndMap(
|
||||
`${qb.alias}.product_type_count`,
|
||||
`${qb.alias}.product_types`
|
||||
)
|
||||
break
|
||||
case "shipping_option_count":
|
||||
qb.loadRelationCountAndMap(
|
||||
`${qb.alias}.shipping_option_count`,
|
||||
`${qb.alias}.shipping_options`
|
||||
)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return qb
|
||||
}
|
||||
|
||||
async removeFromProduct(
|
||||
id: string,
|
||||
productIds: string[]
|
||||
): Promise<DeleteResult> {
|
||||
return await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ProductTaxRate)
|
||||
.where({ rate_id: id, product_id: In(productIds) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async addToProduct(
|
||||
id: string,
|
||||
productIds: string[],
|
||||
overrideExisting: boolean = false
|
||||
): Promise<ProductTaxRate[]> {
|
||||
const toInsert = productIds.map((pId) => ({ rate_id: id, product_id: pId }))
|
||||
const insertResult = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.orIgnore(true)
|
||||
.into(ProductTaxRate)
|
||||
.values(toInsert)
|
||||
.execute()
|
||||
|
||||
if (overrideExisting) {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ProductTaxRate)
|
||||
.where({ rate_id: id, product_id: Not(In(productIds)) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(ProductTaxRate, "ptr")
|
||||
.select()
|
||||
.where(insertResult.identifiers)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async removeFromProductType(
|
||||
id: string,
|
||||
productTypeIds: string[]
|
||||
): Promise<DeleteResult> {
|
||||
return await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ProductTypeTaxRate)
|
||||
.where({ rate_id: id, product_type_id: In(productTypeIds) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async addToProductType(
|
||||
id: string,
|
||||
productTypeIds: string[],
|
||||
overrideExisting: boolean = false
|
||||
): Promise<ProductTypeTaxRate[]> {
|
||||
const toInsert = productTypeIds.map((pId) => ({
|
||||
rate_id: id,
|
||||
product_type_id: pId,
|
||||
}))
|
||||
const insertResult = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.orIgnore(true)
|
||||
.into(ProductTypeTaxRate)
|
||||
.values(toInsert)
|
||||
.execute()
|
||||
|
||||
if (overrideExisting) {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ProductTypeTaxRate)
|
||||
.where({ rate_id: id, product_type_id: Not(In(productTypeIds)) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(ProductTypeTaxRate, "ptr")
|
||||
.select()
|
||||
.where(insertResult.identifiers)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async removeFromShippingOption(
|
||||
id: string,
|
||||
optionIds: string[]
|
||||
): Promise<DeleteResult> {
|
||||
return await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ShippingTaxRate)
|
||||
.where({ rate_id: id, shipping_option_id: In(optionIds) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
async addToShippingOption(
|
||||
id: string,
|
||||
optionIds: string[],
|
||||
overrideExisting: boolean = false
|
||||
): Promise<ShippingTaxRate[]> {
|
||||
const toInsert = optionIds.map((pId) => ({
|
||||
rate_id: id,
|
||||
shipping_option_id: pId,
|
||||
}))
|
||||
const insertResult = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.orIgnore(true)
|
||||
.into(ShippingTaxRate)
|
||||
.values(toInsert)
|
||||
.execute()
|
||||
|
||||
if (overrideExisting) {
|
||||
await this.createQueryBuilder()
|
||||
.delete()
|
||||
.from(ShippingTaxRate)
|
||||
.where({ rate_id: id, shipping_option_id: Not(In(optionIds)) })
|
||||
.execute()
|
||||
}
|
||||
|
||||
return await this.manager
|
||||
.createQueryBuilder(ShippingTaxRate, "str")
|
||||
.select()
|
||||
.where(insertResult.identifiers)
|
||||
.getMany()
|
||||
}
|
||||
|
||||
async listByProduct(productId: string, config: TaxRateListByConfig) {
|
||||
let productRates = this.createQueryBuilder("txr")
|
||||
.leftJoin(ProductTaxRate, "ptr", "ptr.rate_id = txr.id")
|
||||
.leftJoin(Product, "prod", "prod.id = ptr.product_id")
|
||||
.where("prod.id = :productId", { productId })
|
||||
|
||||
let typeRates = this.createQueryBuilder("txr")
|
||||
.leftJoin(ProductTypeTaxRate, "pttr", "pttr.rate_id = txr.id")
|
||||
.leftJoin(Product, "prod", "prod.type_id = pttr.product_type_id")
|
||||
.where("prod.id = :productId", { productId })
|
||||
|
||||
if (typeof config.region_id !== "undefined") {
|
||||
productRates.andWhere("txr.region_id = :regionId", {
|
||||
regionId: config.region_id,
|
||||
})
|
||||
typeRates.andWhere("txr.region_id = :regionId", {
|
||||
regionId: config.region_id,
|
||||
})
|
||||
}
|
||||
|
||||
const results = await Promise.all([
|
||||
productRates.getMany(),
|
||||
typeRates.getMany(),
|
||||
])
|
||||
|
||||
// Only return unique rates by joining the two arrays from typeRates and
|
||||
// productRates matching based on the id
|
||||
return unionBy(...results, (txr) => txr.id)
|
||||
}
|
||||
|
||||
async listByShippingOption(optionId: string, config: TaxRateListByConfig) {
|
||||
let rates = this.createQueryBuilder("txr")
|
||||
.leftJoin(ShippingTaxRate, "ptr", "ptr.rate_id = txr.id")
|
||||
.leftJoin(
|
||||
ShippingMethod,
|
||||
"sm",
|
||||
"sm.shipping_option_id = ptr.shipping_option_id"
|
||||
)
|
||||
.where("sm.shipping_option_id = :optionId", { optionId })
|
||||
|
||||
if (typeof config.region_id !== "undefined") {
|
||||
rates.andWhere("txr.region_id = :regionId", {
|
||||
regionId: config.region_id,
|
||||
})
|
||||
}
|
||||
return await rates.getMany()
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export const shippingOptions = {
|
||||
}
|
||||
|
||||
export const ShippingOptionServiceMock = {
|
||||
retrieve: jest.fn().mockImplementation(optionId => {
|
||||
retrieve: jest.fn().mockImplementation((optionId) => {
|
||||
if (optionId === IdMap.getId("return-shipping")) {
|
||||
return Promise.resolve(shippingOptions.returnShipping)
|
||||
}
|
||||
@@ -86,9 +86,21 @@ export const ShippingOptionServiceMock = {
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
listAndCount: jest.fn().mockImplementation((data) => {
|
||||
if (data.region_id === IdMap.getId("region-france")) {
|
||||
return Promise.resolve([[shippingOptions.franceShipping], 1])
|
||||
}
|
||||
if (data.region_id === IdMap.getId("testRegion")) {
|
||||
return Promise.resolve([
|
||||
[shippingOptions.freeShipping, shippingOptions.expensiveShipping],
|
||||
2,
|
||||
])
|
||||
}
|
||||
return Promise.resolve([[], 0])
|
||||
}),
|
||||
list: jest.fn().mockImplementation((data) => {
|
||||
if (!data) {
|
||||
return Promise.resolve()
|
||||
return Promise.resolve([])
|
||||
}
|
||||
if (data.region_id === IdMap.getId("region-france")) {
|
||||
return Promise.resolve([shippingOptions.franceShipping])
|
||||
@@ -100,7 +112,7 @@ export const ShippingOptionServiceMock = {
|
||||
])
|
||||
}
|
||||
}),
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve(data)
|
||||
}),
|
||||
validateFulfillmentData: jest
|
||||
|
||||
@@ -655,6 +655,8 @@ describe("CartService", () => {
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"discounts.regions",
|
||||
"items.tax_lines",
|
||||
"region.tax_rates",
|
||||
],
|
||||
{
|
||||
where: { id: "withpays" },
|
||||
|
||||
@@ -5,13 +5,14 @@ import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("eventBus")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const totalsService = {
|
||||
getCalculationContext: jest.fn(() => {}),
|
||||
getRefundTotal: jest.fn(() => 1000),
|
||||
}
|
||||
|
||||
@@ -54,12 +55,20 @@ describe("ClaimService", () => {
|
||||
}
|
||||
|
||||
const claimRepo = MockRepository({
|
||||
create: d => ({ id: "claim_134", ...d }),
|
||||
create: (d) => ({ id: "claim_134", ...d }),
|
||||
})
|
||||
|
||||
const taxProviderService = {
|
||||
createTaxLines: jest.fn(),
|
||||
withTransaction: function () {
|
||||
withTransactionMock("return")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const returnService = {
|
||||
create: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("return")
|
||||
return this
|
||||
},
|
||||
@@ -68,7 +77,8 @@ describe("ClaimService", () => {
|
||||
const lineItemService = {
|
||||
generate: jest.fn((d, _, q) => ({ variant_id: d, quantity: q })),
|
||||
retrieve: () => Promise.resolve({}),
|
||||
withTransaction: function() {
|
||||
list: () => Promise.resolve([{}]),
|
||||
withTransaction: function () {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
@@ -76,7 +86,7 @@ describe("ClaimService", () => {
|
||||
|
||||
const inventoryService = {
|
||||
...InventoryServiceMock,
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("inventory")
|
||||
return this
|
||||
},
|
||||
@@ -84,7 +94,7 @@ describe("ClaimService", () => {
|
||||
|
||||
const claimItemService = {
|
||||
create: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("claimItem")
|
||||
return this
|
||||
},
|
||||
@@ -93,6 +103,7 @@ describe("ClaimService", () => {
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
taxProviderService,
|
||||
totalsService,
|
||||
returnService,
|
||||
lineItemService,
|
||||
@@ -303,7 +314,7 @@ describe("ClaimService", () => {
|
||||
createFulfillment: jest.fn((_, items) =>
|
||||
Promise.resolve([{ id: "ful", items }])
|
||||
),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
@@ -339,7 +350,7 @@ describe("ClaimService", () => {
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
retrieve: () => Promise.resolve({}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
@@ -440,11 +451,11 @@ describe("ClaimService", () => {
|
||||
describe("cancelFulfillment", () => {
|
||||
const claimRepo = MockRepository({
|
||||
findOne: () => Promise.resolve({}),
|
||||
save: f => Promise.resolve(f),
|
||||
save: (f) => Promise.resolve(f),
|
||||
})
|
||||
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn().mockImplementation(f => {
|
||||
cancelFulfillment: jest.fn().mockImplementation((f) => {
|
||||
switch (f) {
|
||||
case IdMap.getId("no-claim"):
|
||||
return Promise.resolve({})
|
||||
@@ -454,7 +465,7 @@ describe("ClaimService", () => {
|
||||
})
|
||||
}
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -519,7 +530,7 @@ describe("ClaimService", () => {
|
||||
],
|
||||
})
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
@@ -528,7 +539,7 @@ describe("ClaimService", () => {
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
retrieve: () => Promise.resolve({}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
@@ -623,7 +634,7 @@ describe("ClaimService", () => {
|
||||
describe("cancel", () => {
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
@@ -631,7 +642,7 @@ describe("ClaimService", () => {
|
||||
|
||||
const returnService = {
|
||||
cancel: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
withTransactionMock("return")
|
||||
return this
|
||||
},
|
||||
@@ -642,7 +653,7 @@ describe("ClaimService", () => {
|
||||
const fulfillment = { id: "ful_21", canceled_at: now }
|
||||
|
||||
const claimRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOne: (q) => {
|
||||
const claim = {
|
||||
return_order: { ...ret_order },
|
||||
fulfillments: [{ ...fulfillment }],
|
||||
|
||||
@@ -5,35 +5,35 @@ import { InventoryServiceMock } from "../__mocks__/inventory"
|
||||
describe("OrderService", () => {
|
||||
const totalsService = {
|
||||
getLineItemRefund: () => {},
|
||||
getTotal: o => {
|
||||
getTotal: (o) => {
|
||||
return o.total || 0
|
||||
},
|
||||
getRefundedTotal: o => {
|
||||
getRefundedTotal: (o) => {
|
||||
return o.refunded_total || 0
|
||||
},
|
||||
getShippingTotal: o => {
|
||||
getShippingTotal: (o) => {
|
||||
return o.shipping_total || 0
|
||||
},
|
||||
getGiftCardTotal: o => {
|
||||
getGiftCardTotal: (o) => {
|
||||
return o.gift_card_total || 0
|
||||
},
|
||||
getDiscountTotal: o => {
|
||||
getDiscountTotal: (o) => {
|
||||
return o.discount_total || 0
|
||||
},
|
||||
getTaxTotal: o => {
|
||||
getTaxTotal: (o) => {
|
||||
return o.tax_total || 0
|
||||
},
|
||||
getSubtotal: o => {
|
||||
getSubtotal: (o) => {
|
||||
return o.subtotal || 0
|
||||
},
|
||||
getPaidTotal: o => {
|
||||
getPaidTotal: (o) => {
|
||||
return o.paid_total || 0
|
||||
},
|
||||
}
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -43,7 +43,7 @@ describe("OrderService", () => {
|
||||
}
|
||||
|
||||
describe("create", () => {
|
||||
const orderRepo = MockRepository({ create: f => f })
|
||||
const orderRepo = MockRepository({ create: (f) => f })
|
||||
const orderService = new OrderService({
|
||||
manager: MockManager,
|
||||
orderRepository: orderRepo,
|
||||
@@ -73,37 +73,37 @@ describe("OrderService", () => {
|
||||
|
||||
describe("createFromCart", () => {
|
||||
const orderRepo = MockRepository({
|
||||
create: p => p,
|
||||
save: p => ({ ...p, id: "id" }),
|
||||
create: (p) => p,
|
||||
save: (p) => ({ ...p, id: "id" }),
|
||||
})
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const shippingOptionService = {
|
||||
updateShippingMethod: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const giftCardService = {
|
||||
update: jest.fn(),
|
||||
createTransaction: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
const paymentProviderService = {
|
||||
getStatus: payment => {
|
||||
getStatus: (payment) => {
|
||||
return Promise.resolve(payment.status || "authorized")
|
||||
},
|
||||
updatePayment: jest.fn(),
|
||||
cancelPayment: jest.fn().mockImplementation(payment => {
|
||||
cancelPayment: jest.fn().mockImplementation((payment) => {
|
||||
return Promise.resolve({ ...payment, status: "cancelled" })
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -113,7 +113,7 @@ describe("OrderService", () => {
|
||||
total: 0,
|
||||
}
|
||||
const cartService = {
|
||||
retrieve: jest.fn().mockImplementation(query => {
|
||||
retrieve: jest.fn().mockImplementation((query) => {
|
||||
if (query === "empty") {
|
||||
return Promise.resolve(emptyCart)
|
||||
}
|
||||
@@ -142,7 +142,7 @@ describe("OrderService", () => {
|
||||
total: 100,
|
||||
})
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -211,14 +211,12 @@ describe("OrderService", () => {
|
||||
email: cart.email,
|
||||
customer_id: cart.customer_id,
|
||||
shipping_methods: cart.shipping_methods,
|
||||
customer_id: "cus_1234",
|
||||
discounts: cart.discounts,
|
||||
billing_address_id: cart.billing_address_id,
|
||||
shipping_address_id: cart.shipping_address_id,
|
||||
region_id: cart.region_id,
|
||||
currency_code: "eur",
|
||||
cart_id: "cart_id",
|
||||
tax_rate: 25,
|
||||
gift_cards: [],
|
||||
metadata: {},
|
||||
}
|
||||
@@ -316,14 +314,12 @@ describe("OrderService", () => {
|
||||
email: cart.email,
|
||||
customer_id: cart.customer_id,
|
||||
shipping_methods: cart.shipping_methods,
|
||||
customer_id: "cus_1234",
|
||||
discounts: cart.discounts,
|
||||
billing_address_id: cart.billing_address_id,
|
||||
shipping_address_id: cart.shipping_address_id,
|
||||
region_id: cart.region_id,
|
||||
currency_code: "eur",
|
||||
cart_id: "cart_id",
|
||||
tax_rate: 25,
|
||||
gift_cards: [
|
||||
{
|
||||
id: "gid",
|
||||
@@ -403,7 +399,6 @@ describe("OrderService", () => {
|
||||
email: cart.email,
|
||||
customer_id: cart.customer_id,
|
||||
shipping_methods: cart.shipping_methods,
|
||||
customer_id: "cus_1234",
|
||||
discounts: cart.discounts,
|
||||
billing_address_id: cart.billing_address_id,
|
||||
shipping_address_id: cart.shipping_address_id,
|
||||
@@ -411,7 +406,6 @@ describe("OrderService", () => {
|
||||
region_id: cart.region_id,
|
||||
currency_code: "eur",
|
||||
cart_id: "cart_id",
|
||||
tax_rate: 25,
|
||||
metadata: {},
|
||||
}
|
||||
expect(orderRepo.create).toHaveBeenCalledTimes(1)
|
||||
@@ -471,7 +465,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("retrieve", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOneWithRelations: q => {
|
||||
findOneWithRelations: (q) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
@@ -496,7 +490,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("retrieveByCartId", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOne: (q) => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
@@ -640,14 +634,14 @@ describe("OrderService", () => {
|
||||
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const paymentProviderService = {
|
||||
cancelPayment: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -668,10 +662,7 @@ describe("OrderService", () => {
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
try {
|
||||
const order = await orderService.retrieve(
|
||||
IdMap.getId("not-fulfilled-order")
|
||||
)
|
||||
console.warn(order)
|
||||
await orderService.retrieve(IdMap.getId("not-fulfilled-order"))
|
||||
await orderService.cancel(IdMap.getId("not-fulfilled-order"))
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
@@ -696,6 +687,7 @@ describe("OrderService", () => {
|
||||
expect(orderRepo.save).toHaveBeenCalledWith({
|
||||
fulfillment_status: "canceled",
|
||||
payment_status: "canceled",
|
||||
canceled_at: expect.any(Date),
|
||||
status: "canceled",
|
||||
fulfillments: [{ id: "fulfillment_test", canceled_at: now }],
|
||||
payments: [{ id: "payment_test" }],
|
||||
@@ -741,12 +733,12 @@ describe("OrderService", () => {
|
||||
const paymentProviderService = {
|
||||
capturePayment: jest
|
||||
.fn()
|
||||
.mockImplementation(p =>
|
||||
.mockImplementation((p) =>
|
||||
p.id === "payment_fail"
|
||||
? Promise.reject()
|
||||
: Promise.resolve({ ...p, captured_at: "notnull" })
|
||||
),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -851,7 +843,7 @@ describe("OrderService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -864,7 +856,7 @@ describe("OrderService", () => {
|
||||
},
|
||||
])
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1017,11 +1009,11 @@ describe("OrderService", () => {
|
||||
describe("cancelFulfillment", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOneWithRelations: () => Promise.resolve({}),
|
||||
save: f => Promise.resolve(f),
|
||||
save: (f) => Promise.resolve(f),
|
||||
})
|
||||
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn().mockImplementation(f => {
|
||||
cancelFulfillment: jest.fn().mockImplementation((f) => {
|
||||
switch (f) {
|
||||
case IdMap.getId("no-order"):
|
||||
return Promise.resolve({})
|
||||
@@ -1031,7 +1023,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
}
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1097,10 +1089,10 @@ describe("OrderService", () => {
|
||||
const paymentProviderService = {
|
||||
refundPayment: jest
|
||||
.fn()
|
||||
.mockImplementation(p =>
|
||||
.mockImplementation((p) =>
|
||||
p.id === "payment_fail" ? Promise.reject() : Promise.resolve()
|
||||
),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1176,7 +1168,7 @@ describe("OrderService", () => {
|
||||
return Promise.resolve({ id: IdMap.getId("order") })
|
||||
}
|
||||
},
|
||||
save: jest.fn().mockImplementation(f => f),
|
||||
save: jest.fn().mockImplementation((f) => f),
|
||||
})
|
||||
|
||||
const eventBus = {
|
||||
@@ -1228,7 +1220,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
}
|
||||
},
|
||||
save: jest.fn().mockImplementation(f => f),
|
||||
save: jest.fn().mockImplementation((f) => f),
|
||||
})
|
||||
|
||||
const optionService = {
|
||||
@@ -1241,7 +1233,7 @@ describe("OrderService", () => {
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve({})),
|
||||
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1375,7 +1367,7 @@ describe("OrderService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1398,7 +1390,7 @@ describe("OrderService", () => {
|
||||
],
|
||||
})
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -1425,9 +1417,7 @@ describe("OrderService", () => {
|
||||
)
|
||||
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1)
|
||||
expect(
|
||||
fulfillmentService.createShipment
|
||||
).toHaveBeenCalledWith(
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
|
||||
IdMap.getId("fulfillment"),
|
||||
[{ tracking_number: "1234" }, { tracking_number: "2345" }],
|
||||
{ metadata: undefined, no_notification: true }
|
||||
@@ -1518,8 +1508,8 @@ describe("OrderService", () => {
|
||||
const paymentProviderService = {
|
||||
refundPayment: jest
|
||||
.fn()
|
||||
.mockImplementation(p => Promise.resolve({ id: "ref" })),
|
||||
withTransaction: function() {
|
||||
.mockImplementation((p) => Promise.resolve({ id: "ref" })),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
@@ -160,6 +160,11 @@ describe("SwapService", () => {
|
||||
|
||||
const cartService = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve({ id: "cart" })),
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ id: "cart", items: [{ id: "test-item" }] })
|
||||
),
|
||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
@@ -173,7 +178,7 @@ describe("SwapService", () => {
|
||||
const customShippingOptionService = {
|
||||
create: jest.fn().mockReturnValue(Promise.resolve({ id: "cso-test" })),
|
||||
update: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
withTransaction: function() {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
@@ -182,6 +187,7 @@ describe("SwapService", () => {
|
||||
create: jest.fn().mockImplementation((d) => Promise.resolve(d)),
|
||||
update: jest.fn().mockImplementation((d) => Promise.resolve(d)),
|
||||
retrieve: () => Promise.resolve({}),
|
||||
createReturnLines: jest.fn(() => Promise.resolve()),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -216,12 +222,19 @@ describe("SwapService", () => {
|
||||
"return_order",
|
||||
"return_order.items",
|
||||
"return_order.shipping_method",
|
||||
"return_order.shipping_method.tax_lines",
|
||||
],
|
||||
{
|
||||
where: { id: IdMap.getId("swap-1") },
|
||||
}
|
||||
)
|
||||
|
||||
expect(lineItemService.createReturnLines).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.createReturnLines).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
"cart"
|
||||
)
|
||||
|
||||
expect(cartService.create).toHaveBeenCalledTimes(1)
|
||||
expect(cartService.create).toHaveBeenCalledWith({
|
||||
email: testOrder.email,
|
||||
@@ -404,126 +417,6 @@ describe("SwapService", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("receiveReturn", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("success", () => {
|
||||
const returnService = {
|
||||
receiveReturn: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ test: "received" })),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const existing = {
|
||||
order_id: IdMap.getId("test"),
|
||||
return_id: IdMap.getId("test"),
|
||||
return_order: {
|
||||
id: IdMap.getId("return-swap"),
|
||||
test: "notreceived",
|
||||
refund_amount: 11,
|
||||
},
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const swapRepo = MockRepository({
|
||||
findOneWithRelations: () => Promise.resolve(existing),
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
|
||||
it("calls register return and updates return value", async () => {
|
||||
await swapService.receiveReturn(IdMap.getId("swap"), [
|
||||
{ item_id: IdMap.getId("1234"), quantity: 1 },
|
||||
])
|
||||
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledWith(
|
||||
IdMap.getId("return-swap"),
|
||||
[{ item_id: IdMap.getId("1234"), quantity: 1 }],
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("failure", () => {
|
||||
const returnService = {
|
||||
receiveReturn: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ status: "requires_action" })),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const existing = {
|
||||
order_id: IdMap.getId("test"),
|
||||
return_id: IdMap.getId("return-swap"),
|
||||
return_order_id: IdMap.getId("return-swap"),
|
||||
return_order: { id: IdMap.getId("return-swap") },
|
||||
other: "data",
|
||||
}
|
||||
|
||||
const swapRepo = MockRepository({
|
||||
findOneWithRelations: (rels, q) => {
|
||||
switch (q.where.id) {
|
||||
case IdMap.getId("canceled"):
|
||||
return Promise.resolve({ canceled_at: new Date() })
|
||||
case IdMap.getId("empty"):
|
||||
return Promise.resolve({})
|
||||
default:
|
||||
return Promise.resolve(existing)
|
||||
}
|
||||
},
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
|
||||
it("fails if swap has no return request", async () => {
|
||||
const res = swapService.receiveReturn(IdMap.getId("empty"), [])
|
||||
await expect(res).rejects.toThrow("Swap has no return request")
|
||||
})
|
||||
|
||||
it("sets requires action if return fails", async () => {
|
||||
await swapService.receiveReturn(IdMap.getId("swap"), [
|
||||
{ variant_id: IdMap.getId("1234"), quantity: 1 },
|
||||
])
|
||||
|
||||
expect(swapRepo.save).toHaveBeenCalledWith({
|
||||
...existing,
|
||||
fulfillment_status: "requires_action",
|
||||
})
|
||||
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.receiveReturn).toHaveBeenCalledWith(
|
||||
IdMap.getId("return-swap"),
|
||||
[{ variant_id: IdMap.getId("1234"), quantity: 1 }],
|
||||
undefined,
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it("fails to receive return when swap is canceled", async () => {
|
||||
await expect(
|
||||
swapService.receiveReturn(IdMap.getId("canceled"))
|
||||
).rejects.toThrow("Canceled swap cannot be registered as received")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("createFulfillment", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -757,6 +650,11 @@ describe("SwapService", () => {
|
||||
|
||||
const cartService = {
|
||||
update: jest.fn(),
|
||||
retrieve: jest
|
||||
.fn()
|
||||
.mockReturnValue(
|
||||
Promise.resolve({ id: "cart", items: [{ id: "test-item" }] })
|
||||
),
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
@@ -771,7 +669,6 @@ describe("SwapService", () => {
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
fulfillmentService,
|
||||
cartService,
|
||||
})
|
||||
@@ -854,6 +751,12 @@ describe("SwapService", () => {
|
||||
}
|
||||
|
||||
const cartService = {
|
||||
retrieve: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
id: "cart",
|
||||
items: [{ id: "test-item", variant_id: "variant" }],
|
||||
})
|
||||
),
|
||||
update: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
@@ -885,18 +788,21 @@ describe("SwapService", () => {
|
||||
}
|
||||
|
||||
describe("success", () => {
|
||||
const existing = {
|
||||
cart: {
|
||||
items: [{ id: "1", variant_id: "variant", quantity: 2 }],
|
||||
shipping_methods: [{ id: "method_1" }],
|
||||
payment: {
|
||||
good: "yes",
|
||||
},
|
||||
shipping_address_id: 1234,
|
||||
const cart = {
|
||||
items: [{ id: "1", variant_id: "variant", quantity: 2 }],
|
||||
shipping_methods: [{ id: "method_1" }],
|
||||
payment: {
|
||||
good: "yes",
|
||||
},
|
||||
shipping_address_id: 1234,
|
||||
total: 1,
|
||||
}
|
||||
const existing = {
|
||||
other: "data",
|
||||
}
|
||||
|
||||
cartService.retrieve = () => cart
|
||||
|
||||
const swapRepo = MockRepository({
|
||||
findOneWithRelations: () => Promise.resolve(existing),
|
||||
})
|
||||
@@ -908,7 +814,6 @@ describe("SwapService", () => {
|
||||
totalsService,
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
})
|
||||
@@ -931,7 +836,7 @@ describe("SwapService", () => {
|
||||
|
||||
expect(swapRepo.save).toHaveBeenCalledWith({
|
||||
...existing,
|
||||
difference_due: 100,
|
||||
difference_due: 1,
|
||||
shipping_address_id: 1234,
|
||||
confirmed_at: expect.anything(),
|
||||
})
|
||||
@@ -970,7 +875,6 @@ describe("SwapService", () => {
|
||||
totalsService,
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
inventoryService,
|
||||
})
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import SystemTaxService from "../system-tax"
|
||||
|
||||
describe("SystemTaxService", () => {
|
||||
describe("getTaxLines", () => {
|
||||
const taxService = new SystemTaxService()
|
||||
|
||||
const calculationContext = {
|
||||
region: {},
|
||||
customer: {},
|
||||
allocation_map: {},
|
||||
shipping_address: {},
|
||||
}
|
||||
|
||||
test("success", async () => {
|
||||
const lines = await taxService.getTaxLines(
|
||||
[
|
||||
{
|
||||
item: { id: "line_id" },
|
||||
rates: [
|
||||
{ rate: 20, name: "test", code: "test" },
|
||||
{ rate: 5.5, name: "test_2", code: "test_2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
[],
|
||||
calculationContext
|
||||
)
|
||||
expect(lines).toEqual([
|
||||
{
|
||||
item_id: "line_id",
|
||||
rate: 20,
|
||||
name: "test",
|
||||
code: "test",
|
||||
},
|
||||
{
|
||||
item_id: "line_id",
|
||||
rate: 5.5,
|
||||
name: "test_2",
|
||||
code: "test_2",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("success w. shipping", async () => {
|
||||
const lines = await taxService.getTaxLines(
|
||||
[
|
||||
{
|
||||
item: { id: "line_id" },
|
||||
rates: [
|
||||
{ rate: 20, name: "test", code: "test" },
|
||||
{ rate: 5.5, name: "test_2", code: "test_2" },
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
shipping_method: { id: "sm_id" },
|
||||
rates: [{ rate: 20, name: "test", code: "test" }],
|
||||
},
|
||||
],
|
||||
calculationContext
|
||||
)
|
||||
expect(lines).toEqual([
|
||||
{
|
||||
item_id: "line_id",
|
||||
rate: 20,
|
||||
name: "test",
|
||||
code: "test",
|
||||
},
|
||||
{
|
||||
item_id: "line_id",
|
||||
rate: 5.5,
|
||||
name: "test_2",
|
||||
code: "test_2",
|
||||
},
|
||||
{
|
||||
shipping_method_id: "sm_id",
|
||||
rate: 20,
|
||||
name: "test",
|
||||
code: "test",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,281 @@
|
||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||
import TaxProviderService from "../tax-provider"
|
||||
|
||||
describe("TaxProviderService", () => {
|
||||
describe("retrieveProvider", () => {
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
taxRateService: {},
|
||||
systemTaxService: "system",
|
||||
tp_test: "good",
|
||||
}
|
||||
|
||||
const providerService = new TaxProviderService(container)
|
||||
|
||||
it("successfully retrieves system provider", () => {
|
||||
const provider = providerService.retrieveProvider({
|
||||
tax_provider_id: null,
|
||||
})
|
||||
expect(provider).toEqual("system")
|
||||
})
|
||||
|
||||
it("successfully retrieves external provider", () => {
|
||||
const provider = providerService.retrieveProvider({
|
||||
tax_provider_id: "test",
|
||||
})
|
||||
expect(provider).toEqual("good")
|
||||
})
|
||||
|
||||
it("fails when payment provider not found", () => {
|
||||
expect.assertions(1)
|
||||
|
||||
try {
|
||||
providerService.retrieveProvider({ tax_provider_id: "unregistered" })
|
||||
} catch (err) {
|
||||
expect(err.message).toEqual(
|
||||
"Could not find a tax provider with id: unregistered"
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTaxLines", () => {
|
||||
const mockCalculateLineItemTaxes = jest.fn(() => [
|
||||
{
|
||||
rate: 5,
|
||||
name: "test vat",
|
||||
code: "test_vat",
|
||||
item_id: "item_1",
|
||||
},
|
||||
])
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
lineItemTaxLineRepository: MockRepository({ create: (d) => d }),
|
||||
systemTaxService: {
|
||||
getTaxLines: mockCalculateLineItemTaxes,
|
||||
},
|
||||
}
|
||||
|
||||
test("success", async () => {
|
||||
const providerService = new TaxProviderService(container)
|
||||
providerService.getRegionRatesForProduct = jest.fn(() => [])
|
||||
|
||||
const region = { id: "test", tax_provider_id: null }
|
||||
const { cart, calculationContext: calcContext } = getTaxLineFactory({
|
||||
items: [{ id: "item_1", product_id: "prod_1" }],
|
||||
region,
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
item_id: "item_1",
|
||||
rate: 5,
|
||||
name: "test vat",
|
||||
code: "test_vat",
|
||||
metadata: undefined,
|
||||
},
|
||||
]
|
||||
|
||||
const rates = await providerService.getTaxLines(cart.items, calcContext)
|
||||
|
||||
expect(rates).toEqual(expected)
|
||||
|
||||
expect(container.lineItemTaxLineRepository.create).toHaveBeenCalledTimes(
|
||||
1
|
||||
)
|
||||
expect(container.lineItemTaxLineRepository.create).toHaveBeenCalledWith(
|
||||
expected[0]
|
||||
)
|
||||
|
||||
expect(providerService.getRegionRatesForProduct).toHaveBeenCalledTimes(1)
|
||||
expect(providerService.getRegionRatesForProduct).toHaveBeenCalledWith(
|
||||
"prod_1",
|
||||
region
|
||||
)
|
||||
|
||||
expect(mockCalculateLineItemTaxes).toHaveBeenCalledTimes(1)
|
||||
expect(mockCalculateLineItemTaxes).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
item: cart.items[0],
|
||||
rates: [],
|
||||
},
|
||||
],
|
||||
[],
|
||||
calcContext
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getRegionRatesForProduct", () => {
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
taxRateService: {
|
||||
listByProduct: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
test("success", async () => {
|
||||
container.taxRateService = {
|
||||
listByProduct: jest.fn(() => Promise.resolve([])),
|
||||
}
|
||||
|
||||
const providerService = new TaxProviderService(container)
|
||||
providerService.getCacheEntry = jest.fn(() => null)
|
||||
providerService.setCache = jest.fn()
|
||||
|
||||
const rates = await providerService.getRegionRatesForProduct("prod_id", {
|
||||
id: "reg_id",
|
||||
tax_rates: [],
|
||||
tax_rate: 12.5,
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
rate: 12.5,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
]
|
||||
expect(rates).toEqual(expected)
|
||||
|
||||
expect(providerService.getCacheEntry).toHaveBeenCalledTimes(1)
|
||||
expect(providerService.getCacheEntry).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
"reg_id"
|
||||
)
|
||||
|
||||
expect(providerService.setCache).toHaveBeenCalledTimes(1)
|
||||
expect(providerService.setCache).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
"reg_id",
|
||||
expected
|
||||
)
|
||||
})
|
||||
|
||||
test("success - without product rates", async () => {
|
||||
container.taxRateService = {
|
||||
listByProduct: jest.fn(() => Promise.resolve([])),
|
||||
}
|
||||
|
||||
const providerService = new TaxProviderService(container)
|
||||
providerService.getCacheEntry = jest.fn(() => null)
|
||||
providerService.setCache = jest.fn()
|
||||
|
||||
const rates = await providerService.getRegionRatesForProduct("prod_id", {
|
||||
id: "reg_id",
|
||||
tax_rates: [{ id: "reg_rate", rate: 20, name: "PTR", code: "ptr" }],
|
||||
tax_rate: 12.5,
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
rate: 12.5,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
]
|
||||
expect(container.taxRateService.listByProduct).toHaveBeenCalledTimes(1)
|
||||
expect(container.taxRateService.listByProduct).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
{ region_id: "reg_id" }
|
||||
)
|
||||
|
||||
expect(providerService.setCache).toHaveBeenCalledTimes(1)
|
||||
expect(providerService.setCache).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
"reg_id",
|
||||
expected
|
||||
)
|
||||
|
||||
expect(rates).toEqual(expected)
|
||||
})
|
||||
|
||||
test("success - with product rates", async () => {
|
||||
container.taxRateService = {
|
||||
listByProduct: jest.fn(() =>
|
||||
Promise.resolve([
|
||||
{
|
||||
rate: 20,
|
||||
name: "PTR",
|
||||
code: "ptr",
|
||||
},
|
||||
])
|
||||
),
|
||||
}
|
||||
|
||||
const providerService = new TaxProviderService(container)
|
||||
providerService.getCacheEntry = jest.fn(() => null)
|
||||
providerService.setCache = jest.fn()
|
||||
|
||||
const rates = await providerService.getRegionRatesForProduct("prod_id", {
|
||||
id: "reg_id",
|
||||
tax_rates: [{ id: "reg_rate", rate: 20, name: "PTR", code: "ptr" }],
|
||||
tax_rate: 12.5,
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
rate: 20,
|
||||
name: "PTR",
|
||||
code: "ptr",
|
||||
},
|
||||
]
|
||||
expect(container.taxRateService.listByProduct).toHaveBeenCalledTimes(1)
|
||||
expect(container.taxRateService.listByProduct).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
{ region_id: "reg_id" }
|
||||
)
|
||||
|
||||
expect(providerService.setCache).toHaveBeenCalledTimes(1)
|
||||
expect(providerService.setCache).toHaveBeenCalledWith(
|
||||
"prod_id",
|
||||
"reg_id",
|
||||
expected
|
||||
)
|
||||
|
||||
expect(rates).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getCacheKey", () => {
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
productTaxRateService: {},
|
||||
systemTaxService: "system",
|
||||
tp_test: "good",
|
||||
}
|
||||
|
||||
const providerService = new TaxProviderService(container)
|
||||
|
||||
test("formats correctly", () => {
|
||||
expect(providerService.getCacheKey("product", "region")).toEqual(
|
||||
"txrtcache:product:region"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const getTaxLineFactory = ({ items, region }) => {
|
||||
const cart = {
|
||||
shipping_methods: [],
|
||||
items: items.map((i) => {
|
||||
return {
|
||||
id: i.id,
|
||||
variant: {
|
||||
product_id: i.product_id,
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const calculationContext = {
|
||||
region,
|
||||
customer: {},
|
||||
allocation_map: {},
|
||||
shipping_address: {},
|
||||
shipping_methods: [],
|
||||
}
|
||||
|
||||
return { cart, calculationContext }
|
||||
}
|
||||
@@ -60,9 +60,15 @@ const discounts = {
|
||||
}
|
||||
|
||||
describe("TotalsService", () => {
|
||||
const container = {
|
||||
taxProviderService: {},
|
||||
taxCalculationStrategy: {},
|
||||
}
|
||||
|
||||
describe("getAllocationItemDiscounts", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
|
||||
const totalsService = new TotalsService(container)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -187,7 +193,7 @@ describe("TotalsService", () => {
|
||||
|
||||
describe("getDiscountTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
const totalsService = new TotalsService(container)
|
||||
|
||||
const discountCart = {
|
||||
id: "discount_cart",
|
||||
@@ -275,7 +281,7 @@ describe("TotalsService", () => {
|
||||
|
||||
describe("getRefundTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
const totalsService = new TotalsService(container)
|
||||
const orderToRefund = {
|
||||
id: "refund-order",
|
||||
tax_rate: 25,
|
||||
@@ -441,7 +447,7 @@ describe("TotalsService", () => {
|
||||
})
|
||||
describe("getShippingTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
const totalsService = new TotalsService(container)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -469,10 +475,85 @@ describe("TotalsService", () => {
|
||||
})
|
||||
describe("getTaxTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
let totalsService
|
||||
|
||||
const getTaxLinesMock = jest.fn(() => Promise.resolve([{ id: "line1" }]))
|
||||
const calculateMock = jest.fn(() => Promise.resolve(20.3))
|
||||
const getAllocationMapMock = jest.fn(() => ({}))
|
||||
|
||||
const cradle = {
|
||||
taxProviderService: {
|
||||
getTaxLines: getTaxLinesMock,
|
||||
},
|
||||
taxCalculationStrategy: {
|
||||
calculate: calculateMock,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
totalsService = new TotalsService(cradle)
|
||||
totalsService.getAllocationMap = getAllocationMapMock
|
||||
})
|
||||
|
||||
it("uses order tax lines", async () => {
|
||||
const order = {
|
||||
object: "order",
|
||||
tax_rate: null,
|
||||
region: {
|
||||
tax_rate: 25,
|
||||
},
|
||||
customer: {
|
||||
test: "test",
|
||||
},
|
||||
shipping_address: {
|
||||
test: "test",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
unit_price: 20,
|
||||
quantity: 2,
|
||||
tax_lines: [{ id: "orderline1" }],
|
||||
},
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
profile_id: IdMap.getId("default"),
|
||||
tax_lines: [],
|
||||
data: {
|
||||
extra: "hi",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
res = await totalsService.getTaxTotal(order)
|
||||
|
||||
expect(res).toEqual(20)
|
||||
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
||||
|
||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(0)
|
||||
|
||||
expect(calculateMock).toHaveBeenCalledTimes(1)
|
||||
expect(calculateMock).toHaveBeenCalledWith(
|
||||
order.items,
|
||||
[{ id: "orderline1" }],
|
||||
{
|
||||
shipping_address: order.shipping_address,
|
||||
shipping_methods: order.shipping_methods,
|
||||
is_return: false,
|
||||
customer: order.customer,
|
||||
region: order.region,
|
||||
allocation_map: {},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("calculates tax", async () => {
|
||||
@@ -480,6 +561,12 @@ describe("TotalsService", () => {
|
||||
region: {
|
||||
tax_rate: 25,
|
||||
},
|
||||
customer: {
|
||||
test: "test",
|
||||
},
|
||||
shipping_address: {
|
||||
test: "test",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
unit_price: 20,
|
||||
@@ -488,7 +575,7 @@ describe("TotalsService", () => {
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("expensiveShipping"),
|
||||
id: IdMap.getId("expensiveShipping"),
|
||||
name: "Expensive Shipping",
|
||||
price: 100,
|
||||
provider_id: "default_provider",
|
||||
@@ -500,15 +587,45 @@ describe("TotalsService", () => {
|
||||
],
|
||||
}
|
||||
|
||||
res = totalsService.getTaxTotal(order)
|
||||
res = await totalsService.getTaxTotal(order)
|
||||
|
||||
expect(res).toEqual(35)
|
||||
expect(res).toEqual(20)
|
||||
|
||||
expect(getAllocationMapMock).toHaveBeenCalledTimes(1)
|
||||
expect(getAllocationMapMock).toHaveBeenCalledWith(order, {})
|
||||
|
||||
expect(getTaxLinesMock).toHaveBeenCalledTimes(1)
|
||||
expect(getTaxLinesMock).toHaveBeenCalledWith(
|
||||
[{ quantity: 2, unit_price: 20 }],
|
||||
{
|
||||
shipping_address: order.shipping_address,
|
||||
shipping_methods: order.shipping_methods,
|
||||
is_return: false,
|
||||
customer: order.customer,
|
||||
region: order.region,
|
||||
allocation_map: {},
|
||||
}
|
||||
)
|
||||
|
||||
expect(calculateMock).toHaveBeenCalledTimes(1)
|
||||
expect(calculateMock).toHaveBeenCalledWith(
|
||||
order.items,
|
||||
[{ id: "line1" }],
|
||||
{
|
||||
shipping_address: order.shipping_address,
|
||||
shipping_methods: order.shipping_methods,
|
||||
is_return: false,
|
||||
customer: order.customer,
|
||||
region: order.region,
|
||||
allocation_map: {},
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getTotal", () => {
|
||||
let res
|
||||
const totalsService = new TotalsService()
|
||||
const totalsService = new TotalsService(container)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@@ -538,7 +655,13 @@ describe("TotalsService", () => {
|
||||
},
|
||||
],
|
||||
}
|
||||
res = totalsService.getTotal(order)
|
||||
const getTaxTotalMock = jest.fn(() => Promise.resolve(35))
|
||||
totalsService.getTaxTotal = getTaxTotalMock
|
||||
res = await totalsService.getTotal(order)
|
||||
|
||||
expect(getTaxTotalMock).toHaveBeenCalledTimes(1)
|
||||
expect(getTaxTotalMock).toHaveBeenCalledWith(order, undefined)
|
||||
|
||||
expect(res).toEqual(175)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,7 @@ import LineItemService from "./line-item"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
import CustomerService from "./customer"
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import DiscountService from "./discount"
|
||||
import GiftCardService from "./gift-card"
|
||||
import TotalsService from "./totals"
|
||||
@@ -45,6 +46,7 @@ type CartConstructorProps = {
|
||||
addressRepository: typeof AddressRepository
|
||||
paymentSessionRepository: typeof PaymentSessionRepository
|
||||
eventBusService: EventBusService
|
||||
taxProviderService: TaxProviderService
|
||||
paymentProviderService: PaymentProviderService
|
||||
productService: ProductService
|
||||
productVariantService: ProductVariantService
|
||||
@@ -59,6 +61,10 @@ type CartConstructorProps = {
|
||||
customShippingOptionService: CustomShippingOptionService
|
||||
}
|
||||
|
||||
type TotalsConfig = {
|
||||
force_taxes?: boolean
|
||||
}
|
||||
|
||||
/* Provides layer to manipulate carts.
|
||||
* @implements BaseService
|
||||
*/
|
||||
@@ -82,6 +88,7 @@ class CartService extends BaseService {
|
||||
private shippingOptionService_: ShippingOptionService
|
||||
private discountService_: DiscountService
|
||||
private giftCardService_: GiftCardService
|
||||
private taxProviderService_: TaxProviderService
|
||||
private totalsService_: TotalsService
|
||||
private addressRepository_: typeof AddressRepository
|
||||
private paymentSessionRepository_: typeof PaymentSessionRepository
|
||||
@@ -96,6 +103,7 @@ class CartService extends BaseService {
|
||||
paymentProviderService,
|
||||
productService,
|
||||
productVariantService,
|
||||
taxProviderService,
|
||||
regionService,
|
||||
lineItemService,
|
||||
shippingOptionService,
|
||||
@@ -110,59 +118,25 @@ class CartService extends BaseService {
|
||||
}: CartConstructorProps) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {ShippingMethodRepository} */
|
||||
this.shippingMethodRepository_ = shippingMethodRepository
|
||||
|
||||
/** @private @const {CartRepository} */
|
||||
this.cartRepository_ = cartRepository
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
/** @private @const {ProductService} */
|
||||
this.productService_ = productService
|
||||
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @const {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @const {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @const {CustomerService} */
|
||||
this.customerService_ = customerService
|
||||
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @const {DiscountService} */
|
||||
this.discountService_ = discountService
|
||||
|
||||
/** @private @const {GiftCardService} */
|
||||
this.giftCardService_ = giftCardService
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @const {AddressRepository} */
|
||||
this.addressRepository_ = addressRepository
|
||||
|
||||
/** @private @const {PaymentSessionRepository} */
|
||||
this.paymentSessionRepository_ = paymentSessionRepository
|
||||
|
||||
/** @private @const {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
|
||||
/** @private @const {CustomShippingOptionService} */
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): CartService {
|
||||
@@ -172,6 +146,7 @@ class CartService extends BaseService {
|
||||
|
||||
const cloned = new CartService({
|
||||
manager: transactionManager,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
cartRepository: this.cartRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
@@ -196,25 +171,6 @@ class CartService extends BaseService {
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate cart ids. Throws an error if the cast fails
|
||||
* @param {string} rawId - the raw cart id to validate.
|
||||
* @return {string} the validated id
|
||||
*/
|
||||
/**
|
||||
* Contents of a line item
|
||||
* @typedef {(object | array)} LineItemContent
|
||||
* @property {number} unit_price - the price of the content
|
||||
* @property {object} variant - the product variant of the content
|
||||
* @property {object} product - the product of the content
|
||||
* @property {number} quantity - the quantity of the content
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collection of contents grouped in the same line item
|
||||
* @typedef {LineItemContent[]} LineItemContentArray
|
||||
*/
|
||||
|
||||
transformQueryForTotals_(
|
||||
config: FindConfig<Cart>
|
||||
): FindConfig<Cart> & { totalsToSelect: TotalField[] } {
|
||||
@@ -243,6 +199,7 @@ class CartService extends BaseService {
|
||||
if (totalsToSelect.length > 0) {
|
||||
const relationSet = new Set(relations)
|
||||
relationSet.add("items")
|
||||
relationSet.add("items.tax_lines")
|
||||
relationSet.add("gift_cards")
|
||||
relationSet.add("discounts")
|
||||
relationSet.add("discounts.rule")
|
||||
@@ -252,6 +209,7 @@ class CartService extends BaseService {
|
||||
// relationSet.add("discounts.parent_discount.regions")
|
||||
relationSet.add("shipping_methods")
|
||||
relationSet.add("region")
|
||||
relationSet.add("region.tax_rates")
|
||||
relations = Array.from(relationSet.values())
|
||||
|
||||
select = select.filter((v) => !totalFields.includes(v))
|
||||
@@ -266,14 +224,17 @@ class CartService extends BaseService {
|
||||
|
||||
async decorateTotals_(
|
||||
cart: Cart,
|
||||
totalsToSelect: TotalField[]
|
||||
totalsToSelect: TotalField[],
|
||||
options: TotalsConfig = { force_taxes: false }
|
||||
): Promise<Cart> {
|
||||
const totals: { [K in TotalField]?: number } = {}
|
||||
const totals: { [K in TotalField]?: number | null } = {}
|
||||
|
||||
for (const key of totalsToSelect) {
|
||||
switch (key) {
|
||||
case "total": {
|
||||
totals.total = await this.totalsService_.getTotal(cart)
|
||||
totals.total = await this.totalsService_.getTotal(cart, {
|
||||
force_taxes: options.force_taxes,
|
||||
})
|
||||
break
|
||||
}
|
||||
case "shipping_total": {
|
||||
@@ -284,7 +245,10 @@ class CartService extends BaseService {
|
||||
totals.discount_total = this.totalsService_.getDiscountTotal(cart)
|
||||
break
|
||||
case "tax_total":
|
||||
totals.tax_total = await this.totalsService_.getTaxTotal(cart)
|
||||
totals.tax_total = await this.totalsService_.getTaxTotal(
|
||||
cart,
|
||||
options.force_taxes
|
||||
)
|
||||
break
|
||||
case "gift_card_total":
|
||||
totals.gift_card_total = this.totalsService_.getGiftCardTotal(cart)
|
||||
@@ -301,9 +265,9 @@ class CartService extends BaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - config object
|
||||
* @return {Promise} the result of the find operation
|
||||
* @param selector - the query object for find
|
||||
* @param config - config object
|
||||
* @return the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableCartProps,
|
||||
@@ -317,13 +281,15 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Gets a cart by id.
|
||||
* @param {string} cartId - the id of the cart to get.
|
||||
* @param {Object} options - the options to get a cart
|
||||
* @return {Promise<Cart>} the cart document.
|
||||
* @param cartId - the id of the cart to get.
|
||||
* @param options - the options to get a cart
|
||||
* @param totalsConfig - configuration for retrieval of totals
|
||||
* @return the cart document.
|
||||
*/
|
||||
async retrieve(
|
||||
cartId: string,
|
||||
options: FindConfig<Cart> = {}
|
||||
options: FindConfig<Cart> = {},
|
||||
totalsConfig: TotalsConfig = {}
|
||||
): Promise<Cart> {
|
||||
const cartRepo = this.manager_.getCustomRepository(this.cartRepository_)
|
||||
const validatedId = this.validateId_(cartId)
|
||||
@@ -358,13 +324,13 @@ class CartService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
return await this.decorateTotals_(raw, totalsToSelect)
|
||||
return await this.decorateTotals_(raw, totalsToSelect, totalsConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a cart.
|
||||
* @param {Object} data - the data to create the cart with
|
||||
* @return {Promise} the result of the create operation
|
||||
* @param data - the data to create the cart with
|
||||
* @return the result of the create operation
|
||||
*/
|
||||
async create(data: CartCreateProps): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -435,7 +401,7 @@ class CartService extends BaseService {
|
||||
]
|
||||
|
||||
for (const k of remainingFields) {
|
||||
if (typeof data[k] !== "undefined") {
|
||||
if (typeof data[k] !== "undefined" && k !== "object") {
|
||||
toCreate[k] = data[k]
|
||||
}
|
||||
}
|
||||
@@ -453,9 +419,9 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Removes a line item from the cart.
|
||||
* @param {string} cartId - the id of the cart that we will remove from
|
||||
* @param {LineItem} lineItemId - the line item to remove.
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cartId - the id of the cart that we will remove from
|
||||
* @param lineItemId - the line item to remove.
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async removeLineItem(cartId: string, lineItemId: string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -505,9 +471,9 @@ class CartService extends BaseService {
|
||||
* Checks if a given line item has a shipping method that can fulfill it.
|
||||
* Returns true if all products in the cart can be fulfilled with the current
|
||||
* shipping methods.
|
||||
* @param {ShippingMethod[]} shippingMethods - the set of shipping methods to check from
|
||||
* @param {LineItem} lineItem - the line item
|
||||
* @return {boolean}
|
||||
* @param shippingMethods - the set of shipping methods to check from
|
||||
* @param lineItem - the line item
|
||||
* @return boolean representing wheter shipping method is validated
|
||||
*/
|
||||
validateLineItemShipping_(
|
||||
shippingMethods: ShippingMethod[],
|
||||
@@ -535,9 +501,9 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Adds a line item to the cart.
|
||||
* @param {string} cartId - the id of the cart that we will add to
|
||||
* @param {LineItem} lineItem - the line item to add.
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cartId - the id of the cart that we will add to
|
||||
* @param lineItem - the line item to add.
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async addLineItem(cartId: string, lineItem: LineItem): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -613,11 +579,10 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Updates a cart's existing line item.
|
||||
* @param {string} cartId - the id of the cart to update
|
||||
* @param {string} lineItemId - the id of the line item to update.
|
||||
* @param {LineItemUpdate} lineItemUpdate - the line item to update. Must
|
||||
* include an id field.
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cartId - the id of the cart to update
|
||||
* @param lineItemId - the id of the line item to update.
|
||||
* @param lineItemUpdate - the line item to update. Must include an id field.
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async updateLineItem(
|
||||
cartId: string,
|
||||
@@ -669,8 +634,9 @@ class CartService extends BaseService {
|
||||
* shipping discount
|
||||
* If a free shipping is present, we set shipping methods price to 0
|
||||
* if a free shipping was present, we set shipping methods to original amount
|
||||
* @param {Cart} cart - the the cart to adjust free shipping for
|
||||
* @param {boolean} shouldAdd - flag to indicate, if we should add or remove
|
||||
* @param cart - the the cart to adjust free shipping for
|
||||
* @param shouldAdd - flag to indicate, if we should add or remove
|
||||
* @return void
|
||||
*/
|
||||
async adjustFreeShipping_(cart: Cart, shouldAdd: boolean): Promise<void> {
|
||||
if (cart.shipping_methods?.length) {
|
||||
@@ -851,9 +817,9 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Sets the customer id of a cart
|
||||
* @param {Cart} cart - the cart to add email to
|
||||
* @param {string} customerId - the customer to add to cart
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cart - the cart to add email to
|
||||
* @param customerId - the customer to add to cart
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async updateCustomerId_(cart: Cart, customerId: string): Promise<void> {
|
||||
const customer = await this.customerService_
|
||||
@@ -867,8 +833,8 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Creates or fetches a user based on an email.
|
||||
* @param {string} email - the email to use
|
||||
* @return {Promise} the resultign customer object
|
||||
* @param email - the email to use
|
||||
* @return the resultign customer object
|
||||
*/
|
||||
async createOrFetchUserFromEmail_(email: string): Promise<Customer> {
|
||||
const schema = Validator.string().email().required()
|
||||
@@ -896,12 +862,10 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Updates the cart's billing address.
|
||||
* @param {Cart} cart - the cart to update
|
||||
* @param {Address | string} addressOrId - the value to set the billing
|
||||
* address to
|
||||
* @param {AddressRepository} addrRepo - the repository to use for address
|
||||
* updates
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cart - the cart to update
|
||||
* @param addressOrId - the value to set the billing address to
|
||||
* @param addrRepo - the repository to use for address updates
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async updateBillingAddress_(
|
||||
cart: Cart,
|
||||
@@ -941,12 +905,10 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Updates the cart's shipping address.
|
||||
* @param {Cart} cart - the cart to update
|
||||
* @param {Address | string} addressOrId - the value to set the shipping
|
||||
* address to
|
||||
* @param {AddressRepository} addrRepo - the repository to use for address
|
||||
* updates
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cart - the cart to update
|
||||
* @param addressOrId - the value to set the shipping address to
|
||||
* @param addrRepo - the repository to use for address updates
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async updateShippingAddress_(
|
||||
cart: Cart,
|
||||
@@ -1030,9 +992,9 @@ class CartService extends BaseService {
|
||||
* If discount besides free shipping is already applied, this
|
||||
* will be overwritten
|
||||
* Throws if discount regions does not include the cart region
|
||||
* @param {Cart} cart - the cart to update
|
||||
* @param {string} discountCode - the discount code
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cart - the cart to update
|
||||
* @param discountCode - the discount code
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async applyDiscount(cart: Cart, discountCode: string): Promise<void> {
|
||||
const discount = await this.discountService_.retrieveByCode(discountCode, [
|
||||
@@ -1129,9 +1091,9 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Removes a discount based on a discount code.
|
||||
* @param {string} cartId - the id of the cart to remove from
|
||||
* @param {string} discountCode - the discount code to remove
|
||||
* @return {Promise<Cart>} the resulting cart
|
||||
* @param cartId - the id of the cart to remove from
|
||||
* @param discountCode - the discount code to remove
|
||||
* @return the resulting cart
|
||||
*/
|
||||
async removeDiscount(cartId: string, discountCode: string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1167,19 +1129,11 @@ class CartService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* A payment method represents a way for the customer to pay. The payment
|
||||
* method will typically come from one of the payment sessions.
|
||||
* @typedef {object} PaymentMethod
|
||||
* @property {string} provider_id - the identifier of the payment method's
|
||||
* provider
|
||||
* @property {object} data - the data associated with the payment method
|
||||
*/
|
||||
|
||||
/**
|
||||
* Updates the currently selected payment session.
|
||||
* @param {string} cartId - the id of the cart to update the payment session for
|
||||
* @param {object} update - the data to update the payment session with
|
||||
* @param cartId - the id of the cart to update the payment session for
|
||||
* @param update - the data to update the payment session with
|
||||
* @return the resulting cart
|
||||
*/
|
||||
async updatePaymentSession(cartId: string, update: object): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1209,11 +1163,11 @@ class CartService extends BaseService {
|
||||
* a payment object, that we will use to update our cart payment with.
|
||||
* Additionally, if the payment does not require more or fails, we will
|
||||
* set the payment on the cart.
|
||||
* @param {string} cartId - the id of the cart to authorize payment for
|
||||
* @param {object} context - object containing whatever is relevant for
|
||||
* @param cartId - the id of the cart to authorize payment for
|
||||
* @param context - object containing whatever is relevant for
|
||||
* authorizing the payment with the payment provider. As an example,
|
||||
* this could be IP address or similar for fraud handling.
|
||||
* @return {Promise<Cart>} the resulting cart
|
||||
* @return the resulting cart
|
||||
*/
|
||||
async authorizePayment(cartId: string, context: Record<string, any> = {}): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1265,9 +1219,9 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Sets a payment method for a cart.
|
||||
* @param {string} cartId - the id of the cart to add payment method to
|
||||
* @param {string} providerId - the id of the provider to be set to the cart
|
||||
* @return {Promise} result of update operation
|
||||
* @param cartId - the id of the cart to add payment method to
|
||||
* @param providerId - the id of the provider to be set to the cart
|
||||
* @return result of update operation
|
||||
*/
|
||||
async setPaymentSession(cartId: string, providerId: string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1334,9 +1288,8 @@ class CartService extends BaseService {
|
||||
* provider. Additional calls will ensure that payment sessions have correct
|
||||
* amounts, currencies, etc. as well as make sure to filter payment sessions
|
||||
* that are not available for the cart's region.
|
||||
* @param {Cart | string} cartOrCartId - the id of the cart to set payment
|
||||
* session for
|
||||
* @return {Promise} the result of the update operation.
|
||||
* @param cartOrCartId - the id of the cart to set payment session for
|
||||
* @return the result of the update operation.
|
||||
*/
|
||||
async setPaymentSessions(cartOrCartId: Cart | string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1345,36 +1298,42 @@ class CartService extends BaseService {
|
||||
const cartId =
|
||||
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
|
||||
|
||||
const cart = await this.retrieve(cartId, {
|
||||
select: [
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"tax_total",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"total",
|
||||
],
|
||||
relations: [
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"gift_cards",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"region",
|
||||
"region.payment_providers",
|
||||
"payment_sessions",
|
||||
"customer",
|
||||
],
|
||||
})
|
||||
const cart = await this.retrieve(
|
||||
cartId,
|
||||
{
|
||||
select: [
|
||||
"total",
|
||||
"subtotal",
|
||||
"tax_total",
|
||||
"discount_total",
|
||||
"shipping_total",
|
||||
"gift_card_total",
|
||||
],
|
||||
relations: [
|
||||
"items",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"gift_cards",
|
||||
"shipping_methods",
|
||||
"billing_address",
|
||||
"shipping_address",
|
||||
"region",
|
||||
"region.tax_rates",
|
||||
"region.payment_providers",
|
||||
"payment_sessions",
|
||||
"customer",
|
||||
],
|
||||
},
|
||||
{ force_taxes: true }
|
||||
)
|
||||
|
||||
const region = cart.region
|
||||
const { total, region } = cart
|
||||
|
||||
if (typeof cart.total === "undefined") {
|
||||
if (typeof total === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"cart.total should be defined"
|
||||
"cart.total must be defined"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1383,7 +1342,7 @@ class CartService extends BaseService {
|
||||
if (cart.payment_sessions && cart.payment_sessions.length) {
|
||||
for (const session of cart.payment_sessions) {
|
||||
if (
|
||||
cart.total <= 0 ||
|
||||
total <= 0 ||
|
||||
!region.payment_providers.find(
|
||||
({ id }) => id === session.provider_id
|
||||
)
|
||||
@@ -1400,7 +1359,7 @@ class CartService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cart.total > 0) {
|
||||
if (total > 0) {
|
||||
// If only one payment session exists, we preselect it
|
||||
if (region.payment_providers.length === 1 && !cart.payment_session) {
|
||||
const p = region.payment_providers[0]
|
||||
@@ -1426,10 +1385,10 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Removes a payment session from the cart.
|
||||
* @param {string} cartId - the id of the cart to remove from
|
||||
* @param {string} providerId - the id of the provider whoose payment session
|
||||
* @param cartId - the id of the cart to remove from
|
||||
* @param providerId - the id of the provider whoose payment session
|
||||
* should be removed.
|
||||
* @return {Promise<Cart>} the resulting cart.
|
||||
* @return the resulting cart.
|
||||
*/
|
||||
async deletePaymentSession(
|
||||
cartId: string,
|
||||
@@ -1470,8 +1429,8 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Refreshes a payment session on a cart
|
||||
* @param {string} cartId - the id of the cart to remove from
|
||||
* @param {string} providerId - the id of the provider whoose payment session
|
||||
* @param cartId - the id of the cart to remove from
|
||||
* @param providerId - the id of the provider whoose payment session
|
||||
* should be removed.
|
||||
* @return {Promise<Cart>} the resulting cart.
|
||||
*/
|
||||
@@ -1512,10 +1471,10 @@ class CartService extends BaseService {
|
||||
* Shipping Option is a possible way to ship an order. Shipping Methods may
|
||||
* also have additional details in the data field such as an id for a package
|
||||
* shop.
|
||||
* @param {string} cartId - the id of the cart to add shipping method to
|
||||
* @param {string} optionId - id of shipping option to add as valid method
|
||||
* @param {Object} data - the fulmillment data for the method
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cartId - the id of the cart to add shipping method to
|
||||
* @param optionId - id of shipping option to add as valid method
|
||||
* @param data - the fulmillment data for the method
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async addShippingMethod(
|
||||
cartId: string,
|
||||
@@ -1610,9 +1569,9 @@ class CartService extends BaseService {
|
||||
/**
|
||||
* Finds the cart's custom shipping options based on the passed option id.
|
||||
* throws if custom options is not empty and no shipping option corresponds to optionId
|
||||
* @param {Object} cartCustomShippingOptions - the cart's custom shipping options
|
||||
* @param {string} optionId - id of the normal or custom shipping option to find in the cartCustomShippingOptions
|
||||
* @return {CustomShippingOption | undefined}
|
||||
* @param cartCustomShippingOptions - the cart's custom shipping options
|
||||
* @param optionId - id of the normal or custom shipping option to find in the cartCustomShippingOptions
|
||||
* @return custom shipping option
|
||||
*/
|
||||
findCustomShippingOption(
|
||||
cartCustomShippingOptions: CustomShippingOption[],
|
||||
@@ -1635,10 +1594,10 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Set's the region of a cart.
|
||||
* @param {Cart} cart - the cart to set region on
|
||||
* @param {string} regionId - the id of the region to set the region to
|
||||
* @param {string} countryCode - the country code to set the country to
|
||||
* @return {Promise} the result of the update operation
|
||||
* @param cart - the cart to set region on
|
||||
* @param regionId - the id of the region to set the region to
|
||||
* @param countryCode - the country code to set the country to
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async setRegion_(
|
||||
cart: Cart,
|
||||
@@ -1793,9 +1752,8 @@ class CartService extends BaseService {
|
||||
|
||||
/**
|
||||
* Deletes a cart from the database. Completed carts cannot be deleted.
|
||||
* @param {string} cartId - the id of the cart to delete
|
||||
* @return {Promise<string>} the deleted cart or undefined if the cart was
|
||||
* not found.
|
||||
* @param cartId - the id of the cart to delete
|
||||
* @return the deleted cart or undefined if the cart was not found.
|
||||
*/
|
||||
async delete(cartId: string): Promise<string> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
@@ -1832,10 +1790,10 @@ class CartService extends BaseService {
|
||||
* Dedicated method to set metadata for a cart.
|
||||
* To ensure that plugins does not overwrite each
|
||||
* others metadata fields, setMetadata is provided.
|
||||
* @param {string} cartId - the cart to apply metadata to.
|
||||
* @param {string} key - key for metadata field
|
||||
* @param {string} value - value for metadata field.
|
||||
* @return {Promise} resolves to the updated result.
|
||||
* @param cartId - the cart to apply metadata to.
|
||||
* @param key - key for metadata field
|
||||
* @param value - value for metadata field.
|
||||
* @return resolves to the updated result.
|
||||
*/
|
||||
async setMetadata(
|
||||
cartId: string,
|
||||
@@ -1869,11 +1827,35 @@ class CartService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
async createTaxLines(id: string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const cart = await this.retrieve(id, {
|
||||
relations: [
|
||||
"items",
|
||||
"gift_cards",
|
||||
"discounts",
|
||||
"discounts.rule",
|
||||
"discounts.rule.valid_for",
|
||||
"shipping_methods",
|
||||
"region",
|
||||
"region.tax_rates",
|
||||
],
|
||||
})
|
||||
const calculationContext = this.totalsService_.getCalculationContext(cart)
|
||||
|
||||
await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
.createTaxLines(cart, calculationContext)
|
||||
|
||||
return cart
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to delete metadata for a cart.
|
||||
* @param {string} cartId - the cart to delete metadata from.
|
||||
* @param {string} key - key for metadata field
|
||||
* @return {Promise} resolves to the updated result.
|
||||
* @param cartId - the cart to delete metadata from.
|
||||
* @param key - key for metadata field
|
||||
* @return resolves to the updated result.
|
||||
*/
|
||||
async deleteMetadata(cartId: string, key: string): Promise<Cart> {
|
||||
return this.atomicPhase_(async (manager: EntityManager) => {
|
||||
|
||||
@@ -13,63 +13,40 @@ class ClaimService extends BaseService {
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
claimRepository,
|
||||
addressRepository,
|
||||
claimItemService,
|
||||
claimRepository,
|
||||
eventBusService,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
inventoryService,
|
||||
lineItemService,
|
||||
totalsService,
|
||||
paymentProviderService,
|
||||
regionService,
|
||||
returnService,
|
||||
shippingOptionService,
|
||||
claimItemService,
|
||||
regionService,
|
||||
inventoryService,
|
||||
eventBusService,
|
||||
taxProviderService,
|
||||
totalsService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @constant {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @constant {ClaimRepository} */
|
||||
this.claimRepository_ = claimRepository
|
||||
|
||||
/** @private @constant {AddressRepository} */
|
||||
this.addressRepo_ = addressRepository
|
||||
|
||||
/** @private @constant {FulfillmentProviderService} */
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
|
||||
/** @private @constant {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @constant {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @constant {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @constant {ReturnService} */
|
||||
this.returnService_ = returnService
|
||||
|
||||
/** @private @constant {FulfillmentService} */
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
/** @private @constant {ClaimItemService} */
|
||||
this.claimItemService_ = claimItemService
|
||||
|
||||
/** @private @constant {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @constant {InventoryService} */
|
||||
this.inventoryService_ = inventoryService
|
||||
|
||||
/** @private @constant {EventBus} */
|
||||
this.claimRepository_ = claimRepository
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @constant {ShippingOptionService} */
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.lineItemService_ = lineItemService
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.regionService_ = regionService
|
||||
this.returnService_ = returnService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
@@ -79,19 +56,20 @@ class ClaimService extends BaseService {
|
||||
|
||||
const cloned = new ClaimService({
|
||||
manager,
|
||||
claimRepository: this.claimRepository_,
|
||||
addressRepository: this.addressRepo_,
|
||||
claimItemService: this.claimItemService_,
|
||||
claimRepository: this.claimRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
fulfillmentService: this.fulfillmentService_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
regionService: this.regionService_,
|
||||
returnService: this.returnService_,
|
||||
claimItemService: this.claimItemService_,
|
||||
eventBusService: this.eventBus_,
|
||||
totalsService: this.totalsService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
totalsService: this.totalsService_,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = manager
|
||||
@@ -198,7 +176,7 @@ class ClaimService extends BaseService {
|
||||
|
||||
for (const item of claim_items) {
|
||||
const line = await this.lineItemService_.retrieve(item.item_id, {
|
||||
relations: ["order", "swap", "claim_order"],
|
||||
relations: ["order", "swap", "claim_order", "tax_lines"],
|
||||
})
|
||||
|
||||
if (
|
||||
@@ -261,25 +239,29 @@ class ClaimService extends BaseService {
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, lines)
|
||||
}
|
||||
|
||||
for (const item of additional_items) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
|
||||
const newItems = await Promise.all(
|
||||
additional_items.map((i) =>
|
||||
this.lineItemService_
|
||||
let newItems = []
|
||||
if (typeof additional_items !== "undefined") {
|
||||
for (const item of additional_items) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.generate(i.variant_id, order.region_id, i.quantity)
|
||||
)
|
||||
)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
|
||||
for (const newItem of newItems) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.adjustInventory(newItem.variant_id, -newItem.quantity)
|
||||
newItems = await Promise.all(
|
||||
additional_items.map((i) =>
|
||||
this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.generate(i.variant_id, order.region_id, i.quantity)
|
||||
)
|
||||
)
|
||||
|
||||
for (const newItem of newItems) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.adjustInventory(newItem.variant_id, -newItem.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
const evaluatedNoNotification =
|
||||
no_notification !== undefined ? no_notification : order.no_notification
|
||||
|
||||
@@ -296,6 +278,18 @@ class ClaimService extends BaseService {
|
||||
|
||||
const result = await claimRepo.save(created)
|
||||
|
||||
if (result.additional_items && result.additional_items.length) {
|
||||
const calcContext = this.totalsService_.getCalculationContext(order)
|
||||
const lineItems = await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.list({
|
||||
id: result.additional_items.map((i) => i.id),
|
||||
})
|
||||
await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
.createTaxLines(lineItems, calcContext)
|
||||
}
|
||||
|
||||
if (shipping_methods) {
|
||||
for (const method of shipping_methods) {
|
||||
if (method.id) {
|
||||
@@ -366,7 +360,9 @@ class ClaimService extends BaseService {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: [
|
||||
"additional_items",
|
||||
"additional_items.tax_lines",
|
||||
"shipping_methods",
|
||||
"shipping_methods.tax_lines",
|
||||
"shipping_address",
|
||||
"order",
|
||||
"order.billing_address",
|
||||
|
||||
@@ -299,7 +299,7 @@ class FulfillmentService extends BaseService {
|
||||
const now = new Date()
|
||||
fulfillment.shipped_at = now
|
||||
|
||||
fulfillment.tracking_links = trackingLinks.map((tl) =>
|
||||
fulfillment.tracking_links = (trackingLinks || []).map((tl) =>
|
||||
trackingLinkRepo.create(tl)
|
||||
)
|
||||
|
||||
|
||||
@@ -36,3 +36,6 @@ export { default as SystemPaymentProviderService } from "./system-payment-provid
|
||||
export { default as TotalsService } from "./totals"
|
||||
export { default as TransactionService } from "./transaction"
|
||||
export { default as UserService } from "./user"
|
||||
export { default as TaxRateService } from "./tax-rate"
|
||||
export { default as TaxProviderService } from "./tax-provider"
|
||||
export { default as ProductTypeService } from "./product-type"
|
||||
@@ -9,6 +9,7 @@ class LineItemService extends BaseService {
|
||||
constructor({
|
||||
manager,
|
||||
lineItemRepository,
|
||||
lineItemTaxLineRepository,
|
||||
productVariantService,
|
||||
productService,
|
||||
regionService,
|
||||
@@ -22,6 +23,9 @@ class LineItemService extends BaseService {
|
||||
/** @private @const {LineItemRepository} */
|
||||
this.lineItemRepository_ = lineItemRepository
|
||||
|
||||
/** @private @const {typeof LineItemTaxLineRepository} */
|
||||
this.itemTaxLineRepo_ = lineItemTaxLineRepository
|
||||
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
@@ -43,6 +47,7 @@ class LineItemService extends BaseService {
|
||||
const cloned = new LineItemService({
|
||||
manager: transactionManager,
|
||||
lineItemRepository: this.lineItemRepository_,
|
||||
lineItemTaxLineRepository: this.itemTaxLineRepo_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productService: this.productService_,
|
||||
regionService: this.regionService_,
|
||||
@@ -89,6 +94,49 @@ class LineItemService extends BaseService {
|
||||
return lineItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates return line items for a given cart based on the return items in a
|
||||
* return.
|
||||
* @param {string} returnId - the id to generate return items from.
|
||||
* @param {string} cartId - the cart to assign the return line items to.
|
||||
* @return {Promise<LineItem[]>} the created line items
|
||||
*/
|
||||
async createReturnLines(returnId, cartId) {
|
||||
const lineItemRepo = this.manager_.getCustomRepository(
|
||||
this.lineItemRepository_
|
||||
)
|
||||
|
||||
const itemTaxLineRepo = this.manager_.getCustomRepository(
|
||||
this.itemTaxLineRepo_
|
||||
)
|
||||
|
||||
const items = await lineItemRepo.findByReturn(returnId)
|
||||
|
||||
const toCreate = items.map((i) =>
|
||||
lineItemRepo.create({
|
||||
cart_id: cartId,
|
||||
thumbnail: i.thumbnail,
|
||||
is_return: true,
|
||||
title: i.title,
|
||||
variant_id: i.variant_id,
|
||||
unit_price: -1 * i.unit_price,
|
||||
quantity: i.return_item.quantity,
|
||||
allow_discounts: i.allow_discounts,
|
||||
tax_lines: i.tax_lines.map((tl) => {
|
||||
return itemTaxLineRepo.create({
|
||||
name: tl.name,
|
||||
code: tl.code,
|
||||
rate: tl.rate,
|
||||
metadata: tl.metadata,
|
||||
})
|
||||
}),
|
||||
metadata: i.metadata,
|
||||
})
|
||||
)
|
||||
|
||||
return await lineItemRepo.save(toCreate)
|
||||
}
|
||||
|
||||
async generate(variantId, regionId, quantity, config = {}) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const variant = await this.productVariantService_
|
||||
|
||||
@@ -201,7 +201,9 @@ class OrderService extends BaseService {
|
||||
|
||||
const raw = await orderRepo.find(query)
|
||||
|
||||
return raw.map((r) => this.decorateTotals_(r, totalsToSelect))
|
||||
return await Promise.all(
|
||||
raw.map(async (r) => await this.decorateTotals_(r, totalsToSelect))
|
||||
)
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
@@ -258,7 +260,9 @@ class OrderService extends BaseService {
|
||||
|
||||
const raw = await orderRepo.findWithRelations(rels, query)
|
||||
const count = await orderRepo.count(query)
|
||||
const orders = raw.map((r) => this.decorateTotals_(r, totalsToSelect))
|
||||
const orders = await Promise.all(
|
||||
raw.map(async (r) => await this.decorateTotals_(r, totalsToSelect))
|
||||
)
|
||||
|
||||
return [orders, count]
|
||||
}
|
||||
@@ -293,10 +297,13 @@ class OrderService extends BaseService {
|
||||
if (totalsToSelect.length > 0) {
|
||||
const relationSet = new Set(relations)
|
||||
relationSet.add("items")
|
||||
relationSet.add("items.tax_lines")
|
||||
relationSet.add("swaps")
|
||||
relationSet.add("swaps.additional_items")
|
||||
relationSet.add("swaps.additional_items.tax_lines")
|
||||
relationSet.add("claims")
|
||||
relationSet.add("claims.additional_items")
|
||||
relationSet.add("claims.additional_items.tax_lines")
|
||||
relationSet.add("discounts")
|
||||
relationSet.add("discounts.rule")
|
||||
relationSet.add("discounts.rule.valid_for")
|
||||
@@ -304,15 +311,21 @@ class OrderService extends BaseService {
|
||||
relationSet.add("gift_card_transactions")
|
||||
relationSet.add("refunds")
|
||||
relationSet.add("shipping_methods")
|
||||
relationSet.add("shipping_methods.tax_lines")
|
||||
relationSet.add("region")
|
||||
relations = [...relationSet]
|
||||
|
||||
select = select.filter((v) => !totalFields.includes(v))
|
||||
}
|
||||
|
||||
const toSelect = [...select]
|
||||
if (toSelect.length > 0 && toSelect.indexOf("tax_rate") === -1) {
|
||||
toSelect.push("tax_rate")
|
||||
}
|
||||
|
||||
return {
|
||||
relations,
|
||||
select,
|
||||
select: toSelect,
|
||||
totalsToSelect,
|
||||
}
|
||||
}
|
||||
@@ -352,8 +365,7 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const order = this.decorateTotals_(raw, totalsToSelect)
|
||||
return order
|
||||
return await this.decorateTotals_(raw, totalsToSelect)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,7 +401,7 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const order = this.decorateTotals_(raw, totalsToSelect)
|
||||
const order = await this.decorateTotals_(raw, totalsToSelect)
|
||||
return order
|
||||
}
|
||||
|
||||
@@ -427,7 +439,7 @@ class OrderService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const order = this.decorateTotals_(raw, totalsToSelect)
|
||||
const order = await this.decorateTotals_(raw, totalsToSelect)
|
||||
return order
|
||||
}
|
||||
|
||||
@@ -574,7 +586,6 @@ class OrderService extends BaseService {
|
||||
email: cart.email,
|
||||
customer_id: cart.customer_id,
|
||||
cart_id: cart.id,
|
||||
tax_rate: region.tax_rate,
|
||||
currency_code: region.currency_code,
|
||||
metadata: cart.metadata || {},
|
||||
}
|
||||
@@ -1022,6 +1033,7 @@ class OrderService extends BaseService {
|
||||
order.status = "canceled"
|
||||
order.fulfillment_status = "canceled"
|
||||
order.payment_status = "canceled"
|
||||
order.canceled_at = new Date()
|
||||
|
||||
const orderRepo = manager.getCustomRepository(this.orderRepository_)
|
||||
const result = await orderRepo.save(order)
|
||||
@@ -1398,79 +1410,86 @@ class OrderService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
decorateTotals_(order, totalsFields = []) {
|
||||
if (totalsFields.includes("shipping_total")) {
|
||||
order.shipping_total = this.totalsService_.getShippingTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("gift_card_total")) {
|
||||
order.gift_card_total = this.totalsService_.getGiftCardTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("discount_total")) {
|
||||
order.discount_total = this.totalsService_.getDiscountTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("tax_total")) {
|
||||
order.tax_total = this.totalsService_.getTaxTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("subtotal")) {
|
||||
order.subtotal = this.totalsService_.getSubtotal(order)
|
||||
}
|
||||
if (totalsFields.includes("total")) {
|
||||
order.total = this.totalsService_.getTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("refunded_total")) {
|
||||
order.refunded_total = this.totalsService_.getRefundedTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("paid_total")) {
|
||||
order.paid_total = this.totalsService_.getPaidTotal(order)
|
||||
}
|
||||
if (totalsFields.includes("refundable_amount")) {
|
||||
const paid_total = this.totalsService_.getPaidTotal(order)
|
||||
const refunded_total = this.totalsService_.getRefundedTotal(order)
|
||||
order.refundable_amount = paid_total - refunded_total
|
||||
}
|
||||
|
||||
if (totalsFields.includes("items.refundable")) {
|
||||
order.items = order.items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
if (
|
||||
totalsFields.includes("swaps.additional_items.refundable") &&
|
||||
order.swaps &&
|
||||
order.swaps.length
|
||||
) {
|
||||
for (const s of order.swaps) {
|
||||
s.additional_items = s.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
async decorateTotals_(order, totalsFields = []) {
|
||||
for (const totalField of totalsFields) {
|
||||
switch (totalField) {
|
||||
case "shipping_total": {
|
||||
order.shipping_total = this.totalsService_.getShippingTotal(order)
|
||||
break
|
||||
}
|
||||
case "gift_card_total": {
|
||||
order.gift_card_total = this.totalsService_.getGiftCardTotal(order)
|
||||
break
|
||||
}
|
||||
case "discount_total": {
|
||||
order.discount_total = this.totalsService_.getDiscountTotal(order)
|
||||
break
|
||||
}
|
||||
case "tax_total": {
|
||||
order.tax_total = await this.totalsService_.getTaxTotal(order)
|
||||
break
|
||||
}
|
||||
case "subtotal": {
|
||||
order.subtotal = this.totalsService_.getSubtotal(order)
|
||||
break
|
||||
}
|
||||
case "total": {
|
||||
order.total = await this.totalsService_.getTotal(order)
|
||||
break
|
||||
}
|
||||
case "refunded_total": {
|
||||
order.refunded_total = this.totalsService_.getRefundedTotal(order)
|
||||
break
|
||||
}
|
||||
case "paid_total": {
|
||||
order.paid_total = this.totalsService_.getPaidTotal(order)
|
||||
break
|
||||
}
|
||||
case "refundable_amount": {
|
||||
const paid_total = this.totalsService_.getPaidTotal(order)
|
||||
const refunded_total = this.totalsService_.getRefundedTotal(order)
|
||||
order.refundable_amount = paid_total - refunded_total
|
||||
break
|
||||
}
|
||||
case "items.refundable": {
|
||||
order.items = order.items.map((i) => ({
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
break
|
||||
}
|
||||
case "swaps.additional_items.refundable": {
|
||||
for (const s of order.swaps) {
|
||||
s.additional_items = s.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
break
|
||||
}
|
||||
case "claims.additional_items.refundable": {
|
||||
for (const c of order.claims) {
|
||||
c.additional_items = c.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalsFields.includes("claims.additional_items.refundable") &&
|
||||
order.claims &&
|
||||
order.claims.length
|
||||
) {
|
||||
for (const c of order.claims) {
|
||||
c.additional_items = c.additional_items.map((i) => ({
|
||||
...i,
|
||||
refundable: this.totalsService_.getLineItemRefund(order, {
|
||||
...i,
|
||||
quantity: i.quantity - (i.returned_quantity || 0),
|
||||
}),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ProductTaxRate } from "../models/product-tax-rate"
|
||||
import { ProductTaxRateRepository } from "../repositories/product-tax-rate"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FilterableProductTaxRateProps } from "../types/product-tax-rate"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate product variants.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class ProductTaxRateService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private productTaxRateRepository_: typeof ProductTaxRateRepository
|
||||
|
||||
constructor({ manager, productTaxRateRepository }) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {ProductVariantModel} */
|
||||
this.productTaxRateRepository_ = productTaxRateRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): ProductTaxRateService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ProductTaxRateService({
|
||||
manager: transactionManager,
|
||||
productTaxRateRepository: this.productTaxRateRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FilterableProductVariantProps} selector - the query object for find
|
||||
* @param {FindConfig<ProductVariant>} config - query config object for variant retrieval
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableProductTaxRateProps,
|
||||
config: FindConfig<ProductTaxRate> = { relations: [], skip: 0, take: 20 }
|
||||
): Promise<ProductTaxRate[]> {
|
||||
const pTaxRateRepo = this.manager_.getCustomRepository(
|
||||
this.productTaxRateRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
|
||||
return await pTaxRateRepo.find(query)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductTaxRateService
|
||||
@@ -0,0 +1,99 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FilterableProductTypeProps } from "../types/product"
|
||||
import { ProductType } from "../models/product-type"
|
||||
import { ProductTypeRepository } from "../repositories/product-type"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate products.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class ProductTypeService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private typeRepository_: typeof ProductTypeRepository
|
||||
constructor({ manager, productTypeRepository }) {
|
||||
super()
|
||||
|
||||
this.manager_ = manager
|
||||
this.typeRepository_ = productTypeRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): ProductTypeService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ProductTypeService({
|
||||
manager: transactionManager,
|
||||
productTypeRepository: this.typeRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
cloned.manager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a product by id.
|
||||
* Throws in case of DB Error and if product was not found.
|
||||
* @param id - id of the product to get.
|
||||
* @param config - object that defines what should be included in the
|
||||
* query response
|
||||
* @return {Promise<Product>} the result of the find one operation.
|
||||
*/
|
||||
async retrieve(
|
||||
id: string,
|
||||
config: FindConfig<ProductType> = {}
|
||||
): Promise<ProductType> {
|
||||
const typeRepo = this.manager_.getCustomRepository(this.typeRepository_)
|
||||
|
||||
const query = this.buildQuery_({ id }, config)
|
||||
const type = await typeRepo.findOne(query)
|
||||
|
||||
if (!type) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product with id: ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists product types
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config to be used for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableProductTypeProps = {},
|
||||
config: FindConfig<ProductType> = { skip: 0, take: 20 }
|
||||
): Promise<ProductType[]> {
|
||||
const typeRepo = this.manager_.getCustomRepository(this.typeRepository_)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await typeRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists product tags and adds count.
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config to be used for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async listAndCount(
|
||||
selector: FilterableProductTypeProps = {},
|
||||
config: FindConfig<ProductType> = { skip: 0, take: 20 }
|
||||
): Promise<[ProductType[], number]> {
|
||||
const typeRepo = this.manager_.getCustomRepository(this.typeRepository_)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await typeRepo.findAndCount(query)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductTypeService
|
||||
@@ -22,6 +22,7 @@ class RegionService extends BaseService {
|
||||
currencyRepository,
|
||||
paymentProviderRepository,
|
||||
fulfillmentProviderRepository,
|
||||
taxProviderRepository,
|
||||
paymentProviderService,
|
||||
fulfillmentProviderService,
|
||||
}) {
|
||||
@@ -54,6 +55,9 @@ class RegionService extends BaseService {
|
||||
/** @private @const {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @const {typeof TaxProviderService} */
|
||||
this.taxProviderRepository_ = taxProviderRepository
|
||||
|
||||
/** @private @const {FulfillmentProviderService} */
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
}
|
||||
@@ -72,6 +76,7 @@ class RegionService extends BaseService {
|
||||
eventBusService: this.eventBus_,
|
||||
paymentProviderRepository: this.paymentProviderRepository_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
fulfillmentProviderRepository: this.fulfillmentProviderRepository_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
})
|
||||
@@ -211,6 +216,9 @@ class RegionService extends BaseService {
|
||||
const fpRepository = this.manager_.getCustomRepository(
|
||||
this.fulfillmentProviderRepository_
|
||||
)
|
||||
const tpRepository = this.manager_.getCustomRepository(
|
||||
this.taxProviderRepository_
|
||||
)
|
||||
|
||||
if (region.tax_rate) {
|
||||
this.validateTaxRate_(region.tax_rate)
|
||||
@@ -226,6 +234,18 @@ class RegionService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
if (region.tax_provider_id) {
|
||||
const tp = await tpRepository.findOne({
|
||||
where: { id: region.tax_provider_id },
|
||||
})
|
||||
if (!tp) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Tax provider not found"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (region.payment_providers) {
|
||||
region.payment_providers = await Promise.all(
|
||||
region.payment_providers.map(async (pId) => {
|
||||
|
||||
@@ -14,6 +14,7 @@ class ReturnService extends BaseService {
|
||||
returnItemRepository,
|
||||
shippingOptionService,
|
||||
returnReasonService,
|
||||
taxProviderService,
|
||||
fulfillmentProviderService,
|
||||
inventoryService,
|
||||
orderService,
|
||||
@@ -35,6 +36,8 @@ class ReturnService extends BaseService {
|
||||
/** @private @const {ReturnItemRepository} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
this.taxProviderService_ = taxProviderService
|
||||
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
@@ -59,6 +62,7 @@ class ReturnService extends BaseService {
|
||||
totalsService: this.totalsService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
returnRepository: this.returnRepository_,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
returnItemRepository: this.returnItemRepository_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
@@ -335,9 +339,14 @@ class ReturnService extends BaseService {
|
||||
relations: [
|
||||
"swaps",
|
||||
"swaps.additional_items",
|
||||
"swaps.additional_items.tax_lines",
|
||||
"claims",
|
||||
"claims.additional_items",
|
||||
"claims.additional_items.tax_lines",
|
||||
"items",
|
||||
"items.tax_lines",
|
||||
"region",
|
||||
"region.tax_rates",
|
||||
],
|
||||
})
|
||||
|
||||
@@ -347,18 +356,10 @@ class ReturnService extends BaseService {
|
||||
this.validateReturnLineItem_
|
||||
)
|
||||
|
||||
if (data.shipping_method) {
|
||||
if (typeof data.shipping_method.price === "undefined") {
|
||||
const opt = await this.shippingOptionService_.retrieve(
|
||||
data.shipping_method.option_id
|
||||
)
|
||||
data.shipping_method.price = opt.amount
|
||||
}
|
||||
}
|
||||
|
||||
let toRefund = data.refund_amount
|
||||
if (typeof toRefund !== "undefined") {
|
||||
// refundable from order
|
||||
// Merchant wants to do a custom refund amount; we check if amount is
|
||||
// refundable
|
||||
const refundable = order.refundable_amount
|
||||
|
||||
if (toRefund > refundable) {
|
||||
@@ -368,14 +369,8 @@ class ReturnService extends BaseService {
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Merchant hasn't specified refund amount so we calculate it
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, returnLines)
|
||||
|
||||
if (data.shipping_method) {
|
||||
toRefund = Math.max(
|
||||
0,
|
||||
toRefund - data.shipping_method.price * (1 + order.tax_rate / 100)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const method = data.shipping_method
|
||||
@@ -416,7 +411,7 @@ class ReturnService extends BaseService {
|
||||
const result = await returnRepository.save(created)
|
||||
|
||||
if (method) {
|
||||
await this.shippingOptionService_
|
||||
const shippingMethod = await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.createShippingMethod(
|
||||
method.option_id,
|
||||
@@ -426,6 +421,26 @@ class ReturnService extends BaseService {
|
||||
return_id: result.id,
|
||||
}
|
||||
)
|
||||
|
||||
const calculationContext =
|
||||
this.totalsService_.getCalculationContext(order)
|
||||
|
||||
const taxLines = await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
.createShippingTaxLines(shippingMethod, calculationContext)
|
||||
|
||||
const shippingTotal =
|
||||
shippingMethod.price +
|
||||
taxLines.reduce(
|
||||
(acc, tl) =>
|
||||
acc + Math.round(shippingMethod.price * (tl.rate / 100)),
|
||||
0
|
||||
)
|
||||
|
||||
if (typeof data.refund_amount === "undefined") {
|
||||
result.refund_amount = toRefund - shippingTotal
|
||||
return await returnRepository.save(result)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -438,6 +453,7 @@ class ReturnService extends BaseService {
|
||||
relations: [
|
||||
"items",
|
||||
"shipping_method",
|
||||
"shipping_method.tax_lines",
|
||||
"shipping_method.shipping_option",
|
||||
"swap",
|
||||
"claim_order",
|
||||
@@ -453,9 +469,12 @@ class ReturnService extends BaseService {
|
||||
|
||||
const returnData = { ...returnOrder }
|
||||
|
||||
const items = await this.lineItemService_.list({
|
||||
id: returnOrder.items.map(({ item_id }) => item_id),
|
||||
})
|
||||
const items = await this.lineItemService_.list(
|
||||
{
|
||||
id: returnOrder.items.map(({ item_id }) => item_id),
|
||||
},
|
||||
{ relations: ["tax_lines"] }
|
||||
)
|
||||
|
||||
returnData.items = returnOrder.items.map((item) => {
|
||||
const found = items.find((i) => i.id === item.item_id)
|
||||
|
||||
@@ -13,7 +13,6 @@ class ShippingOptionService extends BaseService {
|
||||
shippingMethodRepository,
|
||||
fulfillmentProviderService,
|
||||
regionService,
|
||||
totalsService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -34,9 +33,6 @@ class ShippingOptionService extends BaseService {
|
||||
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -51,7 +47,6 @@ class ShippingOptionService extends BaseService {
|
||||
shippingOptionRequirementRepository: this.requirementRepository_,
|
||||
fulfillmentProviderService: this.providerService_,
|
||||
regionService: this.regionService_,
|
||||
totalsService: this.totalsService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -132,6 +127,18 @@ class ShippingOptionService extends BaseService {
|
||||
return optRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {object} config - config object
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async listAndCount(selector, config = { skip: 0, take: 50 }) {
|
||||
const optRepo = this.manager_.getCustomRepository(this.optionRepository_)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await optRepo.findAndCount(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a profile by id.
|
||||
* Throws in case of DB Error and if profile was not found.
|
||||
@@ -236,7 +243,7 @@ class ShippingOptionService extends BaseService {
|
||||
)
|
||||
|
||||
let methodPrice
|
||||
if ("price" in config) {
|
||||
if (typeof config.price === "number") {
|
||||
methodPrice = config.price
|
||||
} else {
|
||||
methodPrice = await this.getPrice_(option, validatedData, config.cart)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
import { ShippingTaxRateRepository } from "../repositories/shipping-tax-rate"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { FilterableShippingTaxRateProps } from "../types/shipping-tax-rate"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate Shipping variants.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class ShippingTaxRateService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private shippingTaxRateRepository_: typeof ShippingTaxRateRepository
|
||||
|
||||
constructor({ manager, shippingTaxRateRepository }) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {ShippingVariantModel} */
|
||||
this.shippingTaxRateRepository_ = shippingTaxRateRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): ShippingTaxRateService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ShippingTaxRateService({
|
||||
manager: transactionManager,
|
||||
shippingTaxRateRepository: this.ShippingTaxRateRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists Shipping Tax Rates given a certain query.
|
||||
* @param selector - the query object for find
|
||||
* @param config - query config object for variant retrieval
|
||||
* @return the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector: FilterableShippingTaxRateProps,
|
||||
config: FindConfig<ShippingTaxRate> = { relations: [], skip: 0, take: 20 }
|
||||
): Promise<ShippingTaxRate[]> {
|
||||
const sTaxRateRepo = this.manager_.getCustomRepository(
|
||||
this.shippingTaxRateRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
|
||||
return await sTaxRateRepo.find(query)
|
||||
}
|
||||
}
|
||||
|
||||
export default ShippingTaxRateService
|
||||
@@ -27,6 +27,7 @@ class SwapService extends BaseService {
|
||||
returnService,
|
||||
lineItemService,
|
||||
paymentProviderService,
|
||||
shippingMethodTaxLineRepository,
|
||||
shippingOptionService,
|
||||
fulfillmentService,
|
||||
orderService,
|
||||
@@ -71,6 +72,9 @@ class SwapService extends BaseService {
|
||||
/** @private @const {EventBusService} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @const {typeof ShippingMethodTaxLineRepository} */
|
||||
this.shippingTaxLineRepo_ = shippingMethodTaxLineRepository
|
||||
|
||||
/** @private @const {CustomShippingOptionService} */
|
||||
this.customShippingOptionService_ = customShippingOptionService
|
||||
}
|
||||
@@ -88,6 +92,7 @@ class SwapService extends BaseService {
|
||||
totalsService: this.totalsService_,
|
||||
returnService: this.returnService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
shippingMethodTaxLineRepository: this.shippingTaxLineRepo_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
orderService: this.orderService_,
|
||||
@@ -101,71 +106,67 @@ class SwapService extends BaseService {
|
||||
return cloned
|
||||
}
|
||||
|
||||
transformQueryForTotals_(config) {
|
||||
transformQueryForCart_(config) {
|
||||
let { select, relations } = config
|
||||
|
||||
if (!select) {
|
||||
return {
|
||||
...config,
|
||||
totalsToSelect: [],
|
||||
let cartSelects = null
|
||||
let cartRelations = null
|
||||
|
||||
if (typeof relations !== "undefined" && relations.includes("cart")) {
|
||||
const [swapRelations, cartRels] = relations.reduce(
|
||||
(acc, next) => {
|
||||
if (next === "cart") {
|
||||
return acc
|
||||
}
|
||||
|
||||
if (next.startsWith("cart.")) {
|
||||
const [, ...rel] = next.split(".")
|
||||
acc[1].push(rel.join("."))
|
||||
} else {
|
||||
acc[0].push(next)
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
[[], []]
|
||||
)
|
||||
|
||||
relations = swapRelations
|
||||
cartRelations = cartRels
|
||||
|
||||
let foundCartId = false
|
||||
if (typeof select !== "undefined") {
|
||||
const [swapSelects, cartSels] = select.reduce(
|
||||
(acc, next) => {
|
||||
if (next.startsWith("cart.")) {
|
||||
const [, ...rel] = next.split(".")
|
||||
acc[1].push(rel.join("."))
|
||||
} else {
|
||||
if (next === "cart_id") {
|
||||
foundCartId = true
|
||||
}
|
||||
acc[0].push(next)
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
[[], []]
|
||||
)
|
||||
|
||||
select = foundCartId ? swapSelects : [...swapSelects, "cart_id"]
|
||||
cartSelects = cartSels
|
||||
}
|
||||
}
|
||||
|
||||
const totalFields = [
|
||||
"cart.subtotal",
|
||||
"cart.tax_total",
|
||||
"cart.shipping_total",
|
||||
"cart.discount_total",
|
||||
"cart.gift_card_total",
|
||||
"cart.total",
|
||||
]
|
||||
|
||||
const totalsToSelect = select.filter((v) => totalFields.includes(v))
|
||||
if (totalsToSelect.length > 0) {
|
||||
const relationSet = new Set(relations)
|
||||
relationSet.add("cart")
|
||||
relationSet.add("cart.items")
|
||||
relationSet.add("cart.gift_cards")
|
||||
relationSet.add("cart.discounts")
|
||||
relationSet.add("cart.discounts.rule")
|
||||
relationSet.add("cart.discounts.rule.valid_for")
|
||||
relationSet.add("cart.shipping_methods")
|
||||
relationSet.add("cart.region")
|
||||
relations = [...relationSet]
|
||||
|
||||
select = select.filter((v) => !totalFields.includes(v))
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
relations,
|
||||
select,
|
||||
totalsToSelect,
|
||||
cartSelects,
|
||||
cartRelations,
|
||||
}
|
||||
}
|
||||
|
||||
async decorateTotals_(cart, totalsFields = []) {
|
||||
if (totalsFields.includes("cart.shipping_total")) {
|
||||
cart.shipping_total = await this.totalsService_.getShippingTotal(cart)
|
||||
}
|
||||
if (totalsFields.includes("cart.discount_total")) {
|
||||
cart.discount_total = await this.totalsService_.getDiscountTotal(cart)
|
||||
}
|
||||
if (totalsFields.includes("cart.tax_total")) {
|
||||
cart.tax_total = await this.totalsService_.getTaxTotal(cart)
|
||||
}
|
||||
if (totalsFields.includes("cart.gift_card_total")) {
|
||||
cart.gift_card_total = await this.totalsService_.getGiftCardTotal(cart)
|
||||
}
|
||||
if (totalsFields.includes("cart.subtotal")) {
|
||||
cart.subtotal = await this.totalsService_.getSubtotal(cart)
|
||||
}
|
||||
if (totalsFields.includes("cart.total")) {
|
||||
cart.total = await this.totalsService_.getTotal(cart)
|
||||
}
|
||||
return cart
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a swap with the given id.
|
||||
* @param {string} id - the id of the swap to retrieve
|
||||
@@ -177,8 +178,8 @@ class SwapService extends BaseService {
|
||||
|
||||
const validatedId = this.validateId_(id)
|
||||
|
||||
const { totalsToSelect, ...newConfig } =
|
||||
this.transformQueryForTotals_(config)
|
||||
const { cartSelects, cartRelations, ...newConfig } =
|
||||
this.transformQueryForCart_(config)
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, newConfig)
|
||||
|
||||
@@ -190,8 +191,11 @@ class SwapService extends BaseService {
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, "Swap was not found")
|
||||
}
|
||||
|
||||
if (rels && rels.includes("cart")) {
|
||||
const cart = await this.decorateTotals_(swap.cart, totalsToSelect)
|
||||
if (cartRelations || cartSelects) {
|
||||
const cart = await this.cartService_.retrieve(swap.cart_id, {
|
||||
select: cartSelects,
|
||||
relations: cartRelations,
|
||||
})
|
||||
swap.cart = cart
|
||||
}
|
||||
|
||||
@@ -548,6 +552,7 @@ class SwapService extends BaseService {
|
||||
"return_order",
|
||||
"return_order.items",
|
||||
"return_order.shipping_method",
|
||||
"return_order.shipping_method.tax_lines",
|
||||
],
|
||||
})
|
||||
|
||||
@@ -602,9 +607,12 @@ class SwapService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
// If the swap has a return shipping method the price has to be added to the
|
||||
// cart.
|
||||
// If the swap has a return shipping method the price has to be added to
|
||||
// the cart.
|
||||
if (swap.return_order && swap.return_order.shipping_method) {
|
||||
const shippingTaxLineRepo = this.manager_.getCustomRepository(
|
||||
this.shippingTaxLineRepo_
|
||||
)
|
||||
await this.lineItemService_.withTransaction(manager).create({
|
||||
cart_id: cart.id,
|
||||
title: "Return shipping",
|
||||
@@ -612,51 +620,26 @@ class SwapService extends BaseService {
|
||||
has_shipping: true,
|
||||
allow_discounts: false,
|
||||
unit_price: swap.return_order.shipping_method.price,
|
||||
metadata: {
|
||||
is_return_line: true,
|
||||
},
|
||||
is_return: true,
|
||||
tax_lines: swap.return_order.shipping_method.tax_lines.map((tl) => {
|
||||
return shippingTaxLineRepo.create({
|
||||
name: tl.name,
|
||||
code: tl.code,
|
||||
rate: tl.rate,
|
||||
metadata: tl.metadata,
|
||||
})
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
for (const r of swap.return_order.items) {
|
||||
let allItems = [...order.items]
|
||||
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const s of order.swaps) {
|
||||
allItems = [...allItems, ...s.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
if (order.claims && order.claims.length) {
|
||||
for (const c of order.claims) {
|
||||
allItems = [...allItems, ...c.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
const lineItem = allItems.find((i) => i.id === r.item_id)
|
||||
|
||||
const toCreate = {
|
||||
cart_id: cart.id,
|
||||
thumbnail: lineItem.thumbnail,
|
||||
title: lineItem.title,
|
||||
variant_id: lineItem.variant_id,
|
||||
unit_price: -1 * lineItem.unit_price,
|
||||
quantity: r.quantity,
|
||||
allow_discounts: lineItem.allow_discounts,
|
||||
metadata: {
|
||||
...lineItem.metadata,
|
||||
is_return_line: true,
|
||||
},
|
||||
}
|
||||
|
||||
await this.lineItemService_.withTransaction(manager).create(toCreate)
|
||||
}
|
||||
await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.createReturnLines(swap.return_order.id, cart.id)
|
||||
|
||||
swap.cart_id = cart.id
|
||||
|
||||
const swapRepo = manager.getCustomRepository(this.swapRepository_)
|
||||
const result = await swapRepo.save(swap)
|
||||
return result
|
||||
return await swapRepo.save(swap)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -664,18 +647,16 @@ class SwapService extends BaseService {
|
||||
*@param {string} swapId - The id of the swap
|
||||
*/
|
||||
async registerCartCompletion(swapId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
return await this.atomicPhase_(async (manager) => {
|
||||
const swap = await this.retrieve(swapId, {
|
||||
relations: [
|
||||
"cart",
|
||||
"cart.region",
|
||||
"cart.shipping_methods",
|
||||
"cart.shipping_address",
|
||||
"cart.items",
|
||||
"cart.discounts",
|
||||
"cart.discounts.rule",
|
||||
"cart.payment",
|
||||
"cart.gift_cards",
|
||||
select: [
|
||||
"id",
|
||||
"order_id",
|
||||
"no_notification",
|
||||
"allow_backorder",
|
||||
"canceled_at",
|
||||
"confirmed_at",
|
||||
"cart_id",
|
||||
],
|
||||
})
|
||||
|
||||
@@ -691,10 +672,14 @@ class SwapService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const cart = swap.cart
|
||||
const cart = await this.cartService_.retrieve(swap.cart_id, {
|
||||
select: ["total"],
|
||||
relations: ["payment", "shipping_methods", "items"],
|
||||
})
|
||||
|
||||
const { payment } = cart
|
||||
|
||||
const items = swap.cart.items
|
||||
const items = cart.items
|
||||
|
||||
if (!swap.allow_backorder) {
|
||||
for (const item of items) {
|
||||
@@ -716,7 +701,7 @@ class SwapService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const total = await this.totalsService_.getTotal(cart)
|
||||
const total = cart.total
|
||||
|
||||
if (total > 0) {
|
||||
if (!payment) {
|
||||
@@ -785,49 +770,6 @@ class SwapService extends BaseService {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the return associated with a swap as received. If the return
|
||||
* is received with mismatching return items the swap's status will be updated
|
||||
* to requires_action.
|
||||
* @param {string} swapId - the id of the swap to receive.
|
||||
* @param {Array<ReturnItem>} returnItems - the return items that have been returned
|
||||
* @return {Promise<Swap>} the resulting swap, with an updated return and
|
||||
* status.
|
||||
*/
|
||||
async receiveReturn(swapId, returnItems) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const swap = await this.retrieve(swapId, { relations: ["return_order"] })
|
||||
|
||||
if (swap.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled swap cannot be registered as received"
|
||||
)
|
||||
}
|
||||
|
||||
const returnId = swap.return_order && swap.return_order.id
|
||||
if (!returnId) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Swap has no return request"
|
||||
)
|
||||
}
|
||||
|
||||
const updatedRet = await this.returnService_
|
||||
.withTransaction(manager)
|
||||
.receiveReturn(returnId, returnItems, undefined, false)
|
||||
|
||||
if (updatedRet.status === "requires_action") {
|
||||
const swapRepo = manager.getCustomRepository(this.swapRepository_)
|
||||
swap.fulfillment_status = "requires_action"
|
||||
const result = await swapRepo.save(swap)
|
||||
return result
|
||||
}
|
||||
|
||||
return this.retrieve(swapId)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a given swap if possible. A swap can only be canceled if all
|
||||
* related returns, fulfillments, and payments have been canceled. If a swap
|
||||
@@ -908,8 +850,11 @@ class SwapService extends BaseService {
|
||||
"payment",
|
||||
"shipping_address",
|
||||
"additional_items",
|
||||
"additional_items.tax_lines",
|
||||
"shipping_methods",
|
||||
"shipping_methods.tax_lines",
|
||||
"order",
|
||||
"order.region",
|
||||
"order.billing_address",
|
||||
"order.discounts",
|
||||
"order.discounts.rule",
|
||||
@@ -956,6 +901,7 @@ class SwapService extends BaseService {
|
||||
currency_code: order.currency_code,
|
||||
tax_rate: order.tax_rate,
|
||||
region_id: order.region_id,
|
||||
region: order.region,
|
||||
display_id: order.display_id,
|
||||
billing_address: order.billing_address,
|
||||
items: swap.additional_items,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
import {
|
||||
ITaxService,
|
||||
ItemTaxCalculationLine,
|
||||
ShippingTaxCalculationLine,
|
||||
TaxCalculationContext,
|
||||
} from "../interfaces/tax-service"
|
||||
import { ProviderTaxLine } from "../types/tax-service"
|
||||
|
||||
class SystemTaxService extends BaseService implements ITaxService {
|
||||
static identifier = "system"
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
async getTaxLines(
|
||||
itemLines: ItemTaxCalculationLine[],
|
||||
shippingLines: ShippingTaxCalculationLine[],
|
||||
context: TaxCalculationContext // eslint-disable-line
|
||||
): Promise<ProviderTaxLine[]> {
|
||||
let taxLines: ProviderTaxLine[] = itemLines.flatMap((l) => {
|
||||
return l.rates.map((r) => ({
|
||||
rate: r.rate || 0,
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
item_id: l.item.id,
|
||||
}))
|
||||
})
|
||||
|
||||
taxLines = taxLines.concat(
|
||||
shippingLines.flatMap((l) => {
|
||||
return l.rates.map((r) => ({
|
||||
rate: r.rate || 0,
|
||||
name: r.name,
|
||||
code: r.code,
|
||||
shipping_method_id: l.shipping_method.id,
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
return taxLines
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemTaxService
|
||||
@@ -0,0 +1,433 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { AwilixContainer } from "awilix"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import Redis from "ioredis"
|
||||
|
||||
import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line"
|
||||
import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line"
|
||||
import { TaxProviderRepository } from "../repositories/tax-provider"
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { TaxProvider } from "../models/tax-provider"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { Region } from "../models/region"
|
||||
import { Cart } from "../models/cart"
|
||||
import { isCart } from "../types/cart"
|
||||
import {
|
||||
ITaxService,
|
||||
ItemTaxCalculationLine,
|
||||
TaxCalculationContext,
|
||||
} from "../interfaces/tax-service"
|
||||
|
||||
import { TaxServiceRate } from "../types/tax-service"
|
||||
|
||||
import TaxRateService from "./tax-rate"
|
||||
|
||||
const CACHE_TIME = 30 // seconds
|
||||
|
||||
/**
|
||||
* Finds tax providers and assists in tax related operations.
|
||||
*/
|
||||
class TaxProviderService extends BaseService {
|
||||
private container_: AwilixContainer
|
||||
private manager_: EntityManager
|
||||
private transactionManager_: EntityManager
|
||||
private taxRateService_: TaxRateService
|
||||
private taxLineRepo_: typeof LineItemTaxLineRepository
|
||||
private smTaxLineRepo_: typeof ShippingMethodTaxLineRepository
|
||||
private taxProviderRepo_: typeof TaxProviderRepository
|
||||
private redis_: Redis
|
||||
|
||||
constructor(container: AwilixContainer) {
|
||||
super()
|
||||
|
||||
this.container_ = container
|
||||
this.taxLineRepo_ = container["lineItemTaxLineRepository"]
|
||||
this.smTaxLineRepo_ = container["shippingMethodTaxLineRepository"]
|
||||
this.taxRateService_ = container["taxRateService"]
|
||||
this.eventBus_ = container["eventBusService"]
|
||||
this.taxProviderRepo_ = container["taxProviderRepository"]
|
||||
this.manager_ = container["manager"]
|
||||
this.redis_ = container["redisClient"]
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): TaxProviderService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new TaxProviderService(this.container_)
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
cloned.manager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
async list(): Promise<TaxProvider[]> {
|
||||
const tpRepo = this.manager_.getCustomRepository(this.taxProviderRepo_)
|
||||
return tpRepo.find({})
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the relevant tax provider for the given region.
|
||||
* @param region - the region to get tax provider for.
|
||||
* @return the region specific tax provider
|
||||
*/
|
||||
retrieveProvider(region: Region): ITaxService {
|
||||
let provider: ITaxService
|
||||
if (region.tax_provider_id) {
|
||||
provider = this.container_[`tp_${region.tax_provider_id}`]
|
||||
} else {
|
||||
provider = this.container_["systemTaxService"]
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a tax provider with id: ${region.tax_provider_id}`
|
||||
)
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the tax lines relevant for an order to the database.
|
||||
* @param cartOrLineItems - the cart or line items to create tax lines for
|
||||
* @param calculationContext - the calculation context to get tax lines by
|
||||
* @return the newly created tax lines
|
||||
*/
|
||||
async createTaxLines(
|
||||
cartOrLineItems: Cart | LineItem[],
|
||||
calculationContext: TaxCalculationContext
|
||||
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
|
||||
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[] = []
|
||||
if (isCart(cartOrLineItems)) {
|
||||
taxLines = await this.getTaxLines(
|
||||
cartOrLineItems.items,
|
||||
calculationContext
|
||||
)
|
||||
} else {
|
||||
taxLines = await this.getTaxLines(cartOrLineItems, calculationContext)
|
||||
}
|
||||
|
||||
return this.manager_.save(taxLines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the tax lines relevant for a shipping method to the database. Used
|
||||
* for return shipping methods.
|
||||
* @param shippingMethod - the shipping method to create tax lines for
|
||||
* @param calculationContext - the calculation context to get tax lines by
|
||||
* @return the newly created tax lines
|
||||
*/
|
||||
async createShippingTaxLines(
|
||||
shippingMethod: ShippingMethod,
|
||||
calculationContext: TaxCalculationContext
|
||||
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
|
||||
const taxLines = await this.getShippingTaxLines(
|
||||
shippingMethod,
|
||||
calculationContext
|
||||
)
|
||||
return this.manager_.save(taxLines)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relevant tax lines for a shipping method. Note: this method
|
||||
* doesn't persist the tax lines. Use createShippingTaxLines if you wish to
|
||||
* persist the tax lines to the DB layer.
|
||||
* @param shippingMethod - the shipping method to get tax lines for
|
||||
* @param calculationContext - the calculation context to get tax lines by
|
||||
* @return the computed tax lines
|
||||
*/
|
||||
async getShippingTaxLines(
|
||||
shippingMethod: ShippingMethod,
|
||||
calculationContext: TaxCalculationContext
|
||||
): Promise<ShippingMethodTaxLine[]> {
|
||||
const calculationLines = [
|
||||
{
|
||||
shipping_method: shippingMethod,
|
||||
rates: await this.getRegionRatesForShipping(
|
||||
shippingMethod.shipping_option_id,
|
||||
calculationContext.region
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const taxProvider = this.retrieveProvider(calculationContext.region)
|
||||
const providerLines = await taxProvider.getTaxLines(
|
||||
[],
|
||||
calculationLines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
const smTaxLineRepo = this.manager_.getCustomRepository(this.smTaxLineRepo_)
|
||||
|
||||
// .create only creates entities nothing is persisted in DB
|
||||
return providerLines.map((pl) => {
|
||||
if (!("shipping_method_id" in pl)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Expected only shipping method tax lines"
|
||||
)
|
||||
}
|
||||
|
||||
return smTaxLineRepo.create({
|
||||
shipping_method_id: pl.shipping_method_id,
|
||||
rate: pl.rate,
|
||||
name: pl.name,
|
||||
code: pl.code,
|
||||
metadata: pl.metadata,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the relevant tax lines for an order or cart. If an order is provided
|
||||
* the order's tax lines will be returned. If a cart is provided the tax lines
|
||||
* will be computed from the tax rules and potentially a 3rd party tax plugin.
|
||||
* Note: this method doesn't persist the tax lines. Use createTaxLines if you
|
||||
* wish to persist the tax lines to the DB layer.
|
||||
* @param lineItems - the cart or order to get tax lines for
|
||||
* @param calculationContext - the calculation context to get tax lines by
|
||||
* @return the computed tax lines
|
||||
*/
|
||||
async getTaxLines(
|
||||
lineItems: LineItem[],
|
||||
calculationContext: TaxCalculationContext
|
||||
): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> {
|
||||
const calculationLines = await Promise.all(
|
||||
lineItems.map(async (l) => {
|
||||
if (l.is_return) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (l.variant && l.variant.product_id) {
|
||||
return {
|
||||
item: l,
|
||||
rates: await this.getRegionRatesForProduct(
|
||||
l.variant.product_id,
|
||||
calculationContext.region
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If the line item is custom and therefore not associated with a
|
||||
* product we assume no taxes - we should consider adding rate overrides
|
||||
* to custom lines at some point
|
||||
*/
|
||||
return {
|
||||
item: l,
|
||||
rates: [],
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const shippingCalculationLines = await Promise.all(
|
||||
calculationContext.shipping_methods.map(async (sm) => {
|
||||
return {
|
||||
shipping_method: sm,
|
||||
rates: await this.getRegionRatesForShipping(
|
||||
sm.shipping_option_id,
|
||||
calculationContext.region
|
||||
),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const taxProvider = this.retrieveProvider(calculationContext.region)
|
||||
const providerLines = await taxProvider.getTaxLines(
|
||||
calculationLines.filter((v) => v !== null) as ItemTaxCalculationLine[],
|
||||
shippingCalculationLines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
const liTaxLineRepo = this.manager_.getCustomRepository(this.taxLineRepo_)
|
||||
const smTaxLineRepo = this.manager_.getCustomRepository(this.smTaxLineRepo_)
|
||||
|
||||
// .create only creates entities nothing is persisted in DB
|
||||
return providerLines.map((pl) => {
|
||||
if ("shipping_method_id" in pl) {
|
||||
return smTaxLineRepo.create({
|
||||
shipping_method_id: pl.shipping_method_id,
|
||||
rate: pl.rate,
|
||||
name: pl.name,
|
||||
code: pl.code,
|
||||
metadata: pl.metadata,
|
||||
})
|
||||
}
|
||||
|
||||
if (!("item_id" in pl)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Provider returned invalid tax lines"
|
||||
)
|
||||
}
|
||||
|
||||
return liTaxLineRepo.create({
|
||||
item_id: pl.item_id,
|
||||
rate: pl.rate,
|
||||
name: pl.name,
|
||||
code: pl.code,
|
||||
metadata: pl.metadata,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tax rates configured for a shipping option. The rates are cached
|
||||
* between calls.
|
||||
* @param optionId - the option id of the shipping method.
|
||||
* @param region - the region to get configured rates for.
|
||||
* @return the tax rates configured for the shipping option.
|
||||
*/
|
||||
async getRegionRatesForShipping(
|
||||
optionId: string,
|
||||
region: Region
|
||||
): Promise<TaxServiceRate[]> {
|
||||
const cacheHit = await this.getCacheEntry(optionId, region.id)
|
||||
if (cacheHit) {
|
||||
return cacheHit
|
||||
}
|
||||
|
||||
let toReturn: TaxServiceRate[] = []
|
||||
const optionRates = await this.taxRateService_.listByShippingOption(
|
||||
optionId,
|
||||
{ region_id: region.id }
|
||||
)
|
||||
|
||||
if (optionRates.length > 0) {
|
||||
toReturn = optionRates.map((pr) => {
|
||||
return {
|
||||
rate: pr.rate,
|
||||
name: pr.name,
|
||||
code: pr.code,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (toReturn.length === 0) {
|
||||
toReturn = [
|
||||
{
|
||||
rate: region.tax_rate,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
await this.setCache(optionId, region.id, toReturn)
|
||||
|
||||
return toReturn
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tax rates configured for a product. The rates are cached between
|
||||
* calls.
|
||||
* @param productId - the product id to get rates for
|
||||
* @param region - the region to get configured rates for.
|
||||
* @return the tax rates configured for the shipping option.
|
||||
*/
|
||||
async getRegionRatesForProduct(
|
||||
productId: string,
|
||||
region: Region
|
||||
): Promise<TaxServiceRate[]> {
|
||||
const cacheHit = await this.getCacheEntry(productId, region.id)
|
||||
if (cacheHit) {
|
||||
return cacheHit
|
||||
}
|
||||
|
||||
let toReturn: TaxServiceRate[] = []
|
||||
const productRates = await this.taxRateService_.listByProduct(productId, {
|
||||
region_id: region.id,
|
||||
})
|
||||
|
||||
if (productRates.length > 0) {
|
||||
toReturn = productRates.map((pr) => {
|
||||
return {
|
||||
rate: pr.rate,
|
||||
name: pr.name,
|
||||
code: pr.code,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (toReturn.length === 0) {
|
||||
toReturn = [
|
||||
{
|
||||
rate: region.tax_rate,
|
||||
name: "default",
|
||||
code: "default",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
await this.setCache(productId, region.id, toReturn)
|
||||
|
||||
return toReturn
|
||||
}
|
||||
|
||||
/**
|
||||
* The cache key to get cache hits by.
|
||||
* @param productId - the product id to cache
|
||||
* @param regionId - the region id to cache
|
||||
* @return the cache key to use for the id set
|
||||
*/
|
||||
private getCacheKey(productId: string, regionId: string): string {
|
||||
return `txrtcache:${productId}:${regionId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache results for a set of ids
|
||||
* @param productId - the product id to cache
|
||||
* @param regionId - the region id to cache
|
||||
* @param value - tax rates to cache
|
||||
* @return promise that resolves after the cache has been set
|
||||
*/
|
||||
private async setCache(
|
||||
productId: string,
|
||||
regionId: string,
|
||||
value: TaxServiceRate[]
|
||||
): Promise<void> {
|
||||
const cacheKey = this.getCacheKey(productId, regionId)
|
||||
return await this.redis_.set(
|
||||
cacheKey,
|
||||
JSON.stringify(value),
|
||||
"EX",
|
||||
CACHE_TIME
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cache results for a set of ids
|
||||
* @param productId - the product id to cache
|
||||
* @param regionId - the region id to cache
|
||||
* @return the cached result or null
|
||||
*/
|
||||
private async getCacheEntry(
|
||||
productId: string,
|
||||
regionId: string
|
||||
): Promise<TaxServiceRate[] | null> {
|
||||
const cacheKey = this.getCacheKey(productId, regionId)
|
||||
|
||||
try {
|
||||
const cacheHit = await this.redis_.get(cacheKey)
|
||||
if (cacheHit) {
|
||||
// TODO: Validate that cache has correct data
|
||||
const parsedResults = JSON.parse(cacheHit) as TaxServiceRate[]
|
||||
return parsedResults
|
||||
}
|
||||
} catch (_) {
|
||||
// noop - cache parse failed
|
||||
await this.redis_.del(cacheKey)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default TaxProviderService
|
||||
@@ -0,0 +1,343 @@
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { TaxRate } from "../models/tax-rate"
|
||||
import { ShippingTaxRate } from "../models/shipping-tax-rate"
|
||||
import { ProductTaxRate } from "../models/product-tax-rate"
|
||||
import { ProductTypeTaxRate } from "../models/product-type-tax-rate"
|
||||
import { TaxRateRepository } from "../repositories/tax-rate"
|
||||
import ProductService from "../services/product"
|
||||
import ProductTypeService from "../services/product-type"
|
||||
import ShippingOptionService from "../services/shipping-option"
|
||||
import { FindConfig } from "../types/common"
|
||||
import {
|
||||
CreateTaxRateInput,
|
||||
UpdateTaxRateInput,
|
||||
TaxRateListByConfig,
|
||||
FilterableTaxRateProps,
|
||||
} from "../types/tax-rate"
|
||||
|
||||
class TaxRateService extends BaseService {
|
||||
private manager_: EntityManager
|
||||
private productService_: ProductService
|
||||
private productTypeService_: ProductTypeService
|
||||
private shippingOptionService_: ShippingOptionService
|
||||
private taxRateRepository_: typeof TaxRateRepository
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
productService,
|
||||
productTypeService,
|
||||
shippingOptionService,
|
||||
taxRateRepository,
|
||||
}) {
|
||||
super()
|
||||
|
||||
this.manager_ = manager
|
||||
this.taxRateRepository_ = taxRateRepository
|
||||
this.productService_ = productService
|
||||
this.productTypeService_ = productTypeService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager: EntityManager): TaxRateService {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new TaxRateService({
|
||||
manager: transactionManager,
|
||||
taxRateRepository: this.taxRateRepository_,
|
||||
productService: this.productService_,
|
||||
productTypeService: this.productTypeService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
cloned.manager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
async list(
|
||||
selector: FilterableTaxRateProps,
|
||||
config: FindConfig<TaxRate> = {}
|
||||
): Promise<TaxRate[]> {
|
||||
const taxRateRepo = this.manager_.getCustomRepository(
|
||||
this.taxRateRepository_
|
||||
)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await taxRateRepo.findWithResolution(query)
|
||||
}
|
||||
|
||||
async listAndCount(
|
||||
selector: FilterableTaxRateProps,
|
||||
config: FindConfig<TaxRate> = {}
|
||||
): Promise<[TaxRate[], number]> {
|
||||
const taxRateRepo = this.manager_.getCustomRepository(
|
||||
this.taxRateRepository_
|
||||
)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return await taxRateRepo.findAndCountWithResolution(query)
|
||||
}
|
||||
|
||||
async retrieve(
|
||||
id: string,
|
||||
config: FindConfig<TaxRate> = {}
|
||||
): Promise<TaxRate> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
const query = this.buildQuery_({ id }, config)
|
||||
|
||||
const taxRate = await taxRateRepo.findOneWithResolution(query)
|
||||
if (!taxRate) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`TaxRate with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return taxRate
|
||||
})
|
||||
}
|
||||
|
||||
async create(data: CreateTaxRateInput): Promise<TaxRate> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
|
||||
if (typeof data.region_id === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"TaxRates must belong to a Region"
|
||||
)
|
||||
}
|
||||
|
||||
const taxRate = taxRateRepo.create(data)
|
||||
return await taxRateRepo.save(taxRate)
|
||||
})
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateTaxRateInput): Promise<TaxRate> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
const taxRate = await this.retrieve(id)
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (typeof v !== "undefined") {
|
||||
taxRate[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return await taxRateRepo.save(taxRate)
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: string | string[]): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
const query = this.buildQuery_({ id })
|
||||
await taxRateRepo.delete(query.where)
|
||||
})
|
||||
}
|
||||
|
||||
async removeFromProduct(
|
||||
id: string,
|
||||
productIds: string | string[]
|
||||
): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
|
||||
let ids: string[]
|
||||
if (typeof productIds === "string") {
|
||||
ids = [productIds]
|
||||
} else {
|
||||
ids = productIds
|
||||
}
|
||||
|
||||
await taxRateRepo.removeFromProduct(id, ids)
|
||||
})
|
||||
}
|
||||
|
||||
async removeFromProductType(
|
||||
id: string,
|
||||
typeIds: string | string[]
|
||||
): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
|
||||
let ids: string[]
|
||||
if (typeof typeIds === "string") {
|
||||
ids = [typeIds]
|
||||
} else {
|
||||
ids = typeIds
|
||||
}
|
||||
|
||||
await taxRateRepo.removeFromProductType(id, ids)
|
||||
})
|
||||
}
|
||||
|
||||
async removeFromShippingOption(
|
||||
id: string,
|
||||
optionIds: string | string[]
|
||||
): Promise<void> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
|
||||
let ids: string[]
|
||||
if (typeof optionIds === "string") {
|
||||
ids = [optionIds]
|
||||
} else {
|
||||
ids = optionIds
|
||||
}
|
||||
|
||||
await taxRateRepo.removeFromShippingOption(id, ids)
|
||||
})
|
||||
}
|
||||
|
||||
async addToProduct(
|
||||
id: string,
|
||||
productIds: string | string[],
|
||||
replace = false
|
||||
): Promise<ProductTaxRate | ProductTaxRate[]> {
|
||||
let ids: string[]
|
||||
if (typeof productIds === "string") {
|
||||
ids = [productIds]
|
||||
} else {
|
||||
ids = productIds
|
||||
}
|
||||
|
||||
const result = await this.atomicPhase_(
|
||||
async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
return await taxRateRepo.addToProduct(id, ids, replace)
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (err: any) => {
|
||||
if (err.code === "23503") {
|
||||
// A foreign key constraint failed meaning some thing doesn't exist
|
||||
// either it is a product or the tax rate itself. Using Promise.all
|
||||
// will try to retrieve all of the resources and will fail when
|
||||
// something is not found.
|
||||
await Promise.all([
|
||||
this.retrieve(id, { select: ["id"] }),
|
||||
...ids.map((pId) =>
|
||||
this.productService_.retrieve(pId, { select: ["id"] })
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
async addToProductType(
|
||||
id: string,
|
||||
productTypeIds: string | string[],
|
||||
replace = false
|
||||
): Promise<ProductTypeTaxRate[]> {
|
||||
let ids: string[]
|
||||
if (typeof productTypeIds === "string") {
|
||||
ids = [productTypeIds]
|
||||
} else {
|
||||
ids = productTypeIds
|
||||
}
|
||||
|
||||
return await this.atomicPhase_(
|
||||
async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
return await taxRateRepo.addToProductType(id, ids, replace)
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (err: any) => {
|
||||
if (err.code === "23503") {
|
||||
// A foreign key constraint failed meaning some thing doesn't exist
|
||||
// either it is a product or the tax rate itself. Using Promise.all
|
||||
// will try to retrieve all of the resources and will fail when
|
||||
// something is not found.
|
||||
await Promise.all([
|
||||
this.retrieve(id, {
|
||||
select: ["id"],
|
||||
}) as Promise<unknown>,
|
||||
...ids.map(
|
||||
(pId) =>
|
||||
this.productTypeService_.retrieve(pId, {
|
||||
select: ["id"],
|
||||
}) as Promise<unknown>
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async addToShippingOption(
|
||||
id: string,
|
||||
optionIds: string | string[],
|
||||
replace = false
|
||||
): Promise<ShippingTaxRate> {
|
||||
let ids: string[]
|
||||
if (typeof optionIds === "string") {
|
||||
ids = [optionIds]
|
||||
} else {
|
||||
ids = optionIds
|
||||
}
|
||||
|
||||
return await this.atomicPhase_(
|
||||
async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
const taxRate = await this.retrieve(id, { select: ["id", "region_id"] })
|
||||
const options = await this.shippingOptionService_.list(
|
||||
{ id: ids },
|
||||
{ select: ["id", "region_id"] }
|
||||
)
|
||||
for (const o of options) {
|
||||
if (o.region_id !== taxRate.region_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Shipping Option and Tax Rate must belong to the same Region to be associated. Shipping Option with id: ${o.id} belongs to Region with id: ${o.region_id} and Tax Rate with id: ${taxRate.id} belongs to Region with id: ${taxRate.region_id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
return await taxRateRepo.addToShippingOption(id, ids, replace)
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async (err: any) => {
|
||||
if (err.code === "23503") {
|
||||
// A foreign key constraint failed meaning some thing doesn't exist
|
||||
// either it is a product or the tax rate itself. Using Promise.all
|
||||
// will try to retrieve all of the resources and will fail when
|
||||
// something is not found.
|
||||
await Promise.all([
|
||||
this.retrieve(id, { select: ["id"] }),
|
||||
...ids.map((sId) =>
|
||||
this.shippingOptionService_.retrieve(sId, { select: ["id"] })
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async listByProduct(
|
||||
productId: string,
|
||||
config: TaxRateListByConfig
|
||||
): Promise<TaxRate[]> {
|
||||
// Check both ProductTaxRate + ProductTypeTaxRate
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
return await taxRateRepo.listByProduct(productId, config)
|
||||
})
|
||||
}
|
||||
|
||||
async listByShippingOption(
|
||||
shippingOptionId: string,
|
||||
config: TaxRateListByConfig
|
||||
): Promise<TaxRate[]> {
|
||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||
const taxRateRepo = manager.getCustomRepository(this.taxRateRepository_)
|
||||
return await taxRateRepo.listByShippingOption(shippingOptionId, config)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default TaxRateService
|
||||
@@ -1,389 +0,0 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* A service that calculates total and subtotals for orders, carts etc..
|
||||
* @implements {BaseService}
|
||||
*/
|
||||
class TotalsService extends BaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param {object} object - object to calculate total for
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
getTotal(object) {
|
||||
const subtotal = this.getSubtotal(object)
|
||||
const taxTotal = this.getTaxTotal(object)
|
||||
const discountTotal = this.getDiscountTotal(object)
|
||||
const giftCardTotal = this.getGiftCardTotal(object)
|
||||
const shippingTotal = this.getShippingTotal(object)
|
||||
|
||||
return subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal
|
||||
}
|
||||
|
||||
getPaidTotal(order) {
|
||||
const total = order.payments?.reduce((acc, next) => {
|
||||
acc += next.amount
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
getSwapTotal(order) {
|
||||
let swapTotal = 0
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const s of order.swaps) {
|
||||
swapTotal = swapTotal + s.difference_due
|
||||
}
|
||||
}
|
||||
|
||||
return swapTotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param {(Cart|Order)} object - cart or order to calculate subtotal for
|
||||
* @param {Object} opts - options
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
getSubtotal(object, opts = {}) {
|
||||
let subtotal = 0
|
||||
if (!object.items) {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
object.items.map((item) => {
|
||||
if (opts.excludeNonDiscounts) {
|
||||
if (item.allow_discounts) {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
}
|
||||
} else {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
}
|
||||
})
|
||||
|
||||
return this.rounded(subtotal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates shipping total
|
||||
* @param {Cart | Object} object - cart or order to calculate subtotal for
|
||||
* @return {int} shipping total
|
||||
*/
|
||||
getShippingTotal(object) {
|
||||
const { shipping_methods } = object
|
||||
return shipping_methods.reduce((acc, next) => {
|
||||
return acc + next.price
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates tax total
|
||||
* Currently based on the Danish tax system
|
||||
* @param {Cart | Object} object - cart or order to calculate subtotal for
|
||||
* @return {int} tax total
|
||||
*/
|
||||
getTaxTotal(object) {
|
||||
const subtotal = this.getSubtotal(object)
|
||||
const shippingTotal = this.getShippingTotal(object)
|
||||
const discountTotal = this.getDiscountTotal(object)
|
||||
const giftCardTotal = this.getGiftCardTotal(object)
|
||||
const tax_rate =
|
||||
typeof object.tax_rate !== "undefined"
|
||||
? object.tax_rate
|
||||
: object.region.tax_rate
|
||||
return this.rounded(
|
||||
(subtotal - discountTotal - giftCardTotal + shippingTotal) *
|
||||
(tax_rate / 100)
|
||||
)
|
||||
}
|
||||
|
||||
getRefundedTotal(object) {
|
||||
if (!object.refunds) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const total = object.refunds.reduce((acc, next) => acc + next.amount, 0)
|
||||
return this.rounded(total)
|
||||
}
|
||||
|
||||
getLineItemRefund(object, lineItem) {
|
||||
const { discounts } = object
|
||||
const tax_rate =
|
||||
typeof object.tax_rate !== "undefined"
|
||||
? object.tax_rate
|
||||
: object.region.tax_rate
|
||||
const taxRate = (tax_rate || 0) / 100
|
||||
|
||||
const discount = discounts.find(({ rule }) => rule.type !== "free_shipping")
|
||||
|
||||
if (!discount || !lineItem.allow_discounts) {
|
||||
return lineItem.unit_price * lineItem.quantity * (1 + taxRate)
|
||||
}
|
||||
|
||||
const lineDiscounts = this.getLineDiscounts(object, discount)
|
||||
const discountedLine = lineDiscounts.find(
|
||||
(line) => line.item.id === lineItem.id
|
||||
)
|
||||
|
||||
const discountAmount =
|
||||
(discountedLine.amount / discountedLine.item.quantity) * lineItem.quantity
|
||||
|
||||
return this.rounded(
|
||||
(lineItem.unit_price * lineItem.quantity - discountAmount) * (1 + taxRate)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates refund total of line items.
|
||||
* If any of the items to return have been discounted, we need to
|
||||
* apply the discount again before refunding them.
|
||||
* @param {Order} order - cart or order to calculate subtotal for
|
||||
* @param {[LineItem]} lineItems -
|
||||
* @return {int} the calculated subtotal
|
||||
*/
|
||||
getRefundTotal(order, lineItems) {
|
||||
let itemIds = order.items.map((i) => i.id)
|
||||
|
||||
// in case we swap a swap, we need to include swap items
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const s of order.swaps) {
|
||||
const swapItemIds = s.additional_items.map((el) => el.id)
|
||||
itemIds = [...itemIds, ...swapItemIds]
|
||||
}
|
||||
}
|
||||
|
||||
if (order.claims && order.claims.length) {
|
||||
for (const c of order.claims) {
|
||||
const claimItemIds = c.additional_items.map((el) => el.id)
|
||||
itemIds = [...itemIds, ...claimItemIds]
|
||||
}
|
||||
}
|
||||
|
||||
const refunds = lineItems.map((i) => {
|
||||
if (!itemIds.includes(i.id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Line item does not exist on order"
|
||||
)
|
||||
}
|
||||
|
||||
return this.getLineItemRefund(order, i)
|
||||
})
|
||||
|
||||
return this.rounded(refunds.reduce((acc, next) => acc + next, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates either fixed or percentage discount of a variant
|
||||
* @param {string} lineItem - id of line item
|
||||
* @param {string} variant - id of variant in line item
|
||||
* @param {int} variantPrice - price of the variant based on region
|
||||
* @param {int} value - discount value
|
||||
* @param {string} discountType - the type of discount (fixed or percentage)
|
||||
* @return {{ string, string, int }} triples of lineitem, variant and
|
||||
* applied discount
|
||||
*/
|
||||
calculateDiscount_(lineItem, variant, variantPrice, value, discountType) {
|
||||
if (!lineItem.allow_discounts) {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: 0,
|
||||
}
|
||||
}
|
||||
if (discountType === "percentage") {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: ((variantPrice * lineItem.quantity) / 100) * value,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount:
|
||||
value >= variantPrice * lineItem.quantity
|
||||
? variantPrice * lineItem.quantity
|
||||
: value * lineItem.quantity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the rule of a discount has allocation="item", then we need
|
||||
* to calculate discount on each item in the cart. Furthermore, we need to
|
||||
* make sure to only apply the discount on valid variants. And finally we
|
||||
* return ether an array of percentages discounts or fixed discounts
|
||||
* alongside the variant on which the discount was applied.
|
||||
* @param {Discount} discount - the discount to which we do the calculation
|
||||
* @param {Cart} cart - the cart to calculate discounts for
|
||||
* @return {[{ string, string, int }]} array of triples of lineitem, variant
|
||||
* and applied discount
|
||||
*/
|
||||
getAllocationItemDiscounts(discount, cart) {
|
||||
const discounts = []
|
||||
for (const item of cart.items) {
|
||||
if (discount.rule.valid_for?.length > 0) {
|
||||
discount.rule.valid_for.map(({ id }) => {
|
||||
if (item.variant.product_id === id) {
|
||||
discounts.push(
|
||||
this.calculateDiscount_(
|
||||
item,
|
||||
item.variant.id,
|
||||
item.unit_price,
|
||||
discount.rule.value,
|
||||
discount.rule.type
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return discounts
|
||||
}
|
||||
|
||||
getLineDiscounts(cart, discount) {
|
||||
const subtotal = this.getSubtotal(cart, { excludeNonDiscounts: true })
|
||||
|
||||
let merged = [...cart.items]
|
||||
|
||||
// merge items from order with items from order swaps
|
||||
if (cart.swaps && cart.swaps.length) {
|
||||
for (const s of cart.swaps) {
|
||||
merged = [...merged, ...s.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
if (cart.claims && cart.claims.length) {
|
||||
for (const c of cart.claims) {
|
||||
merged = [...merged, ...c.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
const { type, allocation, value } = discount.rule
|
||||
if (allocation === "total") {
|
||||
let percentage = 0
|
||||
if (type === "percentage") {
|
||||
percentage = value / 100
|
||||
} else if (type === "fixed") {
|
||||
// If the fixed discount exceeds the subtotal we should
|
||||
// calculate a 100% discount
|
||||
const nominator = Math.min(value, subtotal)
|
||||
percentage = nominator / subtotal
|
||||
}
|
||||
|
||||
return merged.map((item) => {
|
||||
const lineTotal = item.unit_price * item.quantity
|
||||
|
||||
return {
|
||||
item,
|
||||
amount: lineTotal * percentage,
|
||||
}
|
||||
})
|
||||
} else if (allocation === "item") {
|
||||
const allocationDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cart,
|
||||
type
|
||||
)
|
||||
return merged.map((item) => {
|
||||
const discounted = allocationDiscounts.find(
|
||||
(a) => a.lineItem.id === item.id
|
||||
)
|
||||
return {
|
||||
item,
|
||||
amount: discounted ? discounted.amount : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return merged.map((i) => ({ item: i, amount: 0 }))
|
||||
}
|
||||
|
||||
getGiftCardTotal(cart) {
|
||||
const giftCardable = this.getSubtotal(cart) - this.getDiscountTotal(cart)
|
||||
|
||||
if (cart.gift_card_transactions) {
|
||||
return cart.gift_card_transactions.reduce(
|
||||
(acc, next) => acc + next.amount,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
if (!cart.gift_cards || !cart.gift_cards.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const toReturn = cart.gift_cards.reduce(
|
||||
(acc, next) => acc + next.balance,
|
||||
0
|
||||
)
|
||||
return Math.min(giftCardable, toReturn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total discount amount for each of the different supported
|
||||
* discount types. If discounts aren't present or invalid returns 0.
|
||||
* @param {Cart} cart - the cart to calculate discounts for
|
||||
* @return {int} the total discounts amount
|
||||
*/
|
||||
getDiscountTotal(cart) {
|
||||
const subtotal = this.getSubtotal(cart, { excludeNonDiscounts: true })
|
||||
|
||||
if (!cart.discounts || !cart.discounts.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// we only support having free shipping and one other discount, so first
|
||||
// find the discount, which is not free shipping.
|
||||
const discount = cart.discounts.find(
|
||||
({ rule }) => rule.type !== "free_shipping"
|
||||
)
|
||||
|
||||
if (!discount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const { type, allocation, value } = discount.rule
|
||||
let toReturn = 0
|
||||
|
||||
if (type === "percentage" && allocation === "total") {
|
||||
toReturn = (subtotal / 100) * value
|
||||
} else if (type === "percentage" && allocation === "item") {
|
||||
const itemPercentageDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cart,
|
||||
"percentage"
|
||||
)
|
||||
toReturn = _.sumBy(itemPercentageDiscounts, (d) => d.amount)
|
||||
} else if (type === "fixed" && allocation === "total") {
|
||||
toReturn = value
|
||||
} else if (type === "fixed" && allocation === "item") {
|
||||
const itemFixedDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cart,
|
||||
"fixed"
|
||||
)
|
||||
toReturn = _.sumBy(itemFixedDiscounts, (d) => d.amount)
|
||||
}
|
||||
|
||||
if (subtotal < 0) {
|
||||
return this.rounded(Math.max(subtotal, toReturn))
|
||||
}
|
||||
|
||||
return this.rounded(Math.min(subtotal, toReturn))
|
||||
}
|
||||
|
||||
rounded(value) {
|
||||
return Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
export default TotalsService
|
||||
@@ -0,0 +1,957 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
import { LineItemTaxLine } from "../models/line-item-tax-line"
|
||||
import { ShippingMethodTaxLine } from "../models/shipping-method-tax-line"
|
||||
import { Order } from "../models/order"
|
||||
import { Cart } from "../models/cart"
|
||||
import { ShippingMethod } from "../models/shipping-method"
|
||||
import { LineItem } from "../models/line-item"
|
||||
import { Discount } from "../models/discount"
|
||||
import { DiscountRuleType } from "../models/discount-rule"
|
||||
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import { ITaxCalculationStrategy } from "../interfaces/tax-calculation-strategy"
|
||||
import { TaxCalculationContext } from "../interfaces/tax-service"
|
||||
import { isCart } from "../types/cart"
|
||||
import { isOrder } from "../types/orders"
|
||||
|
||||
import {
|
||||
SubtotalOptions,
|
||||
LineDiscount,
|
||||
LineAllocationsMap,
|
||||
LineDiscountAmount,
|
||||
} from "../types/totals"
|
||||
|
||||
type ShippingMethodTotals = {
|
||||
price: number
|
||||
tax_total: number
|
||||
total: number
|
||||
original_total: number
|
||||
original_tax_total: number
|
||||
tax_lines: ShippingMethodTaxLine[]
|
||||
}
|
||||
|
||||
type GetShippingMethodTotalsOptions = {
|
||||
include_tax?: boolean
|
||||
use_tax_lines?: boolean
|
||||
}
|
||||
|
||||
type LineItemTotals = {
|
||||
unit_price: number
|
||||
quantity: number
|
||||
subtotal: number
|
||||
tax_total: number
|
||||
total: number
|
||||
original_total: number
|
||||
original_tax_total: number
|
||||
tax_lines: LineItemTaxLine[]
|
||||
discount_total: number
|
||||
gift_card_total: number
|
||||
}
|
||||
|
||||
type LineItemTotalsOptions = {
|
||||
include_tax?: boolean
|
||||
use_tax_lines?: boolean
|
||||
}
|
||||
|
||||
type GetLineItemTotalOptions = {
|
||||
include_tax?: boolean
|
||||
exclude_gift_cards?: boolean
|
||||
exclude_discounts?: boolean
|
||||
}
|
||||
|
||||
type TotalsServiceProps = {
|
||||
taxProviderService: TaxProviderService
|
||||
taxCalculationStrategy: ITaxCalculationStrategy
|
||||
}
|
||||
|
||||
type GetTotalsOptions = {
|
||||
force_taxes?: boolean
|
||||
}
|
||||
|
||||
type AllocationMapOptions = {
|
||||
exclude_gift_cards?: boolean
|
||||
exclude_discounts?: boolean
|
||||
}
|
||||
|
||||
type CalculationContextOptions = {
|
||||
is_return?: boolean
|
||||
exclude_shipping?: boolean
|
||||
exclude_gift_cards?: boolean
|
||||
exclude_discounts?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A service that calculates total and subtotals for orders, carts etc..
|
||||
* @implements {BaseService}
|
||||
*/
|
||||
class TotalsService extends BaseService {
|
||||
private taxProviderService_: TaxProviderService
|
||||
private taxCalculationStrategy_: ITaxCalculationStrategy
|
||||
|
||||
constructor({
|
||||
taxProviderService,
|
||||
taxCalculationStrategy,
|
||||
}: TotalsServiceProps) {
|
||||
super()
|
||||
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.taxCalculationStrategy_ = taxCalculationStrategy
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param cartOrOrder - object to calculate total for
|
||||
* @param options - options to calculate by
|
||||
* @return the calculated subtotal
|
||||
*/
|
||||
async getTotal(
|
||||
cartOrOrder: Cart | Order,
|
||||
options: GetTotalsOptions = {}
|
||||
): Promise<number> {
|
||||
const subtotal = this.getSubtotal(cartOrOrder)
|
||||
const taxTotal =
|
||||
(await this.getTaxTotal(cartOrOrder, options.force_taxes)) || 0
|
||||
const discountTotal = this.getDiscountTotal(cartOrOrder)
|
||||
const giftCardTotal = this.getGiftCardTotal(cartOrOrder)
|
||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
||||
|
||||
return subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total payments made on an order
|
||||
* @param order - the order to calculate paid amount for
|
||||
* @return the total paid amount
|
||||
*/
|
||||
getPaidTotal(order: Order): number {
|
||||
const total = order.payments?.reduce((acc, next) => {
|
||||
acc += next.amount
|
||||
return acc
|
||||
}, 0)
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
/**
|
||||
* The total paid for swaps. May be negative in case of negative swap
|
||||
* difference.
|
||||
* @param order - the order to calculate swap total for
|
||||
* @return the swap total
|
||||
*/
|
||||
getSwapTotal(order: Order): number {
|
||||
let swapTotal = 0
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const s of order.swaps) {
|
||||
swapTotal = swapTotal + s.difference_due
|
||||
}
|
||||
}
|
||||
|
||||
return swapTotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the totals breakdown for a shipping method. Fetches tax lines if not
|
||||
* already provided.
|
||||
* @param shippingMethod - the shipping method to get totals breakdown for.
|
||||
* @param cartOrOrder - the cart or order to use as context for the breakdown
|
||||
* @param opts - options for what should be included
|
||||
* @returns An object that breaks down the totals for the shipping method
|
||||
*/
|
||||
async getShippingMethodTotals(
|
||||
shippingMethod: ShippingMethod,
|
||||
cartOrOrder: Cart | Order,
|
||||
opts: GetShippingMethodTotalsOptions = {}
|
||||
): Promise<ShippingMethodTotals> {
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
||||
exclude_shipping: true,
|
||||
})
|
||||
calculationContext.shipping_methods = [shippingMethod]
|
||||
|
||||
const totals = {
|
||||
price: shippingMethod.price,
|
||||
original_total: shippingMethod.price,
|
||||
total: shippingMethod.price,
|
||||
original_tax_total: 0,
|
||||
tax_total: 0,
|
||||
tax_lines: shippingMethod.tax_lines || [],
|
||||
}
|
||||
|
||||
if (opts.include_tax) {
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate !== null) {
|
||||
totals.original_tax_total = Math.round(
|
||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
||||
)
|
||||
totals.tax_total = Math.round(
|
||||
totals.original_tax_total * (cartOrOrder.tax_rate / 100)
|
||||
)
|
||||
} else {
|
||||
let taxLines: ShippingMethodTaxLine[]
|
||||
if (opts.use_tax_lines || isOrder(cartOrOrder)) {
|
||||
if (typeof shippingMethod.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Lines must be joined on shipping method to calculate taxes"
|
||||
)
|
||||
}
|
||||
|
||||
taxLines = shippingMethod.tax_lines
|
||||
} else {
|
||||
const orderLines = await this.taxProviderService_.getTaxLines(
|
||||
cartOrOrder.items,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
console.log(orderLines)
|
||||
|
||||
taxLines = orderLines.filter((ol) => {
|
||||
if ("shipping_method_id" in ol) {
|
||||
return ol.shipping_method_id === shippingMethod.id
|
||||
}
|
||||
return false
|
||||
}) as ShippingMethodTaxLine[]
|
||||
}
|
||||
totals.tax_lines = taxLines
|
||||
}
|
||||
|
||||
if (totals.tax_lines.length > 0) {
|
||||
totals.original_tax_total =
|
||||
await this.taxCalculationStrategy_.calculate(
|
||||
[],
|
||||
totals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
totals.tax_total = totals.original_tax_total
|
||||
|
||||
totals.original_total += totals.original_tax_total
|
||||
totals.total += totals.tax_total
|
||||
}
|
||||
}
|
||||
|
||||
if (cartOrOrder.discounts) {
|
||||
if (cartOrOrder.discounts.some((d) => d.rule.type === "free_shipping")) {
|
||||
totals.total = 0
|
||||
totals.tax_total = 0
|
||||
}
|
||||
}
|
||||
|
||||
return totals
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates subtotal of a given cart or order.
|
||||
* @param cartOrOrder - cart or order to calculate subtotal for
|
||||
* @param opts - options
|
||||
* @return the calculated subtotal
|
||||
*/
|
||||
getSubtotal(cartOrOrder: Cart | Order, opts: SubtotalOptions = {}): number {
|
||||
let subtotal = 0
|
||||
if (!cartOrOrder.items) {
|
||||
return subtotal
|
||||
}
|
||||
|
||||
cartOrOrder.items.map((item) => {
|
||||
if (opts.excludeNonDiscounts) {
|
||||
if (item.allow_discounts) {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
}
|
||||
} else {
|
||||
subtotal += item.unit_price * item.quantity
|
||||
}
|
||||
})
|
||||
|
||||
return this.rounded(subtotal)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates shipping total
|
||||
* @param cartOrOrder - cart or order to calculate subtotal for
|
||||
* @return shipping total
|
||||
*/
|
||||
getShippingTotal(cartOrOrder: Cart | Order): number {
|
||||
const { shipping_methods } = cartOrOrder
|
||||
return shipping_methods.reduce((acc, next) => {
|
||||
return acc + next.price
|
||||
}, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates tax total
|
||||
* Currently based on the Danish tax system
|
||||
* @param cartOrOrder - cart or order to calculate tax total for
|
||||
* @param forceTaxes - whether taxes should be calculated regardless
|
||||
* of region settings
|
||||
* @return tax total
|
||||
*/
|
||||
async getTaxTotal(
|
||||
cartOrOrder: Cart | Order,
|
||||
forceTaxes = false
|
||||
): Promise<number | null> {
|
||||
if (
|
||||
isCart(cartOrOrder) &&
|
||||
!forceTaxes &&
|
||||
!cartOrOrder.region.automatic_taxes
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder)
|
||||
|
||||
let taxLines: (ShippingMethodTaxLine | LineItemTaxLine)[]
|
||||
if (isOrder(cartOrOrder)) {
|
||||
const taxLinesJoined = cartOrOrder.items.every(
|
||||
(i) => typeof i.tax_lines !== "undefined"
|
||||
)
|
||||
if (!taxLinesJoined) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Order tax calculations must have tax lines joined on line items"
|
||||
)
|
||||
}
|
||||
|
||||
if (cartOrOrder.tax_rate === null) {
|
||||
taxLines = cartOrOrder.items.flatMap((li) => li.tax_lines)
|
||||
|
||||
const shippingTaxLines = cartOrOrder.shipping_methods.flatMap(
|
||||
(sm) => sm.tax_lines
|
||||
)
|
||||
|
||||
taxLines = taxLines.concat(shippingTaxLines)
|
||||
} else {
|
||||
const subtotal = this.getSubtotal(cartOrOrder)
|
||||
const shippingTotal = this.getShippingTotal(cartOrOrder)
|
||||
const discountTotal = this.getDiscountTotal(cartOrOrder)
|
||||
const giftCardTotal = this.getGiftCardTotal(cartOrOrder)
|
||||
return this.rounded(
|
||||
(subtotal - discountTotal - giftCardTotal + shippingTotal) *
|
||||
(cartOrOrder.tax_rate / 100)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
taxLines = await this.taxProviderService_.getTaxLines(
|
||||
cartOrOrder.items,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
if (cartOrOrder.type === "swap") {
|
||||
const returnTaxLines = cartOrOrder.items.flatMap((i) => {
|
||||
if (i.is_return) {
|
||||
if (typeof i.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Return Line Items must join tax lines"
|
||||
)
|
||||
}
|
||||
return i.tax_lines
|
||||
}
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
taxLines = taxLines.concat(returnTaxLines)
|
||||
}
|
||||
}
|
||||
|
||||
const toReturn = await this.taxCalculationStrategy_.calculate(
|
||||
cartOrOrder.items,
|
||||
taxLines,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
return this.rounded(toReturn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a map of discounts and gift cards that apply to line items in an
|
||||
* order. The function calculates the amount of a discount or gift card that
|
||||
* applies to a specific line item.
|
||||
* @param orderOrCart - the order or cart to get an allocation map for
|
||||
* @param options - controls what should be included in allocation map
|
||||
* @return the allocation map for the line items in the cart or order.
|
||||
*/
|
||||
getAllocationMap(
|
||||
orderOrCart: Cart | Order,
|
||||
options: AllocationMapOptions = {}
|
||||
): LineAllocationsMap {
|
||||
const allocationMap: LineAllocationsMap = {}
|
||||
|
||||
if (!options.exclude_discounts) {
|
||||
let lineDiscounts: LineDiscountAmount[] = []
|
||||
|
||||
const discount = orderOrCart.discounts.find(
|
||||
({ rule }) => rule.type !== "free_shipping"
|
||||
)
|
||||
if (discount) {
|
||||
lineDiscounts = this.getLineDiscounts(orderOrCart, discount)
|
||||
}
|
||||
|
||||
for (const ld of lineDiscounts) {
|
||||
if (allocationMap[ld.item.id]) {
|
||||
allocationMap[ld.item.id].discount = {
|
||||
amount: ld.amount,
|
||||
unit_amount: ld.amount / ld.item.quantity,
|
||||
}
|
||||
} else {
|
||||
allocationMap[ld.item.id] = {
|
||||
discount: {
|
||||
amount: ld.amount,
|
||||
unit_amount: ld.amount / ld.item.quantity,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.exclude_gift_cards) {
|
||||
let lineGiftCards: LineDiscountAmount[] = []
|
||||
if (orderOrCart.gift_cards && orderOrCart.gift_cards.length) {
|
||||
const subtotal = this.getSubtotal(orderOrCart)
|
||||
const giftCardTotal = this.getGiftCardTotal(orderOrCart)
|
||||
|
||||
// If the fixed discount exceeds the subtotal we should
|
||||
// calculate a 100% discount
|
||||
const nominator = Math.min(giftCardTotal, subtotal)
|
||||
const percentage = nominator / subtotal
|
||||
|
||||
lineGiftCards = orderOrCart.items.map((l) => {
|
||||
return {
|
||||
item: l,
|
||||
amount: l.unit_price * l.quantity * percentage,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for (const lgc of lineGiftCards) {
|
||||
if (allocationMap[lgc.item.id]) {
|
||||
allocationMap[lgc.item.id].gift_card = {
|
||||
amount: lgc.amount,
|
||||
unit_amount: lgc.amount / lgc.item.quantity,
|
||||
}
|
||||
} else {
|
||||
allocationMap[lgc.item.id] = {
|
||||
discount: {
|
||||
amount: lgc.amount,
|
||||
unit_amount: lgc.amount / lgc.item.quantity,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allocationMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total refund amount for an order.
|
||||
* @param order - the order to get total refund amount for.
|
||||
* @return the total refunded amount for an order.
|
||||
*/
|
||||
getRefundedTotal(order: Order): number {
|
||||
if (!order.refunds) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const total = order.refunds.reduce((acc, next) => acc + next.amount, 0)
|
||||
return this.rounded(total)
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount that can be refunded for a given line item.
|
||||
* @param order - order to use as context for the calculation
|
||||
* @param lineItem - the line item to calculate the refund amount for.
|
||||
* @return the line item refund amount.
|
||||
*/
|
||||
getLineItemRefund(order: Order, lineItem: LineItem): number {
|
||||
const allocationMap = this.getAllocationMap(order)
|
||||
|
||||
const discountAmount =
|
||||
(allocationMap[lineItem.id]?.discount?.unit_amount || 0) *
|
||||
lineItem.quantity
|
||||
const lineSubtotal =
|
||||
lineItem.unit_price * lineItem.quantity - discountAmount
|
||||
|
||||
/*
|
||||
* Used for backcompat with old tax system
|
||||
*/
|
||||
if (order.tax_rate !== null) {
|
||||
const taxRate = order.tax_rate / 100
|
||||
return this.rounded(lineSubtotal * (1 + taxRate))
|
||||
}
|
||||
|
||||
/*
|
||||
* New tax system uses the tax lines registerd on the line items
|
||||
*/
|
||||
if (typeof lineItem.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax calculation did not receive tax_lines"
|
||||
)
|
||||
}
|
||||
|
||||
const taxTotal = lineItem.tax_lines.reduce((acc, next) => {
|
||||
const taxRate = next.rate / 100
|
||||
return acc + this.rounded(lineSubtotal * taxRate)
|
||||
}, 0)
|
||||
|
||||
return lineSubtotal + taxTotal
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates refund total of line items.
|
||||
* If any of the items to return have been discounted, we need to
|
||||
* apply the discount again before refunding them.
|
||||
* @param order - cart or order to calculate subtotal for
|
||||
* @param lineItems - the line items to calculate refund total for
|
||||
* @return the calculated subtotal
|
||||
*/
|
||||
getRefundTotal(order: Order, lineItems: LineItem[]): number {
|
||||
let itemIds = order.items.map((i) => i.id)
|
||||
|
||||
// in case we swap a swap, we need to include swap items
|
||||
if (order.swaps && order.swaps.length) {
|
||||
for (const s of order.swaps) {
|
||||
const swapItemIds = s.additional_items.map((el) => el.id)
|
||||
itemIds = [...itemIds, ...swapItemIds]
|
||||
}
|
||||
}
|
||||
|
||||
if (order.claims && order.claims.length) {
|
||||
for (const c of order.claims) {
|
||||
const claimItemIds = c.additional_items.map((el) => el.id)
|
||||
itemIds = [...itemIds, ...claimItemIds]
|
||||
}
|
||||
}
|
||||
|
||||
const refunds = lineItems.map((i) => {
|
||||
if (!itemIds.includes(i.id)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Line item does not exist on order"
|
||||
)
|
||||
}
|
||||
|
||||
return this.getLineItemRefund(order, i)
|
||||
})
|
||||
|
||||
return this.rounded(refunds.reduce((acc, next) => acc + next, 0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates either fixed or percentage discount of a variant
|
||||
* @param lineItem - id of line item
|
||||
* @param variant - id of variant in line item
|
||||
* @param variantPrice - price of the variant based on region
|
||||
* @param value - discount value
|
||||
* @param discountType - the type of discount (fixed or percentage)
|
||||
* @return triples of lineitem, variant and applied discount
|
||||
*/
|
||||
calculateDiscount_(
|
||||
lineItem: LineItem,
|
||||
variant: string,
|
||||
variantPrice: number,
|
||||
value: number,
|
||||
discountType: DiscountRuleType
|
||||
): LineDiscount {
|
||||
if (!lineItem.allow_discounts) {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: 0,
|
||||
}
|
||||
}
|
||||
if (discountType === DiscountRuleType.PERCENTAGE) {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount: ((variantPrice * lineItem.quantity) / 100) * value,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
lineItem,
|
||||
variant,
|
||||
amount:
|
||||
value >= variantPrice * lineItem.quantity
|
||||
? variantPrice * lineItem.quantity
|
||||
: value * lineItem.quantity,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the rule of a discount has allocation="item", then we need
|
||||
* to calculate discount on each item in the cart. Furthermore, we need to
|
||||
* make sure to only apply the discount on valid variants. And finally we
|
||||
* return ether an array of percentages discounts or fixed discounts
|
||||
* alongside the variant on which the discount was applied.
|
||||
* @param discount - the discount to which we do the calculation
|
||||
* @param cart - the cart to calculate discounts for
|
||||
* @return array of triples of lineitem, variant and applied discount
|
||||
*/
|
||||
getAllocationItemDiscounts(
|
||||
discount: Discount,
|
||||
cart: Cart | Order
|
||||
): LineDiscount[] {
|
||||
const discounts: LineDiscount[] = []
|
||||
for (const item of cart.items) {
|
||||
if (discount.rule.valid_for?.length > 0) {
|
||||
discount.rule.valid_for.map(({ id }) => {
|
||||
if (item.variant.product_id === id) {
|
||||
discounts.push(
|
||||
this.calculateDiscount_(
|
||||
item,
|
||||
item.variant.id,
|
||||
item.unit_price,
|
||||
discount.rule.value,
|
||||
discount.rule.type
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return discounts
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the discount amount allocated to the line items of an order.
|
||||
* @param cartOrOrder - the cart or order to get line discount allocations for
|
||||
* @param discount - the discount to use as context for the calculation
|
||||
* @return the allocations that the discount has on the items in the cart or
|
||||
* order
|
||||
*/
|
||||
getLineDiscounts(
|
||||
cartOrOrder: Cart | Order,
|
||||
discount: Discount
|
||||
): LineDiscountAmount[] {
|
||||
const subtotal = this.getSubtotal(cartOrOrder, {
|
||||
excludeNonDiscounts: true,
|
||||
})
|
||||
|
||||
let merged: LineItem[] = [...cartOrOrder.items]
|
||||
|
||||
// merge items from order with items from order swaps
|
||||
if ("swaps" in cartOrOrder && cartOrOrder.swaps.length) {
|
||||
for (const s of cartOrOrder.swaps) {
|
||||
merged = [...merged, ...s.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
if ("claims" in cartOrOrder && cartOrOrder.claims.length) {
|
||||
for (const c of cartOrOrder.claims) {
|
||||
merged = [...merged, ...c.additional_items]
|
||||
}
|
||||
}
|
||||
|
||||
const { type, allocation, value } = discount.rule
|
||||
if (allocation === "total") {
|
||||
let percentage = 0
|
||||
if (type === "percentage") {
|
||||
percentage = value / 100
|
||||
} else if (type === "fixed") {
|
||||
// If the fixed discount exceeds the subtotal we should
|
||||
// calculate a 100% discount
|
||||
const nominator = Math.min(value, subtotal)
|
||||
percentage = nominator / subtotal
|
||||
}
|
||||
|
||||
return merged.map((item) => {
|
||||
const lineTotal = item.unit_price * item.quantity
|
||||
|
||||
return {
|
||||
item,
|
||||
amount: item.allow_discounts ? lineTotal * percentage : 0,
|
||||
}
|
||||
})
|
||||
} else if (allocation === "item") {
|
||||
const allocationDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cartOrOrder
|
||||
)
|
||||
return merged.map((item) => {
|
||||
const discounted = allocationDiscounts.find(
|
||||
(a) => a.lineItem.id === item.id
|
||||
)
|
||||
return {
|
||||
item,
|
||||
amount: discounted ? discounted.amount : 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return merged.map((i) => ({ item: i, amount: 0 }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks down the totals related to a line item; these are the subtotal, the
|
||||
* amount of discount applied to the line item, the amount of a gift card
|
||||
* applied to a line item and the amount of tax applied to a line item.
|
||||
* @param lineItem - the line item to calculate totals for
|
||||
* @param cartOrOrder - the cart or order to use as context for the calculation
|
||||
* @param options - the options to evaluate the line item totals for
|
||||
* @returns the breakdown of the line item totals
|
||||
*/
|
||||
async getLineItemTotals(
|
||||
lineItem: LineItem,
|
||||
cartOrOrder: Cart | Order,
|
||||
options: LineItemTotalsOptions = {}
|
||||
): Promise<LineItemTotals> {
|
||||
const calculationContext = this.getCalculationContext(cartOrOrder, {
|
||||
exclude_shipping: true,
|
||||
})
|
||||
|
||||
const lineItemAllocation =
|
||||
calculationContext.allocation_map[lineItem.id] || {}
|
||||
|
||||
const subtotal = lineItem.unit_price * lineItem.quantity
|
||||
const gift_card_total = lineItemAllocation.gift_card?.amount || 0
|
||||
const discount_total =
|
||||
(lineItemAllocation.discount?.unit_amount || 0) * lineItem.quantity
|
||||
|
||||
const lineItemTotals: LineItemTotals = {
|
||||
unit_price: lineItem.unit_price,
|
||||
quantity: lineItem.quantity,
|
||||
subtotal,
|
||||
gift_card_total,
|
||||
discount_total,
|
||||
total: subtotal - discount_total,
|
||||
original_total: subtotal,
|
||||
original_tax_total: 0,
|
||||
tax_total: 0,
|
||||
tax_lines: lineItem.tax_lines || [],
|
||||
}
|
||||
|
||||
// Tax Information
|
||||
if (options.include_tax) {
|
||||
// When we have an order with a null'ed tax rate we know that it is an
|
||||
// order from the old tax system. The following is a backward compat
|
||||
// calculation.
|
||||
if (isOrder(cartOrOrder) && cartOrOrder.tax_rate !== null) {
|
||||
lineItemTotals.original_tax_total =
|
||||
subtotal * (cartOrOrder.tax_rate / 100)
|
||||
lineItemTotals.tax_total =
|
||||
(subtotal - discount_total) * (cartOrOrder.tax_rate / 100)
|
||||
|
||||
lineItemTotals.total += lineItemTotals.tax_total
|
||||
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||
} else {
|
||||
let taxLines: LineItemTaxLine[]
|
||||
|
||||
/*
|
||||
* Line Items on orders will already have tax lines. But for cart line
|
||||
* items we have to get the line items from the tax provider.
|
||||
*/
|
||||
if (options.use_tax_lines || isOrder(cartOrOrder)) {
|
||||
if (typeof lineItem.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Tax Lines must be joined on items to calculate taxes"
|
||||
)
|
||||
}
|
||||
|
||||
taxLines = lineItem.tax_lines
|
||||
} else {
|
||||
if (lineItem.is_return) {
|
||||
if (typeof lineItem.tax_lines === "undefined") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
"Return Line Items must join tax lines"
|
||||
)
|
||||
}
|
||||
taxLines = lineItem.tax_lines
|
||||
} else {
|
||||
const orderLines = await this.taxProviderService_.getTaxLines(
|
||||
cartOrOrder.items,
|
||||
calculationContext
|
||||
)
|
||||
|
||||
taxLines = orderLines.filter((ol) => {
|
||||
if ("item_id" in ol) {
|
||||
return ol.item_id === lineItem.id
|
||||
}
|
||||
return false
|
||||
}) as LineItemTaxLine[]
|
||||
}
|
||||
}
|
||||
|
||||
lineItemTotals.tax_lines = taxLines
|
||||
}
|
||||
}
|
||||
|
||||
if (lineItemTotals.tax_lines.length > 0) {
|
||||
lineItemTotals.tax_total = await this.taxCalculationStrategy_.calculate(
|
||||
[lineItem],
|
||||
lineItemTotals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
lineItemTotals.total += lineItemTotals.tax_total
|
||||
|
||||
calculationContext.allocation_map = {} // Don't account for discounts
|
||||
lineItemTotals.original_tax_total =
|
||||
await this.taxCalculationStrategy_.calculate(
|
||||
[lineItem],
|
||||
lineItemTotals.tax_lines,
|
||||
calculationContext
|
||||
)
|
||||
lineItemTotals.original_total += lineItemTotals.original_tax_total
|
||||
}
|
||||
|
||||
return lineItemTotals
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a total for a line item. The total can take gift cards, discounts and
|
||||
* taxes into account. This can be controlled through the options.
|
||||
* @param lineItem - the line item to calculate a total for
|
||||
* @param cartOrOrder - the cart or order to use as context for the calculation
|
||||
* @param options - the options to use for the calculation
|
||||
* @returns the line item total
|
||||
*/
|
||||
async getLineItemTotal(
|
||||
lineItem: LineItem,
|
||||
cartOrOrder: Cart | Order,
|
||||
options: GetLineItemTotalOptions = {}
|
||||
): Promise<number> {
|
||||
const lineItemTotals = await this.getLineItemTotals(lineItem, cartOrOrder, {
|
||||
include_tax: options.include_tax,
|
||||
})
|
||||
|
||||
let toReturn = lineItemTotals.subtotal
|
||||
if (!options.exclude_discounts) {
|
||||
toReturn += lineItemTotals.discount_total
|
||||
}
|
||||
|
||||
if (!options.exclude_gift_cards) {
|
||||
toReturn += lineItemTotals.gift_card_total
|
||||
}
|
||||
|
||||
if (options.include_tax) {
|
||||
toReturn += lineItemTotals.tax_total
|
||||
}
|
||||
|
||||
return toReturn
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the gift card amount on a cart or order.
|
||||
* @param cartOrOrder - the cart or order to get gift card amount for
|
||||
* @return the gift card amount applied to the cart or order
|
||||
*/
|
||||
getGiftCardTotal(cartOrOrder: Cart | Order): number {
|
||||
const giftCardable =
|
||||
this.getSubtotal(cartOrOrder) - this.getDiscountTotal(cartOrOrder)
|
||||
|
||||
if ("gift_card_transactions" in cartOrOrder) {
|
||||
return cartOrOrder.gift_card_transactions.reduce(
|
||||
(acc, next) => acc + next.amount,
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
if (!cartOrOrder.gift_cards || !cartOrOrder.gift_cards.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const toReturn = cartOrOrder.gift_cards.reduce(
|
||||
(acc, next) => acc + next.balance,
|
||||
0
|
||||
)
|
||||
return Math.min(giftCardable, toReturn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total discount amount for each of the different supported
|
||||
* discount types. If discounts aren't present or invalid returns 0.
|
||||
* @param cartOrOrder - the cart or order to calculate discounts for
|
||||
* @return the total discounts amount
|
||||
*/
|
||||
getDiscountTotal(cartOrOrder: Cart | Order): number {
|
||||
const subtotal = this.getSubtotal(cartOrOrder, {
|
||||
excludeNonDiscounts: true,
|
||||
})
|
||||
|
||||
if (!cartOrOrder.discounts || !cartOrOrder.discounts.length) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// we only support having free shipping and one other discount, so first
|
||||
// find the discount, which is not free shipping.
|
||||
const discount = cartOrOrder.discounts.find(
|
||||
({ rule }) => rule.type !== "free_shipping"
|
||||
)
|
||||
|
||||
if (!discount) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const { type, allocation, value } = discount.rule
|
||||
let toReturn = 0
|
||||
|
||||
if (type === "percentage" && allocation === "total") {
|
||||
toReturn = (subtotal / 100) * value
|
||||
} else if (type === "percentage" && allocation === "item") {
|
||||
const itemPercentageDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cartOrOrder
|
||||
)
|
||||
toReturn = _.sumBy(itemPercentageDiscounts, (d) => d.amount)
|
||||
} else if (type === "fixed" && allocation === "total") {
|
||||
toReturn = value
|
||||
} else if (type === "fixed" && allocation === "item") {
|
||||
const itemFixedDiscounts = this.getAllocationItemDiscounts(
|
||||
discount,
|
||||
cartOrOrder
|
||||
)
|
||||
toReturn = _.sumBy(itemFixedDiscounts, (d) => d.amount)
|
||||
}
|
||||
|
||||
if (subtotal < 0) {
|
||||
return this.rounded(Math.max(subtotal, toReturn))
|
||||
}
|
||||
|
||||
return this.rounded(Math.min(subtotal, toReturn))
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the calculation context for a tax total calculation.
|
||||
* @param cartOrOrder - the cart or order to get the calculation context for
|
||||
* @param options - options to gather context by
|
||||
* @return the tax calculation context
|
||||
*/
|
||||
getCalculationContext(
|
||||
cartOrOrder: Cart | Order,
|
||||
options: CalculationContextOptions = {}
|
||||
): TaxCalculationContext {
|
||||
const allocationMap = this.getAllocationMap(cartOrOrder, {
|
||||
exclude_gift_cards: options.exclude_gift_cards,
|
||||
exclude_discounts: options.exclude_discounts,
|
||||
})
|
||||
|
||||
let shippingMethods: ShippingMethod[] = []
|
||||
// Default to include shipping methods
|
||||
if (!options.exclude_shipping) {
|
||||
shippingMethods = cartOrOrder.shipping_methods || []
|
||||
}
|
||||
|
||||
return {
|
||||
shipping_address: cartOrOrder.shipping_address,
|
||||
shipping_methods: shippingMethods,
|
||||
customer: cartOrOrder.customer,
|
||||
region: cartOrOrder.region,
|
||||
is_return: options.is_return ?? false,
|
||||
allocation_map: allocationMap,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds a number using Math.round.
|
||||
* @param value - the value to round
|
||||
* @return the rounded value
|
||||
*/
|
||||
rounded(value: number): number {
|
||||
return Math.round(value)
|
||||
}
|
||||
}
|
||||
|
||||
export default TotalsService
|
||||
Loaded 100 of 223 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user