fix(medusa): Rounding issues on line item adjustments (#3446)

* chores(medusa): Attempt to fix discount rounding issues

* add migration

* update entities

* apply multipler factor properly

* fix discount service

* WIP

* fix rounding issues in discounts

* fix some tests

* Exclude raw_discount_total from responses

* fix adjustments

* cleanup response

* fix

* fix draft order integration

* fix order integration

* fix order integration

* address feedback

* fix test

* Create .changeset/polite-llamas-sit.md

* remove comment

---------

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2023-03-29 11:04:41 +02:00
committed by GitHub
co-authored by Oliver Windall Juhl
parent 577098a4db
commit d886430bf8
86 changed files with 720 additions and 175 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): Rounding issues on line item adjustments
@@ -572,11 +572,10 @@ describe("/admin/draft-orders", () => {
(item) => item.variant_id === testVariant2Id
)
const lineItem1WeightInTotal = 0.444 // line item amount / amount
const lineItem2WeightInTotal = 0.556 // line item amount / amount
expect(draftOrder.cart.items).toHaveLength(2)
expect(lineItem1.adjustments).toHaveLength(1)
expect(lineItem1.adjustments[0].amount).toBeCloseTo(444, 0) // discountAmount * (line item amount / amount) = 444.4444444444
expect(lineItem1).toEqual(
expect.objectContaining({
variant_id: testVariantId,
@@ -585,7 +584,6 @@ describe("/admin/draft-orders", () => {
adjustments: expect.arrayContaining([
expect.objectContaining({
item_id: lineItem1.id,
amount: discountAmount * lineItem1WeightInTotal,
description: "discount",
discount_id: discount.id,
}),
@@ -593,6 +591,8 @@ describe("/admin/draft-orders", () => {
})
)
expect(lineItem2.adjustments).toHaveLength(1)
expect(lineItem2.adjustments[0].amount).toBeCloseTo(556, 0) // discountAmount * (line item amount / amount) = 555.5555555555
expect(lineItem2).toEqual(
expect.objectContaining({
variant_id: testVariant2Id,
@@ -601,7 +601,6 @@ describe("/admin/draft-orders", () => {
adjustments: expect.arrayContaining([
expect.objectContaining({
item_id: lineItem2.id,
amount: discountAmount * lineItem2WeightInTotal,
description: "discount",
discount_id: discount.id,
}),
@@ -2482,6 +2482,9 @@ describe("/admin/order-edits", () => {
)
expect(item2.adjustments).toHaveLength(1)
expect(item1.adjustments[0].amount).toBeCloseTo(1333, 0)
expect(item2.adjustments[0].amount).toBeCloseTo(667, 0)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
@@ -2531,7 +2534,6 @@ describe("/admin/order-edits", () => {
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 1333,
}),
]),
}),
@@ -2564,7 +2566,6 @@ describe("/admin/order-edits", () => {
adjustments: expect.arrayContaining([
expect.objectContaining({
discount_id: discount.id,
amount: 667,
}),
]),
}),
@@ -604,13 +604,22 @@ describe("/store/carts", () => {
.catch((err) => console.log(err))
expect(response.data.cart.items.length).toEqual(2)
const item1 = response.data.cart.items.find((i) => i.id === "test-li")
expect(item1.adjustments).toHaveLength(1)
const item2 = response.data.cart.items.find((i) => i.id !== "test-li")
expect(item2.adjustments).toHaveLength(1)
expect(item1.adjustments[0].amount).toBeCloseTo(17, 0)
expect(item2.adjustments[0].amount).toBeCloseTo(168, 0)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "test-li",
amount: 17,
discount_id: "medusa-185",
}),
],
@@ -618,7 +627,6 @@ describe("/store/carts", () => {
expect.objectContaining({
adjustments: [
expect.objectContaining({
amount: 168,
discount_id: "medusa-185",
}),
],
@@ -663,14 +671,23 @@ describe("/store/carts", () => {
.catch((err) => console.log(err))
expect(response.data.cart.items.length).toEqual(2)
const item1 = response.data.cart.items.find((i) => i.id === "test-li")
expect(item1.adjustments).toHaveLength(1)
const item2 = response.data.cart.items.find(
(i) => i.id === "line-item-2"
)
expect(item2.adjustments).toHaveLength(1)
expect(response.data.cart.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
adjustments: [
expect.objectContaining({
item_id: "test-li",
amount: 9.25,
discount_id: "medusa-185",
amount: 9,
}),
],
}),
@@ -678,7 +695,7 @@ describe("/store/carts", () => {
adjustments: [
expect.objectContaining({
item_id: "line-item-2",
amount: 176,
amount: 175.75,
discount_id: "medusa-185",
}),
],
@@ -166,6 +166,7 @@ describe("/store/carts", () => {
// fields
"status",
"email",
// relations
"shipping_address",
"fulfillments",
@@ -175,6 +176,18 @@ describe("/store/carts", () => {
"customer",
"payments",
"region",
// totals
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})
@@ -186,6 +199,7 @@ describe("/store/carts", () => {
expect(Object.keys(response.data.order)).toEqual([
// fields
"status",
// default relations
"shipping_address",
"fulfillments",
@@ -195,6 +209,18 @@ describe("/store/carts", () => {
"customer",
"payments",
"region",
// totals
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})
@@ -208,8 +234,21 @@ describe("/store/carts", () => {
expect(Object.keys(response.data.order)).toEqual([
// fields
"status",
// selected relations
"billing_address",
// totals
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
])
})
@@ -340,6 +340,7 @@ Object {
"original_tax_total": 200,
"original_total": 1200,
"quantity": 1,
"raw_discount_total": 0,
"subtotal": 1000,
"tax_lines": Array [
Object {
@@ -454,6 +455,7 @@ Object {
"original_tax_total": 400,
"original_total": 2400,
"quantity": 2,
"raw_discount_total": 0,
"returned_quantity": 1,
"shipped_quantity": 2,
"should_merge": true,
@@ -804,6 +806,7 @@ Object {
"original_total": 2400,
"price": "10.00 USD",
"quantity": 2,
"raw_discount_total": 0,
"returned_quantity": null,
"shipped_quantity": null,
"should_merge": true,
@@ -1037,6 +1040,7 @@ Object {
"original_total": 2400,
"price": "12.00 USD",
"quantity": 2,
"raw_discount_total": 0,
"returned_quantity": null,
"shipped_quantity": 2,
"should_merge": true,
@@ -1063,6 +1067,7 @@ Object {
"original_tax_total": 400,
"original_total": 2400,
"quantity": 2,
"raw_discount_total": 0,
"subtotal": 2000,
"tax_lines": Array [
Object {
@@ -1313,6 +1318,7 @@ Object {
"original_tax_total": 400,
"original_total": 2400,
"quantity": 2,
"raw_discount_total": 0,
"returned_quantity": null,
"shipped_quantity": 2,
"should_merge": true,
@@ -1531,6 +1537,7 @@ Object {
"original_tax_total": 200,
"original_total": 1200,
"quantity": 1,
"raw_discount_total": 0,
"subtotal": 1000,
"tax_lines": Array [
Object {
@@ -1645,6 +1652,7 @@ Object {
"original_tax_total": 400,
"original_total": 2400,
"quantity": 2,
"raw_discount_total": 0,
"returned_quantity": null,
"shipped_quantity": 2,
"should_merge": true,
+1 -1
View File
@@ -9,7 +9,7 @@ export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job"
export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount"
export { transformIncludesOptions } from "./transform-includes-options"
export { transformBody } from "./transform-body"
export { transformQuery } from "./transform-query"
export { transformQuery, transformStoreQuery } from "./transform-query"
export default {
authenticate,
@@ -12,9 +12,56 @@ import { FindConfig, QueryConfig, RequestQueryFields } from "../../types/common"
import { omit } from "lodash"
import { removeUndefinedProperties } from "../../utils"
/**
* Middleware that transform the query input for the admin end points
* @param plainToClass
* @param queryConfig
* @param config
*/
export function transformQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig?: Omit<
QueryConfig<TEntity>,
"allowedRelations" | "allowedFields"
>,
config: ValidatorOptions = {}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
normalizeQuery()(req, res, () => void 0)
const validated: T = await validator<T, Record<string, unknown>>(
plainToClass,
req.query,
config
)
req.validatedQuery = validated
req.filterableFields = getFilterableFields(validated)
req.allowedProperties = getAllowedProperties(
validated,
req.includes ?? {},
queryConfig
)
attachListOrRetrieveConfig<TEntity>(req, queryConfig)
next()
} catch (e) {
next(e)
}
}
}
/**
* Middleware that transform the query input for the store endpoints
* @param plainToClass
* @param queryConfig
* @param config
*/
export function transformStoreQuery<
T extends RequestQueryFields,
TEntity extends BaseEntity
>(
plainToClass: ClassConstructor<T>,
queryConfig?: QueryConfig<TEntity>,
@@ -29,47 +76,13 @@ export function transformQuery<
config
)
req.validatedQuery = validated
req.filterableFields = omit(validated, [
"limit",
"offset",
"expand",
"fields",
"order",
])
req.filterableFields = removeUndefinedProperties(req.filterableFields)
if (
(queryConfig?.defaultFields || validated.fields) &&
(queryConfig?.defaultRelations || validated.expand)
) {
req.allowedProperties = [
...(validated.fields
? validated.fields.split(",")
: queryConfig?.allowedFields || [])!,
...(validated.expand
? validated.expand.split(",")
: queryConfig?.allowedRelations || [])!,
] as unknown as string[]
}
const includesFields = Object.keys(req["includes"] ?? {})
if (includesFields.length) {
req.allowedProperties = req.allowedProperties ?? []
req.allowedProperties.push(...includesFields)
}
if (queryConfig?.isList) {
req.listConfig = prepareListQuery(
validated,
queryConfig
) as FindConfig<unknown>
} else {
req.retrieveConfig = prepareRetrieveQuery(
validated,
queryConfig
) as FindConfig<unknown>
}
req.filterableFields = getFilterableFields(validated)
req.allowedProperties = getStoreAllowedProperties(
validated,
req.includes ?? {},
queryConfig
)
attachListOrRetrieveConfig<TEntity>(req, queryConfig)
next()
} catch (e) {
@@ -77,3 +90,97 @@ export function transformQuery<
}
}
}
/**
* Omit the non filterable config from the validated object
* @param obj
*/
function getFilterableFields<T extends RequestQueryFields>(obj: T): T {
const result = omit(obj, [
"limit",
"offset",
"expand",
"fields",
"order",
]) as T
return removeUndefinedProperties(result)
}
/**
* build and attach the `retrieveConfig` or `listConfig`
* @param req
* @param queryConfig
*/
function attachListOrRetrieveConfig<TEntity extends BaseEntity>(
req: Request,
queryConfig?: QueryConfig<TEntity>
) {
const validated = req.validatedQuery
if (queryConfig?.isList) {
req.listConfig = prepareListQuery(
validated,
queryConfig
) as FindConfig<unknown>
} else {
req.retrieveConfig = prepareRetrieveQuery(
validated,
queryConfig
) as FindConfig<unknown>
}
}
/**
* Build the store allowed props based on the custom fields query params, the allowed props config and the includes options.
* This can be used later with `cleanResponseData` in order to clean up the returned objects to the client.
* @param queryConfig
* @param validated
* @param includesOptions
*/
function getStoreAllowedProperties<TEntity extends BaseEntity>(
validated: RequestQueryFields,
includesOptions: Record<string, boolean>,
queryConfig?: QueryConfig<TEntity>
): string[] {
const allowed: string[] = []
const includeKeys = Object.keys(includesOptions)
const fields = validated.fields
? validated.fields?.split(",")
: queryConfig?.allowedFields || []
const expand =
validated.expand || includeKeys.length
? [...(validated.expand?.split(",") || []), ...includeKeys]
: queryConfig?.allowedRelations || []
allowed.push(...fields, ...expand)
return allowed
}
/**
* Build the admin allowed props based on the custom fields query params, the defaults and the includes options.
* Since admin can access everything, it is only in order to return what the user asked for through fields and expand query params.
* This can be used later with `cleanResponseData` in order to clean up the returned objects to the client.
* @param queryConfig
* @param validated
* @param includesOptions
*/
function getAllowedProperties<TEntity extends BaseEntity>(
validated: RequestQueryFields,
includesOptions: Record<string, boolean>,
queryConfig?: QueryConfig<TEntity>
): string[] {
const allowed: (string | keyof TEntity)[] = []
const includeKeys = Object.keys(includesOptions)
const fields = validated.fields
? validated.fields?.split(",")
: queryConfig?.defaultFields || []
const expand =
validated.expand || includeKeys.length
? [...(validated.expand?.split(",") || []), ...includeKeys]
: queryConfig?.defaultRelations || []
allowed.push(...fields, ...expand)
return allowed as string[]
}
@@ -25,6 +25,7 @@ import { AddressPayload } from "../../../../types/common"
import { DraftOrderCreateProps } from "../../../../types/draft-orders"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /draft-orders
@@ -154,7 +155,7 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({ draft_order: cleanResponseData(draftOrder, []) })
}
enum Status {
@@ -13,6 +13,7 @@ import {
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { validator } from "../../../../utils/validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /draft-orders/{id}/line-items
@@ -138,7 +139,9 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({
draft_order: cleanResponseData(draftOrder, []),
})
})
}
@@ -8,6 +8,7 @@ import {
import { DraftOrder } from "../../../.."
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [delete] /draft-orders/{id}/line-items/{line_id}
@@ -93,6 +94,8 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({
draft_order: cleanResponseData(draftOrder, []),
})
})
}
@@ -7,6 +7,7 @@ import {
} from "."
import { DraftOrder } from "../../../.."
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /draft-orders/{id}
@@ -77,5 +78,7 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.json({ draft_order: draftOrder })
res.json({
draft_order: cleanResponseData(draftOrder, []),
})
}
@@ -13,6 +13,7 @@ import {
import { EntityManager } from "typeorm"
import { Order } from "../../../../models"
import { MedusaError } from "medusa-core-utils"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /draft-orders/{id}/pay
@@ -122,7 +123,7 @@ export default async (req, res) => {
return order
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
export const reserveQuantityForDraftOrder = async (
@@ -19,6 +19,7 @@ import { CartUpdateProps } from "../../../../types/cart"
import { AddressPayload } from "../../../../types/common"
import { validator } from "../../../../utils/validator"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /admin/draft-orders/{id}
@@ -137,7 +138,7 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({ draft_order: cleanResponseData(draftOrder, []) })
}
/**
@@ -11,6 +11,7 @@ import { EntityManager } from "typeorm"
import { LineItemUpdate } from "../../../../types/cart"
import { MedusaError } from "medusa-core-utils"
import { validator } from "../../../../utils/validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /draft-orders/{id}/line-items/{line_id}
@@ -139,7 +140,9 @@ export default async (req, res) => {
select: defaultAdminDraftOrdersCartFields,
})
res.status(200).json({ draft_order: draftOrder })
res.status(200).json({
draft_order: cleanResponseData(draftOrder, []),
})
})
}
@@ -9,6 +9,7 @@ import {
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/shipping-methods
@@ -97,7 +98,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
/**
@@ -1,6 +1,7 @@
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/archive
@@ -70,7 +71,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersOrderArchiveParams extends FindParams {}
@@ -3,6 +3,7 @@ import { ClaimService, OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/claims/{claim_id}/cancel
@@ -85,7 +86,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersClaimCancel extends FindParams {}
@@ -7,6 +7,7 @@ import {
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/claims/{claim_id}/fulfillments/{fulfillment_id}/cancel
@@ -101,7 +102,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersClaimFulfillmentsCancelParams extends FindParams {}
@@ -7,6 +7,7 @@ import {
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/swaps/{swap_id}/fulfillments/{fulfillment_id}/cancel
@@ -101,7 +102,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
// eslint-disable-next-line max-len
@@ -9,6 +9,7 @@ import { MedusaError } from "medusa-core-utils"
import { Fulfillment } from "../../../../models"
import { IInventoryService } from "../../../../interfaces"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/fulfillments/{fulfillment_id}/cancel
@@ -108,7 +109,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export const adjustInventoryForCancelledFulfillment = async (
@@ -1,6 +1,7 @@
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/cancel
@@ -69,7 +70,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersOrderCancel extends FindParams {}
@@ -3,6 +3,7 @@ import { OrderService, SwapService } from "../../../../services"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/swaps/{swap_id}/cancel
@@ -83,7 +84,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersSwapCancelParams extends FindParams {}
@@ -1,6 +1,7 @@
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/capture
@@ -72,7 +73,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersOrderCaptureParams extends FindParams {}
@@ -1,6 +1,7 @@
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/complete
@@ -72,7 +73,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminPostOrdersOrderCompleteParams extends FindParams {}
@@ -2,6 +2,7 @@ import { ClaimService, OrderService } from "../../../../services"
import { IsArray, IsNotEmpty, IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/claims/{claim_id}/shipments
@@ -92,7 +93,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
/**
@@ -16,6 +16,7 @@ import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { ClaimTypeValue } from "../../../../types/claim"
import { AddressPayload, FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /order/{id}/claims
@@ -254,7 +255,9 @@ export default async (req, res) => {
return {
response_code: 200,
response_body: { order },
response_body: {
order,
},
}
})
})
@@ -288,6 +291,13 @@ export default async (req, res) => {
throw err
}
if (idempotencyKey.response_body.order) {
idempotencyKey.response_body.order = cleanResponseData(
idempotencyKey.response_body.order,
[]
)
}
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
}
@@ -18,6 +18,7 @@ import {
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
import { Fulfillment, LineItem } from "../../../../models"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/fulfillment
@@ -139,7 +140,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
const updateInventoryAndReservations = async (
@@ -10,6 +10,7 @@ import { EntityManager } from "typeorm"
import { OrderService } from "../../../../services"
import { TrackingLink } from "../../../../models"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/shipment
@@ -104,7 +105,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
/**
@@ -10,6 +10,7 @@ import { OrderService, SwapService } from "../../../../services"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/swaps/{swap_id}/shipments
@@ -102,7 +103,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
/**
@@ -21,6 +21,7 @@ import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Type } from "class-transformer"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /order/{id}/swaps
@@ -216,7 +217,9 @@ export default async (req, res) => {
return {
response_code: 200,
response_body: { order },
response_body: {
order: cleanResponseData(order, []),
},
}
})
})
@@ -3,6 +3,7 @@ import { IsBoolean, IsObject, IsOptional } from "class-validator"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/claims/{claim_id}/fulfillments
@@ -84,7 +85,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
/**
@@ -4,6 +4,7 @@ import { OrderService, SwapService } from "../../../../services"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/swaps/{swap_id}/fulfillments
@@ -88,7 +89,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
/**
@@ -1,5 +1,6 @@
import { OrderService } from "../../../../services"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /orders/{id}
@@ -64,7 +65,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order: order })
res.json({ order: cleanResponseData(order, []) })
}
export class AdminGetOrdersOrderParams extends FindParams {}
@@ -94,7 +94,7 @@ export default (app, featureFlagRouter: FlagRouter) => {
transformIncludesOptions(allowedOrderIncludesFields),
transformQuery(AdminGetOrdersParams, {
defaultRelations: relations,
defaultFields: defaultAdminOrdersFields,
defaultFields: defaultFields,
isList: true,
}),
middlewares.wrap(require("./list-orders").default)
@@ -5,6 +5,7 @@ import { Order } from "../../../../models"
import { OrderService } from "../../../../services"
import { Type } from "class-transformer"
import { pick } from "lodash"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /orders
@@ -214,7 +215,12 @@ export default async (req, res) => {
data = orders.map((o) => pick(o, fields))
}
res.json({ orders: data, count, offset: skip, limit: take })
res.json({
orders: cleanResponseData(data, []),
count,
offset: skip,
limit: take,
})
}
export class AdminGetOrdersParams extends AdminListOrdersSelector {
@@ -2,6 +2,7 @@ import { OrderService, SwapService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/swaps/{swap_id}/process-payment
@@ -73,7 +74,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
// eslint-disable-next-line max-len
@@ -9,6 +9,7 @@ import {
import { OrderService } from "../../../../services"
import { EntityManager } from "typeorm"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/refund
@@ -97,7 +98,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
/**
@@ -18,6 +18,7 @@ import { EntityManager } from "typeorm"
import { Order, Return } from "../../../../models"
import { OrdersReturnItem } from "../../../../types/orders"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}/return
@@ -237,7 +238,9 @@ export default async (req, res) => {
return {
response_code: 200,
response_body: { order },
response_body: {
order: cleanResponseData(order, []),
},
}
})
})
@@ -14,6 +14,7 @@ import { Type } from "class-transformer"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { FindParams } from "../../../../types/common"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /order/{id}/claims/{claim_id}
@@ -103,7 +104,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.json({ order: data })
res.json({ order: cleanResponseData(data, []) })
}
/**
@@ -13,6 +13,7 @@ import { AddressPayload, FindParams } from "../../../../types/common"
import { EntityManager } from "typeorm"
import { OrderService } from "../../../../services"
import { Type } from "class-transformer"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /orders/{id}
@@ -96,7 +97,7 @@ export default async (req, res) => {
includes: req.includes,
})
res.status(200).json({ order })
res.status(200).json({ order: cleanResponseData(order, []) })
}
/**
@@ -3,7 +3,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { validator } from "../../../../utils/validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/shipping-methods
@@ -62,10 +62,7 @@ import { validator } from "../../../../utils/validator"
export default async (req, res) => {
const { id } = req.params
const validated = await validator(
StorePostCartsCartShippingMethodReq,
req.body
)
const validated = req.validatedBody
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
@@ -94,7 +91,7 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
/**
@@ -2,6 +2,7 @@ import { CartService, IdempotencyKeyService } from "../../../../services"
import { EntityManager } from "typeorm"
import { IdempotencyKey } from "../../../../models/idempotency-key"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/taxes
@@ -118,5 +119,12 @@ export default async (req, res) => {
throw err
}
if (idempotencyKey.response_body.cart) {
idempotencyKey.response_body.cart = cleanResponseData(
idempotencyKey.response_body.cart,
[]
)
}
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
}
@@ -2,6 +2,7 @@ import { EntityManager } from "typeorm"
import { AbstractCartCompletionStrategy } from "../../../../interfaces"
import { IdempotencyKey } from "../../../../models"
import { IdempotencyKeyService } from "../../../../services"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/complete
@@ -90,5 +91,9 @@ export default async (req, res) => {
req.request_context
)
if (response_body.data) {
response_body.data = cleanResponseData(response_body.data, [])
}
res.status(response_code).json(response_body)
}
@@ -23,6 +23,7 @@ import { FlagRouter } from "../../../../utils/flag-router"
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { CartCreateProps } from "../../../../types/cart"
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts
@@ -176,7 +177,7 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart })
res.status(200).json({ cart: cleanResponseData(cart, []) })
}
export class Item {
@@ -11,6 +11,7 @@ import {
runIdempotencyStep,
RunIdempotencyStepOptions,
} from "../../../../../utils/idempotency"
import { cleanResponseData } from "../../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/line-items
@@ -130,6 +131,13 @@ export default async (req, res) => {
throw err
}
if (idempotencyKey.response_body.cart) {
idempotencyKey.response_body.cart = cleanResponseData(
idempotencyKey.response_body.cart,
[]
)
}
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
}
@@ -2,6 +2,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import IdempotencyKeyService from "../../../../services/idempotency-key"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/payment-sessions
@@ -133,5 +134,11 @@ export default async (req, res) => {
throw err
}
if (idempotencyKey.response_body.cart) {
idempotencyKey.response_body.data = cleanResponseData(
idempotencyKey.response_body.cart,
[]
)
}
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
}
@@ -1,6 +1,7 @@
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [delete] /carts/{id}/discounts/{code}
@@ -71,5 +72,5 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,6 +1,7 @@
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [delete] /carts/{id}/line-items/{line_id}
@@ -72,5 +73,6 @@ export default async (req, res) => {
select: defaultStoreCartFields,
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,6 +1,7 @@
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [delete] /carts/{id}/payment-sessions/{provider_id}
@@ -63,5 +64,5 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -1,5 +1,6 @@
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /carts/{id}
@@ -70,5 +71,5 @@ export default async (req, res) => {
}
const data = await cartService.retrieveWithTotals(id, req.retrieveConfig)
res.json({ cart: data })
res.json({ cart: cleanResponseData(data, []) })
}
@@ -5,7 +5,7 @@ import { Cart, Order, Swap } from "../../../../"
import { DeleteResponse, FindParams } from "../../../../types/common"
import middlewares, {
transformBody,
transformQuery,
transformStoreQuery,
} from "../../../middlewares"
import { StorePostCartsCartReq } from "./update-cart"
import { StorePostCartReq } from "./create-cart"
@@ -13,6 +13,10 @@ import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-cha
import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys"
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
import { validateSalesChannelParam } from "../../../middlewares/publishable-api-key/validate-sales-channel-param"
import { StorePostCartsCartShippingMethodReq } from "./add-shipping-method"
import { StorePostCartsCartPaymentSessionReq } from "./set-payment-session"
import { StorePostCartsCartLineItemsItemReq } from "./update-line-item"
import { StorePostCartsCartPaymentSessionUpdateReq } from "./update-payment-session"
const route = Router()
@@ -34,7 +38,7 @@ export default (app, container) => {
route.get(
"/:id",
transformQuery(FindParams, {
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
@@ -44,6 +48,11 @@ export default (app, container) => {
const createMiddlewares = [
middlewareService.usePreCartCreation(),
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartReq),
]
@@ -62,75 +71,149 @@ export default (app, container) => {
route.post(
"/:id",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartsCartReq),
middlewares.wrap(require("./update-cart").default)
)
route.post(
"/:id/complete",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./complete-cart").default)
)
// DEPRECATION
route.post(
"/:id/complete-cart",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./complete-cart").default)
)
// Line items
route.post(
"/:id/line-items",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./create-line-item").default)
)
route.post(
"/:id/line-items/:line_id",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartsCartLineItemsItemReq),
middlewares.wrap(require("./update-line-item").default)
)
route.delete(
"/:id/line-items/:line_id",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./delete-line-item").default)
)
route.delete(
"/:id/discounts/:code",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./delete-discount").default)
)
// Payment sessions
route.post(
"/:id/payment-sessions",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./create-payment-sessions").default)
)
route.post(
"/:id/payment-sessions/:provider_id",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartsCartPaymentSessionUpdateReq),
middlewares.wrap(require("./update-payment-session").default)
)
route.delete(
"/:id/payment-sessions/:provider_id",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./delete-payment-session").default)
)
route.post(
"/:id/payment-sessions/:provider_id/refresh",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./refresh-payment-session").default)
)
route.post(
"/:id/payment-session",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartsCartPaymentSessionReq),
middlewares.wrap(require("./set-payment-session").default)
)
// Shipping Options
route.post(
"/:id/shipping-methods",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
transformBody(StorePostCartsCartShippingMethodReq),
middlewares.wrap(require("./add-shipping-method").default)
)
// Taxes
route.post(
"/:id/taxes",
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreCartRelations,
defaultFields: defaultStoreCartFields,
isList: false,
}),
middlewares.wrap(require("./calculate-taxes").default)
)
@@ -1,5 +1,6 @@
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/payment-sessions/{provider_id}/refresh
@@ -67,5 +68,5 @@ export default async (req, res) => {
],
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
@@ -3,7 +3,7 @@ import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { EntityManager } from "typeorm"
import { IsString } from "class-validator"
import { validator } from "../../../../utils/validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/payment-session
@@ -62,10 +62,7 @@ import { validator } from "../../../../utils/validator"
export default async (req, res) => {
const { id } = req.params
const validated = await validator(
StorePostCartsCartPaymentSessionReq,
req.body
)
const validated = req.validatedBody
const cartService: CartService = req.scope.resolve("cartService")
@@ -81,7 +78,7 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
/**
@@ -14,6 +14,7 @@ import { CartService } from "../../../../services"
import { AddressPayload } from "../../../../types/common"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { IsType } from "../../../../utils/validators/is-type"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}
@@ -100,7 +101,7 @@ export default async (req, res) => {
select: defaultStoreCartFields,
relations: defaultStoreCartRelations,
})
res.json({ cart: data })
res.json({ cart: cleanResponseData(data, []) })
}
class GiftCard {
@@ -3,7 +3,7 @@ import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/line-items/{line_id}
@@ -63,10 +63,7 @@ import { validator } from "../../../../utils/validator"
export default async (req, res) => {
const { id, line_id } = req.params
const validated = await validator(
StorePostCartsCartLineItemsItemReq,
req.body
)
const validated = req.validatedBody
const manager: EntityManager = req.scope.resolve("manager")
const cartService: CartService = req.scope.resolve("cartService")
@@ -115,7 +112,7 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
/**
@@ -1,8 +1,8 @@
import { IsObject } from "class-validator"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { EntityManager } from "typeorm"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [post] /carts/{id}/payment-sessions/{provider_id}
@@ -64,10 +64,7 @@ import { EntityManager } from "typeorm"
export default async (req, res) => {
const { id, provider_id } = req.params
const validated = await validator(
StorePostCartsCartPaymentSessionUpdateReq,
req.body
)
const validated = req.validatedBody
const cartService: CartService = req.scope.resolve("cartService")
@@ -86,7 +83,7 @@ export default async (req, res) => {
relations: defaultStoreCartRelations,
})
res.status(200).json({ cart: data })
res.status(200).json({ cart: cleanResponseData(data, []) })
}
/**
@@ -1,7 +1,7 @@
import { Router } from "express"
import { PaginatedResponse } from "../../../../types/common"
import { ProductCollection } from "../../../../"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import { StoreGetCollectionsParams } from "./list-collections"
const route = Router()
@@ -11,7 +11,7 @@ export default (app) => {
route.get(
"/",
transformQuery(StoreGetCollectionsParams, {
transformStoreQuery(StoreGetCollectionsParams, {
allowedFields,
isList: true,
}),
@@ -1,7 +1,7 @@
import { Router } from "express"
import { Customer, Order } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import {
defaultStoreOrdersFields,
defaultStoreOrdersRelations,
@@ -41,7 +41,7 @@ export default (app, container) => {
route.get(
"/me/orders",
transformQuery(StoreGetCustomersCustomerOrdersParams, {
transformStoreQuery(StoreGetCustomersCustomerOrdersParams, {
defaultFields: defaultStoreOrdersFields,
defaultRelations: defaultStoreOrdersRelations,
isList: true,
@@ -7,7 +7,7 @@ import {
} from "../../../../types/order-edit"
import middlewares, {
transformBody,
transformQuery,
transformStoreQuery,
} from "../../../middlewares"
import { StorePostOrderEditsOrderEditDecline } from "./decline-order-edit"
@@ -18,7 +18,7 @@ export default (app) => {
route.get(
"/:id",
transformQuery(FindParams, {
transformStoreQuery(FindParams, {
defaultRelations: defaultStoreOrderEditRelations,
defaultFields: defaultStoreOrderEditFields,
allowedFields: defaultStoreOrderEditFields,
@@ -1,6 +1,7 @@
import { defaultStoreOrdersFields, defaultStoreOrdersRelations } from "."
import { OrderService } from "../../../../services"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
* @oas [get] /orders/cart/{cart_id}
@@ -54,5 +55,5 @@ export default async (req, res) => {
relations: defaultStoreOrdersRelations,
})
res.json({ order })
res.json({ order: cleanResponseData(order, []) })
}
@@ -3,13 +3,14 @@ import "reflect-metadata"
import { Order } from "../../../.."
import middlewares, {
transformBody,
transformQuery,
transformStoreQuery,
} from "../../../middlewares"
import requireCustomerAuthentication from "../../../middlewares/require-customer-authentication"
import { StorePostCustomersCustomerOrderClaimReq } from "./request-order"
import { StorePostCustomersCustomerAcceptClaimReq } from "./confirm-order-request"
import { StoreGetOrderParams } from "./get-order"
import { StoreGetOrdersParams } from "./lookup-order"
import { FindParams } from "../../../../types/common"
const route = Router()
@@ -21,7 +22,7 @@ export default (app) => {
*/
route.get(
"/",
transformQuery(StoreGetOrdersParams, {
transformStoreQuery(StoreGetOrdersParams, {
defaultFields: defaultStoreOrdersFields,
defaultRelations: defaultStoreOrdersRelations,
allowedFields: allowedStoreOrdersFields,
@@ -36,7 +37,7 @@ export default (app) => {
*/
route.get(
"/:id",
transformQuery(StoreGetOrderParams, {
transformStoreQuery(StoreGetOrderParams, {
defaultFields: defaultStoreOrdersFields,
defaultRelations: defaultStoreOrdersRelations,
allowedFields: allowedStoreOrdersFields,
@@ -50,6 +51,12 @@ export default (app) => {
*/
route.get(
"/cart/:cart_id",
transformStoreQuery(FindParams, {
defaultFields: defaultStoreOrdersFields,
defaultRelations: defaultStoreOrdersRelations,
allowedFields: allowedStoreOrdersFields,
allowedRelations: allowedStoreOrdersRelations,
}),
middlewares.wrap(require("./get-order-by-cart").default)
)
@@ -2,7 +2,7 @@ import { Router } from "express"
import "reflect-metadata"
import middlewares, {
transformBody,
transformQuery,
transformStoreQuery,
} from "../../../middlewares"
import { PaymentCollection, PaymentSession } from "../../../../models"
@@ -18,7 +18,7 @@ export default (app, container) => {
route.get(
"/:id",
transformQuery(GetPaymentCollectionsParams, {
transformStoreQuery(GetPaymentCollectionsParams, {
defaultFields: defaultPaymentCollectionFields,
defaultRelations: defaultPaymentCollectionRelations,
isList: false,
@@ -1,5 +1,5 @@
import { Router } from "express"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import { ProductCategory } from "../../../../models"
import { PaginatedResponse } from "../../../../types/common"
@@ -18,7 +18,7 @@ export default (app) => {
route.get(
"/",
transformQuery(StoreGetProductCategoriesParams, {
transformStoreQuery(StoreGetProductCategoriesParams, {
defaultFields: defaultStoreProductCategoryFields,
allowedFields: allowedStoreProductCategoryFields,
defaultRelations: defaultStoreProductCategoryRelations,
@@ -29,7 +29,7 @@ export default (app) => {
route.get(
"/:id",
transformQuery(StoreGetProductCategoryParams, {
transformStoreQuery(StoreGetProductCategoryParams, {
defaultFields: defaultStoreProductCategoryFields,
allowedFields: allowedStoreProductCategoryFields,
defaultRelations: defaultStoreProductCategoryRelations,
@@ -1,7 +1,7 @@
import { Router } from "express"
import { ProductTag } from "../../../../models"
import { PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import { StoreGetProductTagsParams } from "./list-product-tags"
const route = Router()
@@ -11,7 +11,7 @@ export default (app: Router) => {
route.get(
"/",
transformQuery(StoreGetProductTagsParams, {
transformStoreQuery(StoreGetProductTagsParams, {
defaultFields: defaultStoreProductTagFields,
defaultRelations: defaultStoreProductTagRelations,
allowedFields: allowedStoreProductTagFields,
@@ -1,7 +1,7 @@
import { Router } from "express"
import { ProductType } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import "reflect-metadata"
import { StoreGetProductTypesParams } from "./list-product-types"
@@ -12,7 +12,7 @@ export default (app) => {
route.get(
"/",
transformQuery(StoreGetProductTypesParams, {
transformStoreQuery(StoreGetProductTypesParams, {
defaultFields: defaultStoreProductTypeFields,
defaultRelations: defaultStoreProductTypeRelations,
allowedFields: allowedStoreProductTypeFields,
@@ -2,7 +2,7 @@ import { RequestHandler, Router } from "express"
import "reflect-metadata"
import { Product } from "../../../.."
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, { transformStoreQuery } from "../../../middlewares"
import { FlagRouter } from "../../../../utils/flag-router"
import { PaginatedResponse } from "../../../../types/common"
import { extendRequestParams } from "../../../middlewares/publishable-api-key/extend-request-params"
@@ -28,7 +28,7 @@ export default (app, featureFlagRouter: FlagRouter) => {
route.get(
"/",
transformQuery(StoreGetProductsParams, {
transformStoreQuery(StoreGetProductsParams, {
defaultRelations: defaultStoreProductsRelations,
defaultFields: defaultStoreProductsFields,
allowedFields: allowedStoreProductsFields,
@@ -40,7 +40,7 @@ export default (app, featureFlagRouter: FlagRouter) => {
route.get(
"/:id",
transformQuery(StoreGetProductsProductParams, {
transformStoreQuery(StoreGetProductsProductParams, {
defaultRelations: defaultStoreProductsRelations,
defaultFields: defaultStoreProductsFields,
allowedFields: allowedStoreProductsFields,
@@ -1,13 +1,12 @@
import { IdempotencyKey } from "../models"
import { RequestContext } from "../types/request"
import { TransactionBaseService } from "./transaction-base-service"
export type CartCompletionResponse = {
/** The response code for the completion request */
response_code: number
/** The response body for the completion request */
response_body: object
response_body: Record<string, unknown>
}
export interface ICartCompletionStrategy {
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class lineItemAdjustmentsAmount1678093365812 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE line_item_adjustment ALTER COLUMN amount TYPE NUMERIC;
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE line_item_adjustment ALTER COLUMN amount TYPE integer;
`)
}
}
+5
View File
@@ -170,6 +170,10 @@
* type: integer
* example: 1000
* discount_total:
* description: The total of discount rounded
* type: integer
* example: 800
* raw_discount_total:
* description: The total of discount
* type: integer
* example: 800
@@ -376,6 +380,7 @@ export class Cart extends SoftDeletableEntity {
shipping_total?: number
discount_total?: number
raw_discount_total?: number
item_tax_total?: number | null
shipping_tax_total?: number | null
tax_total?: number | null
@@ -8,10 +8,9 @@ import {
PrimaryColumn,
} from "typeorm"
import { DbAwareColumn } from "../utils/db-aware-column"
import { DbAwareColumn, generateEntityId } from "../utils"
import { Discount } from "./discount"
import { LineItem } from "./line-item"
import { generateEntityId } from "../utils/generate-entity-id"
@Entity()
@Index(["discount_id", "item_id"], {
@@ -41,7 +40,7 @@ export class LineItemAdjustment {
@Column({ nullable: true })
discount_id: string
@Column({ type: "int" })
@Column({ type: "numeric", transformer: { to: (value) => value, from: (value) => parseFloat(value) } })
amount: number
@DbAwareColumn({ type: "jsonb", nullable: true })
@@ -93,7 +92,7 @@ export class LineItemAdjustment {
* $ref: "#/components/schemas/Discount"
* amount:
* description: The adjustment amount
* type: integer
* type: number
* example: 1000
* metadata:
* description: An optional key-value map with additional details
+7 -1
View File
@@ -10,7 +10,8 @@ import {
} from "typeorm"
import { BaseEntity } from "../interfaces"
import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing"
import TaxInclusivePricingFeatureFlag
from "../loaders/feature-flags/tax-inclusive-pricing"
import { generateEntityId } from "../utils"
import { DbAwareColumn } from "../utils/db-aware-column"
import { FeatureFlagColumn } from "../utils/feature-flag-decorators"
@@ -147,6 +148,7 @@ export class LineItem extends BaseEntity {
original_total?: number | null
original_tax_total?: number | null
discount_total?: number | null
raw_discount_total?: number | null
gift_card_total?: number | null
@BeforeInsert()
@@ -341,6 +343,10 @@ export class LineItem extends BaseEntity {
* type: integer
* example: 0
* discount_total:
* description: The total of discount of the line item rounded
* type: integer
* example: 0
* raw_discount_total:
* description: The total of discount of the line item
* type: integer
* example: 0
+6 -1
View File
@@ -248,6 +248,7 @@ export class Order extends BaseEntity {
// Total fields
shipping_total: number
discount_total: number
raw_discount_total: number
tax_total: number | null
refunded_total: number
total: number
@@ -513,10 +514,14 @@ export class Order extends BaseEntity {
* type: integer
* description: The total of shipping
* example: 1000
* discount_total:
* raw_discount_total:
* description: The total of discount
* type: integer
* example: 800
* discount_total:
* description: The total of discount rounded
* type: integer
* example: 800
* tax_total:
* description: The total of tax
* type: integer
@@ -737,8 +737,9 @@ describe("DiscountService", () => {
}
)
expect(adjustment1).toBe(291)
expect(adjustment2).toBe(109)
// The sum of both is equal to the expected 400
expect(adjustment1).toBeCloseTo(291, 0)
expect(adjustment2).toBeCloseTo(109, 0)
})
it("returns line item amount if discount exceeds lime item price", async () => {
@@ -1345,6 +1345,7 @@ describe("OrderService", () => {
id: IdMap.getId("order"),
items: [],
paid_total: 0,
raw_discount_total: 0,
refundable_amount: 0,
refunded_total: 0,
shipping_methods: [
@@ -1384,6 +1385,7 @@ describe("OrderService", () => {
id: IdMap.getId("order"),
items: [],
paid_total: 0,
raw_discount_total: 0,
refundable_amount: 0,
refunded_total: 0,
shipping_methods: [
+6 -1
View File
@@ -2577,7 +2577,7 @@ class CartService extends TransactionBaseService {
const itemWithTotals = Object.assign(item, itemsTotals[item.id] ?? {})
cart.subtotal! += itemWithTotals.subtotal ?? 0
cart.discount_total! += itemWithTotals.discount_total ?? 0
cart.discount_total! += itemWithTotals.raw_discount_total ?? 0
cart.item_tax_total! += itemWithTotals.tax_total ?? 0
return itemWithTotals
@@ -2604,10 +2604,15 @@ class CartService extends TransactionBaseService {
giftCards: cart.gift_cards,
}
)
cart.gift_card_total = giftCardTotal.total || 0
cart.gift_card_tax_total = giftCardTotal.tax_total || 0
cart.tax_total = cart.item_tax_total + cart.shipping_tax_total
cart.discount_total = Math.round(cart.discount_total)
cart.raw_discount_total = cart.discount_total
cart.discount_total = Math.round(cart.discount_total)
cart.total =
cart.subtotal +
+7 -5
View File
@@ -634,7 +634,10 @@ class DiscountService extends TransactionBaseService {
})
let fullItemPrice = lineItem.unit_price * lineItem.quantity
const includesTax = this.featureFlagRouter_.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key) && lineItem.includes_tax
const includesTax =
this.featureFlagRouter_.isFeatureEnabled(
TaxInclusivePricingFeatureFlag.key
) && lineItem.includes_tax
if (includesTax) {
const lineItemTotals = await this.newTotalsService_
@@ -671,14 +674,13 @@ class DiscountService extends TransactionBaseService {
}, 0)
const nominator = Math.min(value, subtotal)
const totalItemPercentage = fullItemPrice / subtotal
adjustment = Math.round(nominator * totalItemPercentage)
adjustment = nominator * totalItemPercentage
} else {
adjustment = value * lineItem.quantity
}
// if the amount of the discount exceeds the total price of the item,
// we return the total item price, else the fixed amount
return adjustment >= fullItemPrice ? fullItemPrice : adjustment
return Math.min(adjustment, fullItemPrice)
})
}
+10 -2
View File
@@ -31,6 +31,8 @@ type LineItemTotals = {
original_tax_total: number
tax_lines: LineItemTaxLine[]
discount_total: number
raw_discount_total: number
}
type GiftCardTransaction = {
@@ -172,7 +174,8 @@ export default class NewTotalsService extends TransactionBaseService {
subtotal = 0 // in that case we need to know the tax rate to compute it later
}
const discount_total = lineItemAllocation.discount?.amount ?? 0
const raw_discount_total = lineItemAllocation.discount?.amount ?? 0
const discount_total = Math.round(raw_discount_total)
const totals: LineItemTotals = {
unit_price: item.unit_price,
@@ -184,6 +187,8 @@ export default class NewTotalsService extends TransactionBaseService {
original_tax_total: 0,
tax_total: 0,
tax_lines: item.tax_lines ?? [],
raw_discount_total: raw_discount_total,
}
if (includeTax) {
@@ -274,7 +279,8 @@ export default class NewTotalsService extends TransactionBaseService {
subtotal = 0 // in that case we need to know the tax rate to compute it later
}
const discount_total = lineItemAllocation.discount?.amount ?? 0
const raw_discount_total = lineItemAllocation.discount?.amount ?? 0
const discount_total = Math.round(raw_discount_total)
const totals: LineItemTotals = {
unit_price: item.unit_price,
@@ -286,6 +292,8 @@ export default class NewTotalsService extends TransactionBaseService {
original_tax_total: 0,
tax_total: 0,
tax_lines: [],
raw_discount_total,
}
taxRate = taxRate / 100
+8 -4
View File
@@ -17,14 +17,14 @@ import {
PaymentStatus,
Return,
Swap,
TrackingLink
TrackingLink,
} from "../models"
import { AddressRepository } from "../repositories/address"
import { OrderRepository } from "../repositories/order"
import { FindConfig, QuerySelector, Selector } from "../types/common"
import {
CreateFulfillmentOrder,
FulFillmentItemType
FulFillmentItemType,
} from "../types/fulfillment"
import { TotalsContext, UpdateOrderInput } from "../types/orders"
import { CreateShippingMethodDto } from "../types/shipping-options"
@@ -48,7 +48,7 @@ import {
ShippingOptionService,
ShippingProfileService,
TaxProviderService,
TotalsService
TotalsService,
} from "."
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
@@ -1777,6 +1777,7 @@ class OrderService extends TransactionBaseService {
order.paid_total =
order.payments?.reduce((acc, next) => (acc += next.amount), 0) || 0
order.refundable_amount = order.paid_total - order.refunded_total || 0
let item_tax_total = 0
let shipping_tax_total = 0
@@ -1790,7 +1791,7 @@ class OrderService extends TransactionBaseService {
Object.assign(item, itemsTotals[item.id] ?? {}, { refundable })
order.subtotal += item.subtotal ?? 0
order.discount_total += item.discount_total ?? 0
order.discount_total += item.raw_discount_total ?? 0
item_tax_total += item.tax_total ?? 0
if (isReturnableItem(item)) {
@@ -1864,6 +1865,9 @@ class OrderService extends TransactionBaseService {
})
}
order.raw_discount_total = order.discount_total
order.discount_total = Math.round(order.discount_total)
order.total =
order.subtotal +
order.shipping_total +
+10 -3
View File
@@ -58,6 +58,8 @@ type LineItemTotals = {
original_tax_total: number
tax_lines: LineItemTaxLine[]
discount_total: number
raw_discount_total: number
}
type LineItemTotalsOptions = {
@@ -465,7 +467,7 @@ class TotalsService extends TransactionBaseService {
/**
* Used for the refund computation
*/
unit_amount: Math.round(adjustmentAmount / ld.item.quantity),
unit_amount: adjustmentAmount / ld.item.quantity,
}
} else {
allocationMap[ld.item.id] = {
@@ -800,7 +802,8 @@ class TotalsService extends TransactionBaseService {
subtotal = 0 // in that case we need to know the tax rate to compute it later
}
const discount_total = lineItemAllocation.discount?.amount ?? 0
const raw_discount_total = lineItemAllocation.discount?.amount ?? 0
const discount_total = Math.round(raw_discount_total)
const lineItemTotals: LineItemTotals = {
unit_price: lineItem.unit_price,
@@ -812,6 +815,8 @@ class TotalsService extends TransactionBaseService {
original_tax_total: 0,
tax_total: 0,
tax_lines: lineItem.tax_lines || [],
raw_discount_total,
}
// Tax Information
@@ -1007,7 +1012,9 @@ class TotalsService extends TransactionBaseService {
excludeNonDiscounts: true,
})
const discountTotal = this.getLineItemAdjustmentsTotal(cartOrOrder)
const discountTotal = Math.round(
this.getLineItemAdjustmentsTotal(cartOrOrder)
)
if (subtotal < 0) {
return this.rounded(Math.max(subtotal, discountTotal))
+1
View File
@@ -18,6 +18,7 @@ declare global {
retrieveConfig: FindConfig<unknown>
filterableFields: Record<string, unknown>
allowedProperties: string[]
includes?: Record<string, boolean>
errors: string[]
}
}
@@ -0,0 +1,59 @@
import { omitDeep } from "../omit-deep"
describe("omitDeep", () => {
it("should omit properties in a nested object", () => {
const input = {
id: 1,
__typename: "123",
createdAt: "1020209",
address: {
id: 1,
__typename: "123",
},
variants: [
20,
{
id: 22,
title: "hello world",
__typename: "123",
createdAt: "1020209",
variantOption: {
id: 1,
__typename: "123",
},
},
{
id: 32,
test: null,
__typename: "123",
createdAt: "1020209",
},
],
}
const output = {
id: 1,
address: {
id: 1,
},
variants: [
20,
{
id: 22,
title: "hello world",
variantOption: {
id: 1,
},
},
{
id: 32,
test: null,
},
],
}
expect(
omitDeep(input, ["createdAt", "updatedAt", "__typename"])
).toEqual(output)
})
})
@@ -1,21 +1,64 @@
import { pick } from "lodash"
import { omitDeep } from "./omit-deep"
// TODO: once the legacy totals decoration will be removed.
// We will be able to only compute the totals if one of the total fields is present
// and therefore avoid totals computation if the user don't want them to appear in the response
// and therefore the below const will be removed
const INCLUDED_FIELDS = [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"total",
"subtotal",
"paid_total",
"refundable_amount",
"gift_card_total",
"gift_card_tax_total",
"item_tax_total",
"shipping_tax_total",
"refundable",
"original_total",
"original_tax_total",
]
const EXCLUDED_FIELDS = ["raw_discount_total"]
/**
* Filter response data to contain props specified in the fields array.
* Filter response data to contain props specified in the `allowedProperties`.
* You can read more in the transformQuery middleware utility methods.
*
* @param data - record or an array of records in the response
* @param fields - record props allowed in the response
*/
function cleanResponseData<T>(data: T, fields: string[]) {
function cleanResponseData<T extends unknown | unknown[]>(
data: T,
fields: string[]
): T extends [] ? Partial<T>[] : Partial<T> {
fields = fields ?? []
const isDataArray = Array.isArray(data)
let arrayData: Partial<T>[] = isDataArray ? data : [data]
if (!fields.length) {
return data
arrayData = arrayData.map((record) => omitDeep(record, EXCLUDED_FIELDS))
return (isDataArray ? arrayData : arrayData[0]) as T extends []
? Partial<T>[]
: Partial<T>
}
if (Array.isArray(data)) {
return data.map((record) => pick(record, fields))
}
const fieldsSet = new Set([...fields, ...INCLUDED_FIELDS])
return pick(data, fields)
fields = [...fieldsSet]
arrayData = arrayData.map((record) =>
pick(omitDeep(record, EXCLUDED_FIELDS), fields)
)
return (isDataArray ? arrayData : arrayData[0]) as T extends []
? Partial<T>[]
: Partial<T>
}
export { cleanResponseData }
+1
View File
@@ -10,3 +10,4 @@ export * from "./exception-formatter"
export * from "./db-aware-column"
export * from "./is-object"
export * from "./has-changes"
export * from "./omit-deep"
+2 -2
View File
@@ -1,3 +1,3 @@
export function isObject(obj: unknown): obj is object {
return typeof obj === "object" && !!obj
export function isObject(obj: any): obj is object {
return obj != null && obj?.constructor?.name === "Object"
}
+34
View File
@@ -0,0 +1,34 @@
import { isObject } from "./is-object"
export function omitDeep<T extends object = object>(
input: object,
excludes: Array<number | string>
): T {
if (!input) {
return input
}
return Object.entries(input).reduce((nextInput, [key, value]) => {
const shouldExclude = excludes.includes(key)
if (shouldExclude) {
return nextInput
}
if (Array.isArray(value)) {
nextInput[key] = value.map((arrItem) => {
if (isObject(arrItem)) {
return omitDeep(arrItem, excludes)
}
return arrItem
})
return nextInput
} else if (isObject(value)) {
nextInput[key] = omitDeep(value, excludes)
return nextInput
}
nextInput[key] = value
return nextInput
}, {} as T)
}