From 36a61658f969a7b19c84a1e621ad1464927cafb1 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 22 Feb 2024 17:58:41 +0100 Subject: [PATCH] feat(utils): Fix big number decorator and cleanup (#6473) **What** - Fix big number decorator and cleanup --- .changeset/fair-panthers-nail.md | 5 + packages/order/src/models/adjustment-line.ts | 5 +- packages/order/src/models/line-item.ts | 14 +- packages/order/src/models/order-detail.ts | 23 +- packages/order/src/models/shipping-method.ts | 7 +- packages/order/src/models/tax-line.ts | 5 +- packages/order/src/models/transaction.ts | 5 +- .../services/payment-module/index.spec.ts | 1256 ++++++++--------- .../payment/src/models/payment-session.ts | 16 +- packages/utils/jest.config.js | 2 +- .../__tests__/big-number-field.spec.ts | 47 + .../src/dal/mikro-orm/big-number-field.ts | 116 +- packages/utils/src/totals/big-number.ts | 21 +- packages/utils/tsconfig.spec.json | 8 + 14 files changed, 757 insertions(+), 773 deletions(-) create mode 100644 .changeset/fair-panthers-nail.md create mode 100644 packages/utils/src/dal/mikro-orm/__tests__/big-number-field.spec.ts create mode 100644 packages/utils/tsconfig.spec.json diff --git a/.changeset/fair-panthers-nail.md b/.changeset/fair-panthers-nail.md new file mode 100644 index 0000000000..128b0eaeef --- /dev/null +++ b/.changeset/fair-panthers-nail.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +feat(utils): Fix big number decorator and cleanup diff --git a/packages/order/src/models/adjustment-line.ts b/packages/order/src/models/adjustment-line.ts index 6c988f912c..5686856993 100644 --- a/packages/order/src/models/adjustment-line.ts +++ b/packages/order/src/models/adjustment-line.ts @@ -1,5 +1,5 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" -import { BigNumber, BigNumberField } from "@medusajs/utils" +import { BigNumber, MikroOrmBigNumberProperty } from "@medusajs/utils" import { OptionalProps, PrimaryKey, Property } from "@mikro-orm/core" type OptionalAdjustmentLineProps = DAL.EntityDateColumns @@ -26,8 +26,7 @@ export default abstract class AdjustmentLine { @Property({ columnType: "text", nullable: true }) code: string | null = null - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() amount: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/order/src/models/line-item.ts b/packages/order/src/models/line-item.ts index 91da94c8eb..831d905523 100644 --- a/packages/order/src/models/line-item.ts +++ b/packages/order/src/models/line-item.ts @@ -1,9 +1,9 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, - BigNumberField, createPsqlIndexStatementHelper, generateEntityId, + MikroOrmBigNumberProperty, } from "@medusajs/utils" import { BeforeCreate, @@ -11,8 +11,8 @@ import { Collection, Entity, ManyToOne, - OnInit, OneToMany, + OnInit, OptionalProps, PrimaryKey, Property, @@ -114,15 +114,17 @@ export default class LineItem { @Property({ columnType: "boolean" }) is_tax_inclusive = false - @Property({ columnType: "numeric", nullable: true }) - @BigNumberField({ nullable: true }) + @MikroOrmBigNumberProperty({ + nullable: true, + }) compare_at_unit_price?: BigNumber | number | null = null @Property({ columnType: "jsonb", nullable: true }) raw_compare_at_unit_price: BigNumberRawValue | null = null - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty({ + nullable: true, + }) unit_price: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/order/src/models/order-detail.ts b/packages/order/src/models/order-detail.ts index 476bd01a89..357f200c8d 100644 --- a/packages/order/src/models/order-detail.ts +++ b/packages/order/src/models/order-detail.ts @@ -1,9 +1,9 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, - BigNumberField, createPsqlIndexStatementHelper, generateEntityId, + MikroOrmBigNumberProperty, } from "@medusajs/utils" import { BeforeCreate, @@ -57,50 +57,43 @@ export default class OrderDetail { }) item: LineItem - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() fulfilled_quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_fulfilled_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() shipped_quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_shipped_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() return_requested_quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_return_requested_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() return_received_quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_return_received_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() return_dismissed_quantity: BigNumber | number @Property({ columnType: "jsonb" }) raw_return_dismissed_quantity: BigNumberRawValue - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() written_off_quantity: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/order/src/models/shipping-method.ts b/packages/order/src/models/shipping-method.ts index 612f3e4edd..8f1686173f 100644 --- a/packages/order/src/models/shipping-method.ts +++ b/packages/order/src/models/shipping-method.ts @@ -1,9 +1,9 @@ import { BigNumberRawValue } from "@medusajs/types" import { BigNumber, - BigNumberField, createPsqlIndexStatementHelper, generateEntityId, + MikroOrmBigNumberProperty, } from "@medusajs/utils" import { BeforeCreate, @@ -12,8 +12,8 @@ import { Collection, Entity, ManyToOne, - OnInit, OneToMany, + OnInit, PrimaryKey, Property, } from "@mikro-orm/core" @@ -54,8 +54,7 @@ export default class ShippingMethod { @Property({ columnType: "jsonb", nullable: true }) description: string | null = null - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() amount: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/order/src/models/tax-line.ts b/packages/order/src/models/tax-line.ts index 19637c915b..5ebc53193f 100644 --- a/packages/order/src/models/tax-line.ts +++ b/packages/order/src/models/tax-line.ts @@ -1,5 +1,5 @@ import { BigNumberRawValue } from "@medusajs/types" -import { BigNumber, BigNumberField } from "@medusajs/utils" +import {BigNumber, MikroOrmBigNumberProperty} from "@medusajs/utils" import { PrimaryKey, Property } from "@mikro-orm/core" /** @@ -22,8 +22,7 @@ export default abstract class TaxLine { @Property({ columnType: "text" }) code: string - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() rate: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/order/src/models/transaction.ts b/packages/order/src/models/transaction.ts index 725e3a0aa4..7d86ad9460 100644 --- a/packages/order/src/models/transaction.ts +++ b/packages/order/src/models/transaction.ts @@ -1,9 +1,9 @@ import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, - BigNumberField, createPsqlIndexStatementHelper, generateEntityId, + MikroOrmBigNumberProperty, } from "@medusajs/utils" import { BeforeCreate, @@ -52,8 +52,7 @@ export default class Transaction { }) order: Order - @Property({ columnType: "numeric" }) - @BigNumberField() + @MikroOrmBigNumberProperty() amount: BigNumber | number @Property({ columnType: "jsonb" }) diff --git a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index 2c3a89286f..603d7d7007 100644 --- a/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -1,735 +1,675 @@ import { IPaymentModuleService } from "@medusajs/types" -import { initModules } from "medusa-test-utils" import { Modules } from "@medusajs/modules-sdk" -import { MikroOrmWrapper } from "../../../utils" import { createPaymentCollections, - createPaymentSessions, createPayments, + createPaymentSessions, } from "../../../__fixtures__" -import { getInitModuleConfig } from "../../../utils/get-init-module-config" +import { + moduleIntegrationTestRunner, + SuiteOptions, +} from "medusa-test-utils/dist" jest.setTimeout(30000) -describe("Payment Module Service", () => { - describe("Payment Flow", () => { - let service: IPaymentModuleService - let shutdownFunc: () => Promise +moduleIntegrationTestRunner({ + moduleName: Modules.PAYMENT, + testSuite: ({ + MikroOrmWrapper, + service, + }: SuiteOptions) => { + describe("Payment Module Service", () => { + describe("Payment Flow", () => { + beforeEach(async () => { + const repositoryManager = MikroOrmWrapper.forkManager() - afterAll(async () => { - await shutdownFunc() - }) + await createPaymentCollections(repositoryManager) + await createPaymentSessions(repositoryManager) + await createPayments(repositoryManager) + }) - beforeEach(async () => { - await MikroOrmWrapper.setupDatabase() - const repositoryManager = await MikroOrmWrapper.forkManager() - - const initModulesConfig = getInitModuleConfig() - const { medusaApp, shutdown } = await initModules(initModulesConfig) - service = medusaApp.modules[Modules.PAYMENT] - - shutdownFunc = shutdown - - await createPaymentCollections(repositoryManager) - await createPaymentSessions(repositoryManager) - await createPayments(repositoryManager) - }) - - afterEach(async () => { - await MikroOrmWrapper.clearDatabase() - await shutdownFunc() - }) - it("complete payment flow successfully", async () => { - let paymentCollection = await service.createPaymentCollections({ - currency_code: "USD", - amount: 200, - region_id: "reg_123", - }) - - const paymentSession = await service.createPaymentSession( - paymentCollection.id, - { - provider_id: "system", - providerContext: { - amount: 200, + it("complete payment flow successfully", async () => { + let paymentCollection = await service.createPaymentCollections({ currency_code: "USD", - payment_session_data: {}, - context: {}, - customer: {}, - billing_address: {}, - email: "test@test.test.com", - resource_id: "cart_test", - }, - } - ) + amount: 200, + region_id: "reg_123", + }) - const payment = await service.authorizePaymentSession( - paymentSession.id, - {} - ) + const paymentSession = await service.createPaymentSession( + paymentCollection.id, + { + provider_id: "system", + providerContext: { + amount: 200, + currency_code: "USD", + payment_session_data: {}, + context: {}, + customer: {}, + billing_address: {}, + email: "test@test.test.com", + resource_id: "cart_test", + }, + } + ) - await service.capturePayment({ - amount: 200, - payment_id: payment.id, - }) + const payment = await service.authorizePaymentSession( + paymentSession.id, + {} + ) - await service.completePaymentCollections(paymentCollection.id) + await service.capturePayment({ + amount: 200, + payment_id: payment.id, + }) - paymentCollection = await service.retrievePaymentCollection( - paymentCollection.id, - { relations: ["payment_sessions", "payments.captures"] } - ) + await service.completePaymentCollections(paymentCollection.id) - expect(paymentCollection).toEqual( - expect.objectContaining({ - id: expect.any(String), - currency_code: "USD", - amount: 200, - // TODO - // authorized_amount: 200, - // status: "authorized", - region_id: "reg_123", - deleted_at: null, - completed_at: expect.any(Date), - payment_sessions: [ + paymentCollection = await service.retrievePaymentCollection( + paymentCollection.id, + { relations: ["payment_sessions", "payments.captures"] } + ) + + expect(paymentCollection).toEqual( expect.objectContaining({ id: expect.any(String), currency_code: "USD", amount: 200, - provider_id: "system", - status: "authorized", - authorized_at: expect.any(Date), - }), - ], - payments: [ - expect.objectContaining({ - id: expect.any(String), - amount: 200, - currency_code: "USD", - provider_id: "system", - captures: [ + // TODO + // authorized_amount: 200, + // status: "authorized", + region_id: "reg_123", + deleted_at: null, + completed_at: expect.any(Date), + payment_sessions: [ expect.objectContaining({ + id: expect.any(String), + currency_code: "USD", amount: 200, + provider_id: "system", + status: "authorized", + authorized_at: expect.any(Date), }), ], - }), - ], + payments: [ + expect.objectContaining({ + id: expect.any(String), + amount: 200, + currency_code: "USD", + provider_id: "system", + captures: [ + expect.objectContaining({ + amount: 200, + }), + ], + }), + ], + }) + ) }) - ) - }) - }) - - describe("PaymentCollection", () => { - let service: IPaymentModuleService - let shutdownFunc: () => Promise - - afterAll(async () => { - await shutdownFunc() - }) - - beforeEach(async () => { - await MikroOrmWrapper.setupDatabase() - const repositoryManager = await MikroOrmWrapper.forkManager() - - const initModulesConfig = getInitModuleConfig() - const { medusaApp, shutdown } = await initModules(initModulesConfig) - service = medusaApp.modules[Modules.PAYMENT] - - shutdownFunc = shutdown - - await createPaymentCollections(repositoryManager) - await createPaymentSessions(repositoryManager) - await createPayments(repositoryManager) - }) - - afterEach(async () => { - await MikroOrmWrapper.clearDatabase() - await shutdownFunc() - }) - - describe("create", () => { - it("should throw an error when required params are not passed", async () => { - let error = await service - .createPaymentCollections([ - { - amount: 200, - region_id: "req_123", - } as any, - ]) - .catch((e) => e) - - expect(error.message).toContain( - "Value for PaymentCollection.currency_code is required, 'undefined' found" - ) - - error = await service - .createPaymentCollections([ - { - currency_code: "USD", - region_id: "req_123", - } as any, - ]) - .catch((e) => e) - - expect(error.message).toContain( - "Value for PaymentCollection.amount is required, 'undefined' found" - ) - - error = await service - .createPaymentCollections([ - { - currency_code: "USD", - amount: 200, - } as any, - ]) - .catch((e) => e) - - expect(error.message).toContain( - "Value for PaymentCollection.region_id is required, 'undefined' found" - ) }) - it("should create a payment collection successfully", async () => { - const [createdPaymentCollection] = - await service.createPaymentCollections([ - { currency_code: "USD", amount: 200, region_id: "reg_123" }, - ]) + describe("PaymentCollection", () => { + beforeEach(async () => { + const repositoryManager = await MikroOrmWrapper.forkManager() - expect(createdPaymentCollection).toEqual( - expect.objectContaining({ - id: expect.any(String), - status: "not_paid", - payment_providers: [], - payment_sessions: [], - payments: [], - currency_code: "USD", - amount: 200, + await createPaymentCollections(repositoryManager) + await createPaymentSessions(repositoryManager) + await createPayments(repositoryManager) + }) + + describe("create", () => { + it("should throw an error when required params are not passed", async () => { + let error = await service + .createPaymentCollections([ + { + amount: 200, + region_id: "req_123", + } as any, + ]) + .catch((e) => e) + + expect(error.message).toContain( + "Value for PaymentCollection.currency_code is required, 'undefined' found" + ) + + error = await service + .createPaymentCollections([ + { + currency_code: "USD", + region_id: "req_123", + } as any, + ]) + .catch((e) => e) + + expect(error.message).toContain( + "Value for PaymentCollection.amount is required, 'undefined' found" + ) + + error = await service + .createPaymentCollections([ + { + currency_code: "USD", + amount: 200, + } as any, + ]) + .catch((e) => e) + + expect(error.message).toContain( + "Value for PaymentCollection.region_id is required, 'undefined' found" + ) }) - ) - }) - }) - describe("delete", () => { - it("should delete a Payment Collection", async () => { - let collection = await service.listPaymentCollections({ - id: ["pay-col-id-1"], - }) + it("should create a payment collection successfully", async () => { + const [createdPaymentCollection] = + await service.createPaymentCollections([ + { currency_code: "USD", amount: 200, region_id: "reg_123" }, + ]) - expect(collection.length).toEqual(1) - - await service.deletePaymentCollections(["pay-col-id-1"]) - - collection = await service.listPaymentCollections({ - id: ["pay-col-id-1"], - }) - - expect(collection.length).toEqual(0) - }) - }) - - describe("retrieve", () => { - it("should retrieve a Payment Collection", async () => { - let collection = await service.retrievePaymentCollection("pay-col-id-2") - - expect(collection).toEqual( - expect.objectContaining({ - id: "pay-col-id-2", - amount: 200, - region_id: "region-id-1", - currency_code: "usd", - }) - ) - }) - - it("should fail to retrieve a non existent Payment Collection", async () => { - let error = await service - .retrievePaymentCollection("pay-col-id-not-exists") - .catch((e) => e) - - expect(error.message).toContain( - "PaymentCollection with id: pay-col-id-not-exists was not found" - ) - }) - }) - - describe("list", () => { - it("should list and count Payment Collection", async () => { - let [collections, count] = - await service.listAndCountPaymentCollections() - - expect(count).toEqual(3) - - expect(collections).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "pay-col-id-1", - amount: 100, - region_id: "region-id-1", - currency_code: "usd", - }), - expect.objectContaining({ - id: "pay-col-id-2", - amount: 200, - region_id: "region-id-1", - currency_code: "usd", - }), - expect.objectContaining({ - id: "pay-col-id-3", - amount: 300, - region_id: "region-id-2", - currency_code: "usd", - }), - ]) - ) - }) - - it("should list Payment Collections by region_id", async () => { - let collections = await service.listPaymentCollections( - { - region_id: "region-id-1", - }, - { select: ["id", "amount", "region_id"] } - ) - - expect(collections.length).toEqual(2) - - expect(collections).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: "pay-col-id-1", - amount: 100, - region_id: "region-id-1", - }), - expect.objectContaining({ - id: "pay-col-id-2", - amount: 200, - region_id: "region-id-1", - }), - ]) - ) - }) - }) - - describe("update", () => { - it("should update a Payment Collection", async () => { - await service.updatePaymentCollections({ - id: "pay-col-id-2", - currency_code: "eur", - region_id: "reg-2", - }) - - const collection = await service.retrievePaymentCollection( - "pay-col-id-2" - ) - - expect(collection).toEqual( - expect.objectContaining({ - id: "pay-col-id-2", - region_id: "reg-2", - currency_code: "eur", - }) - ) - }) - }) - - describe("complete", () => { - it("should complete a Payment Collection", async () => { - await service.completePaymentCollections("pay-col-id-1") - - const collection = await service.retrievePaymentCollection( - "pay-col-id-1" - ) - - expect(collection).toEqual( - expect.objectContaining({ - id: "pay-col-id-1", - completed_at: expect.any(Date), - }) - ) - }) - }) - }) - - describe("PaymentSession", () => { - let service: IPaymentModuleService - let shutdownFunc: () => Promise - - afterAll(async () => { - await shutdownFunc() - }) - - beforeEach(async () => { - await MikroOrmWrapper.setupDatabase() - const repositoryManager = await MikroOrmWrapper.forkManager() - - const initModulesConfig = getInitModuleConfig() - const { medusaApp, shutdown } = await initModules(initModulesConfig) - service = medusaApp.modules[Modules.PAYMENT] - - shutdownFunc = shutdown - - await createPaymentCollections(repositoryManager) - await createPaymentSessions(repositoryManager) - await createPayments(repositoryManager) - }) - - afterEach(async () => { - await MikroOrmWrapper.clearDatabase() - await shutdownFunc() - }) - - describe("create", () => { - it("should create a payment session successfully", async () => { - await service.createPaymentSession("pay-col-id-1", { - provider_id: "system", - providerContext: { - amount: 200, - currency_code: "usd", - payment_session_data: {}, - context: {}, - customer: {}, - billing_address: {}, - email: "test@test.test.com", - resource_id: "cart_test", - }, - }) - - const paymentCollection = await service.retrievePaymentCollection( - "pay-col-id-1", - { relations: ["payment_sessions"] } - ) - - expect(paymentCollection).toEqual( - expect.objectContaining({ - id: "pay-col-id-1", - status: "not_paid", - payment_sessions: expect.arrayContaining([ + expect(createdPaymentCollection).toEqual( expect.objectContaining({ id: expect.any(String), - data: {}, - status: "pending", - authorized_at: null, - currency_code: "usd", + status: "not_paid", + payment_providers: [], + payment_sessions: [], + payments: [], + currency_code: "USD", amount: 200, - provider_id: "system", - }), - ]), + }) + ) }) - ) - }) - }) - - describe("update", () => { - it("should update a payment session successfully", async () => { - let session = await service.createPaymentSession("pay-col-id-1", { - provider_id: "system", - providerContext: { - amount: 200, - currency_code: "usd", - payment_session_data: {}, - context: {}, - customer: {}, - billing_address: {}, - email: "test@test.test.com", - resource_id: "cart_test", - }, }) - session = await service.updatePaymentSession({ - id: session.id, - providerContext: { - amount: 200, - currency_code: "eur", - resource_id: "res_id", - context: {}, - customer: {}, - billing_address: {}, - email: "new@test.tsst", - payment_session_data: {}, - }, - }) + describe("delete", () => { + it("should delete a Payment Collection", async () => { + let collection = await service.listPaymentCollections({ + id: ["pay-col-id-1"], + }) - expect(session).toEqual( - expect.objectContaining({ - id: expect.any(String), - status: "pending", - currency_code: "eur", - amount: 200, + expect(collection.length).toEqual(1) + + await service.deletePaymentCollections(["pay-col-id-1"]) + + collection = await service.listPaymentCollections({ + id: ["pay-col-id-1"], + }) + + expect(collection.length).toEqual(0) }) - ) + }) + + describe("retrieve", () => { + it("should retrieve a Payment Collection", async () => { + let collection = await service.retrievePaymentCollection( + "pay-col-id-2" + ) + + expect(collection).toEqual( + expect.objectContaining({ + id: "pay-col-id-2", + amount: 200, + region_id: "region-id-1", + currency_code: "usd", + }) + ) + }) + + it("should fail to retrieve a non existent Payment Collection", async () => { + let error = await service + .retrievePaymentCollection("pay-col-id-not-exists") + .catch((e) => e) + + expect(error.message).toContain( + "PaymentCollection with id: pay-col-id-not-exists was not found" + ) + }) + }) + + describe("list", () => { + it("should list and count Payment Collection", async () => { + let [collections, count] = + await service.listAndCountPaymentCollections() + + expect(count).toEqual(3) + + expect(collections).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "pay-col-id-1", + amount: 100, + region_id: "region-id-1", + currency_code: "usd", + }), + expect.objectContaining({ + id: "pay-col-id-2", + amount: 200, + region_id: "region-id-1", + currency_code: "usd", + }), + expect.objectContaining({ + id: "pay-col-id-3", + amount: 300, + region_id: "region-id-2", + currency_code: "usd", + }), + ]) + ) + }) + + it("should list Payment Collections by region_id", async () => { + let collections = await service.listPaymentCollections( + { + region_id: "region-id-1", + }, + { select: ["id", "amount", "region_id"] } + ) + + expect(collections.length).toEqual(2) + + expect(collections).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "pay-col-id-1", + amount: 100, + region_id: "region-id-1", + }), + expect.objectContaining({ + id: "pay-col-id-2", + amount: 200, + region_id: "region-id-1", + }), + ]) + ) + }) + }) + + describe("update", () => { + it("should update a Payment Collection", async () => { + await service.updatePaymentCollections({ + id: "pay-col-id-2", + currency_code: "eur", + region_id: "reg-2", + }) + + const collection = await service.retrievePaymentCollection( + "pay-col-id-2" + ) + + expect(collection).toEqual( + expect.objectContaining({ + id: "pay-col-id-2", + region_id: "reg-2", + currency_code: "eur", + }) + ) + }) + }) + + describe("complete", () => { + it("should complete a Payment Collection", async () => { + await service.completePaymentCollections("pay-col-id-1") + + const collection = await service.retrievePaymentCollection( + "pay-col-id-1" + ) + + expect(collection).toEqual( + expect.objectContaining({ + id: "pay-col-id-1", + completed_at: expect.any(Date), + }) + ) + }) + }) }) - }) - describe("authorize", () => { - it("should authorize a payment session", async () => { - const collection = await service.createPaymentCollections({ - amount: 200, - region_id: "test-region", - currency_code: "usd", + describe("PaymentSession", () => { + beforeEach(async () => { + const repositoryManager = await MikroOrmWrapper.forkManager() + + await createPaymentCollections(repositoryManager) + await createPaymentSessions(repositoryManager) + await createPayments(repositoryManager) }) - const session = await service.createPaymentSession(collection.id, { - provider_id: "system", - providerContext: { - amount: 100, - currency_code: "usd", - payment_session_data: {}, - context: {}, - resource_id: "test", - email: "test@test.com", - billing_address: {}, - customer: {}, - }, - }) - - const payment = await service.authorizePaymentSession(session.id, {}) - - expect(payment).toEqual( - expect.objectContaining({ - id: expect.any(String), - amount: 100, - authorized_amount: 100, - currency_code: "usd", - provider_id: "system", - - refunds: [], - captures: [], - data: {}, - cart_id: null, - order_id: null, - order_edit_id: null, - customer_id: null, - deleted_at: null, - captured_at: null, - canceled_at: null, - payment_collection: expect.objectContaining({ - id: expect.any(String), - }), - payment_session: { - id: expect.any(String), - currency_code: "usd", - amount: 100, + describe("create", () => { + it("should create a payment session successfully", async () => { + await service.createPaymentSession("pay-col-id-1", { provider_id: "system", - data: {}, - status: "authorized", - authorized_at: expect.any(Date), - payment_collection: expect.objectContaining({ + providerContext: { + amount: 200, + currency_code: "usd", + payment_session_data: {}, + context: {}, + customer: {}, + billing_address: {}, + email: "test@test.test.com", + resource_id: "cart_test", + }, + }) + + const paymentCollection = await service.retrievePaymentCollection( + "pay-col-id-1", + { relations: ["payment_sessions"] } + ) + + expect(paymentCollection).toEqual( + expect.objectContaining({ + id: "pay-col-id-1", + status: "not_paid", + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + data: {}, + status: "pending", + authorized_at: null, + currency_code: "usd", + amount: 200, + provider_id: "system", + }), + ]), + }) + ) + }) + }) + + describe("update", () => { + it("should update a payment session successfully", async () => { + let session = await service.createPaymentSession("pay-col-id-1", { + provider_id: "system", + providerContext: { + amount: 200, + currency_code: "usd", + payment_session_data: {}, + context: {}, + customer: {}, + billing_address: {}, + email: "test@test.test.com", + resource_id: "cart_test", + }, + }) + + session = await service.updatePaymentSession({ + id: session.id, + providerContext: { + amount: 200, + currency_code: "eur", + resource_id: "res_id", + context: {}, + customer: {}, + billing_address: {}, + email: "new@test.tsst", + payment_session_data: {}, + }, + }) + + expect(session).toEqual( + expect.objectContaining({ id: expect.any(String), - }), - payment: expect.objectContaining({ + status: "pending", + currency_code: "eur", + amount: 200, + }) + ) + }) + }) + + describe("authorize", () => { + it("should authorize a payment session", async () => { + const collection = await service.createPaymentCollections({ + amount: 200, + region_id: "test-region", + currency_code: "usd", + }) + + const session = await service.createPaymentSession(collection.id, { + provider_id: "system", + providerContext: { + amount: 100, + currency_code: "usd", + payment_session_data: {}, + context: {}, + resource_id: "test", + email: "test@test.com", + billing_address: {}, + customer: {}, + }, + }) + + const payment = await service.authorizePaymentSession( + session.id, + {} + ) + + expect(payment).toEqual( + expect.objectContaining({ + id: expect.any(String), + amount: 100, authorized_amount: 100, + currency_code: "usd", + provider_id: "system", + + refunds: [], + captures: [], + data: {}, cart_id: null, order_id: null, order_edit_id: null, customer_id: null, - data: {}, deleted_at: null, captured_at: null, canceled_at: null, - refunds: [], - captures: [], - amount: 100, - currency_code: "usd", - provider_id: "system", - }), - }, + payment_collection: expect.objectContaining({ + id: expect.any(String), + }), + payment_session: { + id: expect.any(String), + currency_code: "usd", + amount: 100, + raw_amount: { value: "100.00000000000000000", precision: 20 }, + provider_id: "system", + data: {}, + status: "authorized", + authorized_at: expect.any(Date), + payment_collection: expect.objectContaining({ + id: expect.any(String), + }), + payment: expect.objectContaining({ + authorized_amount: 100, + cart_id: null, + order_id: null, + order_edit_id: null, + customer_id: null, + data: {}, + deleted_at: null, + captured_at: null, + canceled_at: null, + refunds: [], + captures: [], + amount: 100, + currency_code: "usd", + provider_id: "system", + }), + }, + }) + ) }) - ) + }) }) - }) - }) - describe("Payment", () => { - let service: IPaymentModuleService - let shutdownFunc: () => Promise + describe("Payment", () => { + beforeEach(async () => { + const repositoryManager = await MikroOrmWrapper.forkManager() - afterAll(async () => { - await shutdownFunc() - }) - - beforeEach(async () => { - await MikroOrmWrapper.setupDatabase() - const repositoryManager = await MikroOrmWrapper.forkManager() - - const initModulesConfig = getInitModuleConfig() - const { medusaApp, shutdown } = await initModules(initModulesConfig) - service = medusaApp.modules[Modules.PAYMENT] - - shutdownFunc = shutdown - - await createPaymentCollections(repositoryManager) - await createPaymentSessions(repositoryManager) - await createPayments(repositoryManager) - }) - - afterEach(async () => { - await MikroOrmWrapper.clearDatabase() - await shutdownFunc() - }) - - describe("update", () => { - it("should update a payment successfully", async () => { - const updatedPayment = await service.updatePayment({ - id: "pay-id-1", - cart_id: "new-cart", + await createPaymentCollections(repositoryManager) + await createPaymentSessions(repositoryManager) + await createPayments(repositoryManager) }) - expect(updatedPayment).toEqual( - expect.objectContaining({ - id: "pay-id-1", - cart_id: "new-cart", - }) - ) - }) - }) + describe("update", () => { + it("should update a payment successfully", async () => { + const updatedPayment = await service.updatePayment({ + id: "pay-id-1", + cart_id: "new-cart", + }) - describe("capture", () => { - it("should capture a payment successfully", async () => { - const capturedPayment = await service.capturePayment({ - amount: 100, - payment_id: "pay-id-1", - }) - - expect(capturedPayment).toEqual( - expect.objectContaining({ - id: "pay-id-1", - amount: 100, - - captures: [ + expect(updatedPayment).toEqual( expect.objectContaining({ - created_by: null, - amount: 100, - }), - ], - - // TODO: uncomment when totals calculations are implemented - // captured_amount: 100, - // captured_at: expect.any(Date), + id: "pay-id-1", + cart_id: "new-cart", + }) + ) }) - ) - }) - - // TODO: uncomment when totals are implemented - - // it("should fail to capture amount greater than authorized", async () => { - // const error = await service - // .capturePayment({ - // amount: 200, - // payment_id: "pay-id-1", - // }) - // .catch((e) => e) - // - // expect(error.message).toEqual( - // "Total captured amount for payment: pay-id-1 exceeds authorised amount." - // ) - // }) - // - // it("should fail to capture already captured payment", async () => { - // await service.capturePayment({ - // amount: 100, - // payment_id: "pay-id-1", - // }) - // - // const error = await service - // .capturePayment({ - // amount: 100, - // payment_id: "pay-id-1", - // }) - // .catch((e) => e) - // - // expect(error.message).toEqual( - // "The payment: pay-id-1 is already fully captured." - // ) - // }) - // - // it("should fail to capture a canceled payment", async () => { - // await service.cancelPayment("pay-id-1") - // - // const error = await service - // .capturePayment({ - // amount: 100, - // payment_id: "pay-id-1", - // }) - // .catch((e) => e) - // - // expect(error.message).toEqual( - // "The payment: pay-id-1 has been canceled." - // ) - // }) - }) - - describe("refund", () => { - it("should refund a payments successfully", async () => { - await service.capturePayment({ - amount: 100, - payment_id: "pay-id-2", }) - const refundedPayment = await service.refundPayment({ - amount: 100, - payment_id: "pay-id-2", - }) + describe("capture", () => { + it("should capture a payment successfully", async () => { + const capturedPayment = await service.capturePayment({ + amount: 100, + payment_id: "pay-id-1", + }) - expect(refundedPayment).toEqual( - expect.objectContaining({ - id: "pay-id-2", - amount: 100, - refunds: [ + expect(capturedPayment).toEqual( expect.objectContaining({ - created_by: null, + id: "pay-id-1", amount: 100, - }), - ], - // captured_amount: 100, - // refunded_amount: 100, + + captures: [ + expect.objectContaining({ + created_by: null, + amount: 100, + }), + ], + + // TODO: uncomment when totals calculations are implemented + // captured_amount: 100, + // captured_at: expect.any(Date), + }) + ) }) - ) - }) - // it("should throw if refund is greater than captured amount", async () => { - // await service.capturePayment({ - // amount: 50, - // payment_id: "pay-id-1", - // }) - // - // const error = await service - // .refundPayment({ - // amount: 100, - // payment_id: "pay-id-1", - // }) - // .catch((e) => e) - // - // expect(error.message).toEqual( - // "Refund amount for payment: pay-id-1 cannot be greater than the amount captured on the payment." - // ) - // }) - }) + // TODO: uncomment when totals are implemented - describe("cancel", () => { - it("should cancel a payment", async () => { - const payment = await service.cancelPayment("pay-id-2") + // it("should fail to capture amount greater than authorized", async () => { + // const error = await service + // .capturePayment({ + // amount: 200, + // payment_id: "pay-id-1", + // }) + // .catch((e) => e) + // + // expect(error.message).toEqual( + // "Total captured amount for payment: pay-id-1 exceeds authorised amount." + // ) + // }) + // + // it("should fail to capture already captured payment", async () => { + // await service.capturePayment({ + // amount: 100, + // payment_id: "pay-id-1", + // }) + // + // const error = await service + // .capturePayment({ + // amount: 100, + // payment_id: "pay-id-1", + // }) + // .catch((e) => e) + // + // expect(error.message).toEqual( + // "The payment: pay-id-1 is already fully captured." + // ) + // }) + // + // it("should fail to capture a canceled payment", async () => { + // await service.cancelPayment("pay-id-1") + // + // const error = await service + // .capturePayment({ + // amount: 100, + // payment_id: "pay-id-1", + // }) + // .catch((e) => e) + // + // expect(error.message).toEqual( + // "The payment: pay-id-1 has been canceled." + // ) + // }) + }) - expect(payment).toEqual( - expect.objectContaining({ - id: "pay-id-2", - canceled_at: expect.any(Date), + describe("refund", () => { + it("should refund a payments successfully", async () => { + await service.capturePayment({ + amount: 100, + payment_id: "pay-id-2", + }) + + const refundedPayment = await service.refundPayment({ + amount: 100, + payment_id: "pay-id-2", + }) + + expect(refundedPayment).toEqual( + expect.objectContaining({ + id: "pay-id-2", + amount: 100, + refunds: [ + expect.objectContaining({ + created_by: null, + amount: 100, + }), + ], + // captured_amount: 100, + // refunded_amount: 100, + }) + ) }) - ) - }) - // TODO: revisit when totals are implemented - // it("should throw if trying to cancel a captured payment", async () => { - // await service.capturePayment({ payment_id: "pay-id-2", amount: 100 }) - // - // const error = await service - // .cancelPayment("pay-id-2") - // .catch((e) => e.message) - // - // expect(error).toEqual( - // "Cannot cancel a payment: pay-id-2 that has been captured." - // ) - // }) + // it("should throw if refund is greater than captured amount", async () => { + // await service.capturePayment({ + // amount: 50, + // payment_id: "pay-id-1", + // }) + // + // const error = await service + // .refundPayment({ + // amount: 100, + // payment_id: "pay-id-1", + // }) + // .catch((e) => e) + // + // expect(error.message).toEqual( + // "Refund amount for payment: pay-id-1 cannot be greater than the amount captured on the payment." + // ) + // }) + }) + + describe("cancel", () => { + it("should cancel a payment", async () => { + const payment = await service.cancelPayment("pay-id-2") + + expect(payment).toEqual( + expect.objectContaining({ + id: "pay-id-2", + canceled_at: expect.any(Date), + }) + ) + }) + + // TODO: revisit when totals are implemented + // it("should throw if trying to cancel a captured payment", async () => { + // await service.capturePayment({ payment_id: "pay-id-2", amount: 100 }) + // + // const error = await service + // .cancelPayment("pay-id-2") + // .catch((e) => e.message) + // + // expect(error).toEqual( + // "Cannot cancel a payment: pay-id-2 that has been captured." + // ) + // }) + }) + }) }) - }) + }, }) diff --git a/packages/payment/src/models/payment-session.ts b/packages/payment/src/models/payment-session.ts index c0f8230185..cfd1e0dff3 100644 --- a/packages/payment/src/models/payment-session.ts +++ b/packages/payment/src/models/payment-session.ts @@ -9,7 +9,13 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" -import { generateEntityId, PaymentSessionStatus } from "@medusajs/utils" +import { + BigNumber, + generateEntityId, + MikroOrmBigNumberProperty, + PaymentSessionStatus, +} from "@medusajs/utils" +import { BigNumberRawValue } from "@medusajs/types" import PaymentCollection from "./payment-collection" import Payment from "./payment" @@ -24,11 +30,13 @@ export default class PaymentSession { @Property({ columnType: "text" }) currency_code: string + @MikroOrmBigNumberProperty() + amount: BigNumber | number + @Property({ - columnType: "numeric", - serializer: Number, + columnType: "jsonb", }) - amount: number + raw_amount: BigNumberRawValue @Property({ columnType: "text" }) provider_id: string diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index 2fd636dce6..701550aa4b 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -3,7 +3,7 @@ module.exports = { "^.+\\.[jt]s?$": [ "ts-jest", { - tsConfig: "tsconfig.json", + tsConfig: "tsconfig.spec.json", isolatedModules: true, }, ], diff --git a/packages/utils/src/dal/mikro-orm/__tests__/big-number-field.spec.ts b/packages/utils/src/dal/mikro-orm/__tests__/big-number-field.spec.ts new file mode 100644 index 0000000000..52d8613532 --- /dev/null +++ b/packages/utils/src/dal/mikro-orm/__tests__/big-number-field.spec.ts @@ -0,0 +1,47 @@ +import { MikroOrmBigNumberProperty } from "../big-number-field" +import { BigNumberRawValue } from "@medusajs/types" +import { BigNumber } from "../../../totals/big-number" + +describe("@MikroOrmBigNumberProperty", () => { + it("should correctly assign and update BigNumber values", () => { + class TestAmount { + @MikroOrmBigNumberProperty() + amount: BigNumber | number + + raw_amount: BigNumberRawValue + } + + const testAmount = new TestAmount() + + expect(testAmount.amount).toBeUndefined() + expect(testAmount.raw_amount).toBeUndefined() + + testAmount.amount = 100 + + expect(testAmount.amount).toEqual(100) + expect((testAmount as any).amount_).toEqual(100) + expect(testAmount.raw_amount).toEqual({ + value: "100.00000000000000000", + precision: 20, + }) + + // Update the amount + + testAmount.amount = 200 + + expect(testAmount.amount).toEqual(200) + expect((testAmount as any).amount_).toEqual(200) + expect(testAmount.raw_amount).toEqual({ + value: "200.00000000000000000", + precision: 20, + }) + + // Update with big number + + testAmount.amount = new BigNumber(300, { precision: 5 }) + + expect(testAmount.amount).toEqual(300) + expect((testAmount as any).amount_).toEqual(300) + expect(testAmount.raw_amount).toEqual({ value: "300.00", precision: 5 }) + }) +}) diff --git a/packages/utils/src/dal/mikro-orm/big-number-field.ts b/packages/utils/src/dal/mikro-orm/big-number-field.ts index 940aedba01..d58b0ba3e6 100644 --- a/packages/utils/src/dal/mikro-orm/big-number-field.ts +++ b/packages/utils/src/dal/mikro-orm/big-number-field.ts @@ -1,78 +1,54 @@ import { BigNumber } from "../../totals/big-number" +import { Property } from "@mikro-orm/core" +import { BigNumberInput } from "@medusajs/types" -const bigNumberFields = new WeakMap< - object, - { prop: string; options: { nullable?: boolean } }[] ->() +export function MikroOrmBigNumberProperty( + options: Parameters[0] & { + rawColumnName?: string + } = {} +) { + return function (target: any, columnName: string) { + const targetColumn = columnName + "_" + const rawColumnName = options.rawColumnName ?? `raw_${columnName}` -export function BigNumberField(options: { nullable?: boolean } = {}) { - return function (target: any, prop: string) { - const entity = target.constructor - if (!bigNumberFields.has(entity)) { - bigNumberFields.set(entity, []) - } + Object.defineProperty(target, columnName, { + get() { + return this[targetColumn] + }, + set(value: BigNumberInput) { + let bigNumber: BigNumber + if (value instanceof BigNumber) { + bigNumber = value + } else if (this[rawColumnName]) { + const precision = this[rawColumnName].precision + this[rawColumnName].value = new BigNumber(value, { + precision, + }).raw!.value + bigNumber = new BigNumber(this[rawColumnName]) + } else { + bigNumber = new BigNumber(value) + } - if (prop.startsWith("raw_")) { - const suggestedPropName = prop.replace("raw_", "") - throw new Error( - `BigNumberField decorator has to be used on the property "${suggestedPropName}" and ${prop} typed as BigNumberRawValue.` - ) - } + this[targetColumn] = bigNumber.numeric + this[rawColumnName] = bigNumber.raw + }, + }) - bigNumberFields.get(entity)?.push({ prop, options }) + Property({ + type: "number", + columnType: "numeric", + fieldName: columnName, + serializer: () => { + return undefined + }, + ...options, + })(target, targetColumn) - if (!entity.prototype.__bigNumberInitialized) { - entity.prototype.__bigNumberInitialized = true - - registerGlobalHook(entity) - } - } -} - -function registerGlobalHook(entity: any) { - const originalOnInit = entity.prototype.onInit - const originalOnCreate = entity.prototype.onCreate - const originalOnUpdate = entity.prototype.onUpdate - - entity.prototype.onInit = function (...args: any[]) { - initializeBigNumberFields(this) - - if (originalOnInit) { - originalOnInit.apply(this, args) - } - } - - entity.prototype.onCreate = function (...args: any[]) { - initializeBigNumberFields(this) - - if (originalOnCreate) { - originalOnCreate.apply(this, args) - } - } - - entity.prototype.onUpdate = function (...args: any[]) { - initializeBigNumberFields(this) - - if (originalOnUpdate) { - originalOnUpdate.apply(this, args) - } - } -} - -function initializeBigNumberFields(entity: any) { - const fields = bigNumberFields.get(entity.constructor) ?? [] - - for (const field of fields) { - const { prop, options } = field - - const rawValue = entity[`raw_${prop}`] - const value = entity[prop] - if (options.nullable && rawValue === null && value === null) { - return - } - - const val = new BigNumber(rawValue ?? value) - entity[prop] = val.numeric - entity[`raw_${prop}`] = val.raw + Property({ + type: "number", + persist: false, + getter: true, + setter: true, + })(target, columnName) } } diff --git a/packages/utils/src/totals/big-number.ts b/packages/utils/src/totals/big-number.ts index c7a39a4e08..16ea8b6f36 100644 --- a/packages/utils/src/totals/big-number.ts +++ b/packages/utils/src/totals/big-number.ts @@ -8,11 +8,16 @@ export class BigNumber { private numeric_: number private raw_?: BigNumberRawValue - constructor(rawPrice: BigNumberInput) { - this.setRawPriceOrThrow(rawPrice) + constructor(rawPrice: BigNumberInput, options?: { precision?: number }) { + this.setRawPriceOrThrow(rawPrice, options) } - setRawPriceOrThrow(rawPrice: BigNumberInput) { + setRawPriceOrThrow( + rawPrice: BigNumberInput, + { precision }: { precision?: number } = {} + ) { + precision ??= BigNumber.DEFAULT_PRECISION + if (BigNumberJS.isBigNumber(rawPrice)) { /** * Example: @@ -21,7 +26,8 @@ export class BigNumber { */ this.numeric_ = rawPrice.toNumber() this.raw_ = { - value: rawPrice.toPrecision(BigNumber.DEFAULT_PRECISION), + value: rawPrice.toPrecision(precision), + precision, } } else if (isString(rawPrice)) { /** @@ -31,7 +37,8 @@ export class BigNumber { this.numeric_ = bigNum.toNumber() this.raw_ = this.raw_ = { - value: bigNum.toPrecision(BigNumber.DEFAULT_PRECISION), + value: bigNum.toPrecision(precision), + precision, } } else if (isBigNumber(rawPrice)) { /** @@ -41,6 +48,7 @@ export class BigNumber { this.raw_ = { ...rawPrice, + precision, } } else if (typeof rawPrice === `number` && !Number.isNaN(rawPrice)) { /** @@ -49,7 +57,8 @@ export class BigNumber { this.numeric_ = rawPrice as number this.raw_ = { - value: BigNumberJS(rawPrice as number).toString(), + value: BigNumberJS(rawPrice as number).toPrecision(precision), + precision, } } else { throw new Error( diff --git a/packages/utils/tsconfig.spec.json b/packages/utils/tsconfig.spec.json new file mode 100644 index 0000000000..bbf7d8cd9a --- /dev/null +++ b/packages/utils/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } +}