chore(order): big number calculations (#6651)

Co-authored-by: Adrien de Peretti <25098370+adrien2p@users.noreply.github.com>
This commit is contained in:
Carlos R. L. Rodrigues
2024-03-11 14:51:00 -03:00
committed by GitHub
parent 7c46b0f88b
commit d48c076b77
23 changed files with 637 additions and 197 deletions
@@ -1,7 +1,7 @@
import { BigNumberInput } from "@medusajs/types"
import { Property } from "@mikro-orm/core"
import { isPresent, trimZeros } from "../../common"
import { BigNumber } from "../../totals/big-number"
import { BigNumberInput } from "@medusajs/types"
export function MikroOrmBigNumberProperty(
options: Parameters<typeof Property>[0] & {
@@ -13,47 +13,52 @@ export function MikroOrmBigNumberProperty(
Object.defineProperty(target, columnName, {
get() {
return this.__helper.__data[columnName]
let value = this.__helper?.__data?.[columnName]
if (!value && this[rawColumnName]) {
value = new BigNumber(this[rawColumnName].value, {
precision: this[rawColumnName].precision,
}).numeric
}
return value
},
set(value: BigNumberInput) {
if (options?.nullable && !isPresent(value)) {
this.__helper.__data[columnName] = null
this.__helper.__data[rawColumnName]
this[rawColumnName] = null
return
}
let bigNumber: BigNumber
if (value instanceof BigNumber) {
bigNumber = value
} else if (this[rawColumnName]) {
const precision = this[rawColumnName].precision
this[rawColumnName].value = trimZeros(
new BigNumber(value, {
precision,
}).raw!.value as string
)
bigNumber = new BigNumber(this[rawColumnName])
} else {
bigNumber = new BigNumber(value)
let bigNumber: BigNumber
if (value instanceof BigNumber) {
bigNumber = value
} else if (this[rawColumnName]) {
const precision = this[rawColumnName].precision
bigNumber = new BigNumber(value, {
precision,
})
} else {
bigNumber = new BigNumber(value)
}
const raw = bigNumber.raw!
raw.value = trimZeros(raw.value as string)
this.__helper.__data[columnName] = bigNumber.numeric
this.__helper.__data[rawColumnName] = raw
this[rawColumnName] = raw
}
this.__helper.__data[columnName] = bigNumber.numeric
const raw = bigNumber.raw!
raw.value = trimZeros(raw.value as string)
this[rawColumnName] = raw
this.__helper.__touched = !this.__helper.hydrator.isRunning()
},
enumerable: true,
configurable: true,
})
Property({
type: "number",
type: "any",
columnType: "numeric",
trackChanges: false,
...options,
@@ -278,10 +278,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
async update(data: { entity; update }[], context?: Context): Promise<T[]> {
const manager = this.getActiveManager<EntityManager>(context)
const entities = data.map((data_) => {
return manager.assign(
data_.entity,
data_.update as RequiredEntityData<T>
)
return manager.assign(data_.entity, data_.update)
})
manager.persist(entities)
@@ -0,0 +1,130 @@
import { BigNumber } from "../big-number"
import { transformPropertiesToBigNumber } from "../transform-properties-to-bignumber"
describe("Transfor Properties to BigNumber", function () {
it("should transform all properties containing matching prefix _raw to BigNumber", function () {
const obj = {
price: 42,
raw_price: {
value: "42",
precision: 10,
},
field: 111,
metadata: {
numeric_field: 100,
raw_numeric_field: {
value: "100",
},
random_field: 134,
},
abc: null,
raw_abc: {
value: "9.00000010000103991234",
precision: 20,
},
}
transformPropertiesToBigNumber(obj)
const price = obj.price as unknown as BigNumber
expect(price).toBeInstanceOf(BigNumber)
expect(price.numeric).toEqual(42)
expect(price.raw).toEqual({
value: "42",
precision: 10,
})
expect(obj.field).toBe(111)
const metaNum = obj.metadata.numeric_field as unknown as BigNumber
expect(metaNum).toBeInstanceOf(BigNumber)
expect(metaNum.numeric).toEqual(100)
expect(metaNum.raw).toEqual({
value: "100",
precision: 20,
})
expect(obj.metadata.random_field).toBe(134)
const abc = obj.abc as unknown as BigNumber
expect(abc).toBeInstanceOf(BigNumber)
expect(abc.numeric).toEqual(9.00000010000104)
expect(abc.raw).toEqual({
value: "9.00000010000103991234",
precision: 20,
})
})
it("should transform all properties on the option 'include' to BigNumber", function () {
const obj = {
price: 42,
raw_price: {
value: "42",
precision: 10,
},
field: 111,
metadata: {
random_field: 134,
},
}
transformPropertiesToBigNumber(obj, {
include: ["metadata.random_field"],
})
expect(obj.price).toBeInstanceOf(BigNumber)
const price = obj.price as unknown as BigNumber
expect(price.numeric).toEqual(42)
expect(price.raw).toEqual({
value: "42",
precision: 10,
})
expect(obj.field).toBe(111)
const metaNum = obj.metadata.random_field as unknown as BigNumber
expect(metaNum).toBeInstanceOf(BigNumber)
expect(metaNum.numeric).toEqual(134)
expect(metaNum.raw).toEqual({
value: "134.00000000000000000",
precision: 20,
})
})
it("should transform all properties containing matching prefix _raw to BigNumber excluding selected ones", function () {
const obj = {
price: 42,
raw_price: {
value: "42",
precision: 10,
},
metadata: {
numeric_field: 100,
raw_numeric_field: {
value: "100",
},
},
abc: null,
raw_abc: {
value: "9.00000010000103991234",
precision: 20,
},
}
transformPropertiesToBigNumber(obj, {
exclude: ["abc", "metadata.numeric_field"],
})
const price = obj.price as unknown as BigNumber
expect(obj.price).toBeInstanceOf(BigNumber)
expect(price.numeric).toEqual(42)
expect(price.raw).toEqual({
value: "42",
precision: 10,
})
expect(obj.abc).toEqual(null)
})
})
+27 -9
View File
@@ -7,18 +7,24 @@ export class BigNumber {
private numeric_: number
private raw_?: BigNumberRawValue
private bignumber_?: BigNumberJS
constructor(rawValue: BigNumberInput, options?: { precision?: number }) {
constructor(
rawValue: BigNumberInput | BigNumber,
options?: { precision?: number }
) {
this.setRawValueOrThrow(rawValue, options)
}
setRawValueOrThrow(
rawValue: BigNumberInput,
rawValue: BigNumberInput | BigNumber,
{ precision }: { precision?: number } = {}
) {
precision ??= BigNumber.DEFAULT_PRECISION
if (BigNumberJS.isBigNumber(rawValue)) {
if (rawValue instanceof BigNumber) {
Object.assign(this, rawValue)
} else if (BigNumberJS.isBigNumber(rawValue)) {
/**
* Example:
* const bnUnitValue = new BigNumberJS("10.99")
@@ -29,6 +35,7 @@ export class BigNumber {
value: rawValue.toPrecision(precision),
precision,
}
this.bignumber_ = rawValue
} else if (isString(rawValue)) {
/**
* Example: const unitValue = "1234.1234"
@@ -40,26 +47,31 @@ export class BigNumber {
value: bigNum.toPrecision(precision),
precision,
}
this.bignumber_ = bigNum
} else if (isBigNumber(rawValue)) {
/**
* Example: const unitValue = { value: "1234.1234" }
*/
const definedPrecision = rawValue.precision ?? precision
this.numeric_ = BigNumberJS(rawValue.value).toNumber()
const bigNum = new BigNumberJS(rawValue.value)
this.numeric_ = bigNum.toNumber()
this.raw_ = {
...rawValue,
precision: definedPrecision,
}
this.bignumber_ = bigNum
} else if (typeof rawValue === `number` && !Number.isNaN(rawValue)) {
/**
* Example: const unitValue = 1234
*/
this.numeric_ = rawValue as number
const bigNum = new BigNumberJS(rawValue as number)
this.raw_ = {
value: BigNumberJS(rawValue as number).toPrecision(precision),
value: bigNum.toPrecision(precision),
precision,
}
this.bignumber_ = bigNum
} else {
throw new Error(
`Invalid BigNumber value: ${rawValue}. Should be one of: string, number, BigNumber (bignumber.js), BigNumberRawValue`
@@ -80,27 +92,33 @@ export class BigNumber {
const newValue = new BigNumber(value)
this.numeric_ = newValue.numeric_
this.raw_ = newValue.raw_
this.bignumber_ = newValue.bignumber_
}
get raw(): BigNumberRawValue | undefined {
return this.raw_
}
get bigNumber(): BigNumberJS | undefined {
return this.bignumber_
}
set raw(rawValue: BigNumberInput) {
const newValue = new BigNumber(rawValue)
this.numeric_ = newValue.numeric_
this.raw_ = newValue.raw_
this.bignumber_ = newValue.bignumber_
}
toJSON() {
return this.raw_
return this.bignumber_
? this.bignumber_?.toNumber()
: this.raw_
? new BigNumberJS(this.raw_.value).toNumber()
: this.numeric_
}
valueOf() {
return this.raw_
? new BigNumberJS(this.raw_.value).toNumber()
: this.numeric_
return this.bignumber_
}
}
+3
View File
@@ -7,7 +7,10 @@ import { BigNumber as BigNumberJs } from "bignumber.js"
import { BigNumber } from "./big-number"
import { toBigNumberJs } from "./to-big-number-js"
export * from "./math"
export * from "./promotion"
export * from "./to-big-number-js"
export * from "./transform-properties-to-bignumber"
type GetLineItemTotalsContext = {
includeTax?: boolean
+108
View File
@@ -0,0 +1,108 @@
import { BigNumberInput, BigNumberRawValue } from "@medusajs/types"
import { BigNumber as BigNumberJS } from "bignumber.js"
import { isDefined } from "../common"
import { BigNumber } from "./big-number"
type BNInput = BigNumberInput | BigNumber
export class MathBN {
static convert(num: BNInput): BigNumberJS {
if (num instanceof BigNumber) {
return num.bigNumber!
} else if (num instanceof BigNumberJS) {
return num
} else if (isDefined((num as BigNumberRawValue)?.value)) {
return new BigNumberJS((num as BigNumberRawValue).value)
}
return new BigNumberJS(num as BigNumberJS | number)
}
static add(...nums: BNInput[]): BigNumberJS {
let sum = new BigNumberJS(0)
for (const num of nums) {
const n = MathBN.convert(num)
sum = sum.plus(n)
}
return sum
}
static sum(...nums: BNInput[]): BigNumberJS {
return MathBN.add(...nums)
}
static sub(...nums: BNInput[]): BigNumberJS {
let agg = MathBN.convert(nums[0])
for (let i = 1; i < nums.length; i++) {
const n = MathBN.convert(nums[i])
agg = agg.minus(n)
}
return agg
}
static mult(n1: BNInput, n2: BNInput): BigNumberJS {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.times(num2)
}
static div(n1: BNInput, n2: BNInput): BigNumberJS {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.dividedBy(num2)
}
static abs(n: BNInput): BigNumberJS {
const num = MathBN.convert(n)
return num.absoluteValue()
}
static mod(n1: BNInput, n2: BNInput): BigNumberJS {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.modulo(num2)
}
static exp(n: BNInput, exp = 2): BigNumberJS {
const num = MathBN.convert(n)
const expBy = MathBN.convert(exp)
return num.exponentiatedBy(expBy)
}
static min(...nums: BNInput[]): BigNumberJS {
return BigNumberJS.minimum(...nums.map((num) => MathBN.convert(num)))
}
static max(...nums: BNInput[]): BigNumberJS {
return BigNumberJS.maximum(...nums.map((num) => MathBN.convert(num)))
}
static gt(n1: BNInput, n2: BNInput): boolean {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.isGreaterThan(num2)
}
static gte(n1: BNInput, n2: BNInput): boolean {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.isGreaterThanOrEqualTo(num2)
}
static lt(n1: BNInput, n2: BNInput): boolean {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.isLessThan(num2)
}
static lte(n1: BNInput, n2: BNInput): boolean {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.isLessThanOrEqualTo(num2)
}
static eq(n1: BNInput, n2: BNInput): boolean {
const num1 = MathBN.convert(n1)
const num2 = MathBN.convert(n2)
return num1.isEqualTo(num2)
}
}
@@ -2,14 +2,11 @@ import { BigNumberInput } from "@medusajs/types"
import { BigNumber as BigNumberJs } from "bignumber.js"
import { isDefined, toCamelCase } from "../common"
import { BigNumber } from "./big-number"
type InputEntity<T, V extends string> = { [key in V]?: InputEntityField }
type InputEntityField = number | string | BigNumber
type Camelize<V extends string> = V extends `${infer A}_${infer B}`
? `${A}${Camelize<Capitalize<B>>}`
: V
type Output<V extends string> = { [key in Camelize<V>]: BigNumberJs }
export function toBigNumberJs<T, V extends string>(
@@ -0,0 +1,56 @@
import { BigNumber } from "./big-number"
export function transformPropertiesToBigNumber(
obj,
{
prefix = "raw_",
include = [],
exclude = [],
}: {
prefix?: string
include?: string[]
exclude?: string[]
} = {}
) {
const stack = [{ current: obj, path: "" }]
while (stack.length > 0) {
const { current, path } = stack.pop()!
if (
current == null ||
typeof current !== "object" ||
current instanceof BigNumber
) {
continue
}
if (Array.isArray(current)) {
current.forEach((element, index) =>
stack.push({ current: element, path })
)
} else {
for (const key of Object.keys(current)) {
const value = current[key]
const currentPath = path ? `${path}.${key}` : key
if (value != null && !exclude.includes(currentPath)) {
if (key.startsWith(prefix)) {
const newKey = key.replace(prefix, "")
const newPath = path ? `${path}.${newKey}` : newKey
if (!exclude.includes(newPath)) {
current[newKey] = new BigNumber(value)
continue
}
} else if (include.includes(currentPath)) {
current[key] = new BigNumber(value)
continue
}
}
stack.push({ current: value, path: currentPath })
}
}
}
}