From d47e9464968c00539172f12cc5597b42dbdbbf7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:04:46 +0100 Subject: [PATCH] feat(payment): PaymentCollection CRUD (#6124) --- .../__fixtures__/payment-collection/data.ts | 20 ++ .../__fixtures__/payment-collection/index.ts | 22 ++ .../services/payment-module/index.spec.ts | 212 +++++++++++++++ packages/payment/src/initialize/index.ts | 7 +- packages/payment/src/joiner-config.ts | 3 +- packages/payment/src/services/index.ts | 1 + .../src/services/payment-collection.ts | 26 ++ .../payment/src/services/payment-module.ts | 253 +++++++++++++++++- packages/payment/src/types/repositories.ts | 18 ++ packages/types/src/payment/common.ts | 28 ++ packages/types/src/payment/mutations.ts | 26 +- 11 files changed, 606 insertions(+), 10 deletions(-) create mode 100644 packages/payment/integration-tests/__fixtures__/payment-collection/data.ts create mode 100644 packages/payment/integration-tests/__fixtures__/payment-collection/index.ts create mode 100644 packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts create mode 100644 packages/payment/src/services/payment-collection.ts create mode 100644 packages/payment/src/types/repositories.ts diff --git a/packages/payment/integration-tests/__fixtures__/payment-collection/data.ts b/packages/payment/integration-tests/__fixtures__/payment-collection/data.ts new file mode 100644 index 0000000000..692d57bc3e --- /dev/null +++ b/packages/payment/integration-tests/__fixtures__/payment-collection/data.ts @@ -0,0 +1,20 @@ +export const defaultPaymentCollectionData = [ + { + id: "pay-col-id-1", + amount: 100, + region_id: "region-id-1", + currency_code: "usd", + }, + { + id: "pay-col-id-2", + amount: 200, + region_id: "region-id-1", + currency_code: "usd", + }, + { + id: "pay-col-id-3", + amount: 300, + region_id: "region-id-2", + currency_code: "usd", + }, +] diff --git a/packages/payment/integration-tests/__fixtures__/payment-collection/index.ts b/packages/payment/integration-tests/__fixtures__/payment-collection/index.ts new file mode 100644 index 0000000000..3700ffd5df --- /dev/null +++ b/packages/payment/integration-tests/__fixtures__/payment-collection/index.ts @@ -0,0 +1,22 @@ +import { CreatePaymentCollectionDTO } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { PaymentCollection } from "../../../src/models" +import { defaultPaymentCollectionData } from "./data" + +export * from "./data" + +export async function createPaymentCollections( + manager: SqlEntityManager, + paymentCollectionData: CreatePaymentCollectionDTO[] = defaultPaymentCollectionData +): Promise { + const collections: PaymentCollection[] = [] + + for (let data of paymentCollectionData) { + let collection = manager.create(PaymentCollection, data) + + await manager.persistAndFlush(collection) + } + + return collections +} 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 new file mode 100644 index 0000000000..98ff66df31 --- /dev/null +++ b/packages/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -0,0 +1,212 @@ +import { IPaymentModuleService } from "@medusajs/types" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { initialize } from "../../../../src/initialize" +import { DB_URL, MikroOrmWrapper } from "../../../utils" +import { createPaymentCollections } from "../../../__fixtures__/payment-collection" + +jest.setTimeout(30000) + +describe("Payment Module Service", () => { + let service: IPaymentModuleService + let repositoryManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PAYMNET_DB_SCHEMA, + }, + }) + + await createPaymentCollections(repositoryManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("create", () => { + it("should throw an error when required params are not passed", async () => { + let error = await service + .createPaymentCollection([ + { + 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 + .createPaymentCollection([ + { + 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 + .createPaymentCollection([ + { + 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.createPaymentCollection([ + { currency_code: "USD", amount: 200, region_id: "reg_123" }, + ]) + + expect(createdPaymentCollection).toEqual( + expect.objectContaining({ + id: expect.any(String), + status: "not_paid", + payment_providers: [], + payment_sessions: [], + payments: [], + currency_code: "USD", + amount: 200, + }) + ) + }) + }) + + describe("delete", () => { + it("should delete a Payment Collection", async () => { + let collection = await service.listPaymentCollections({ + id: ["pay-col-id-1"], + }) + + expect(collection.length).toEqual(1) + + await service.deletePaymentCollection(["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.updatePaymentCollection({ + id: "pay-col-id-2", + currency_code: "eur", + authorized_amount: 200, + }) + + const collection = await service.retrievePaymentCollection("pay-col-id-2") + + expect(collection).toEqual( + expect.objectContaining({ + id: "pay-col-id-2", + authorized_amount: 200, + currency_code: "eur", + }) + ) + }) + }) +}) diff --git a/packages/payment/src/initialize/index.ts b/packages/payment/src/initialize/index.ts index 37830dcf8a..28208d3db3 100644 --- a/packages/payment/src/initialize/index.ts +++ b/packages/payment/src/initialize/index.ts @@ -6,11 +6,16 @@ import { Modules, } from "@medusajs/modules-sdk" import { IPaymentModuleService, ModulesSdkTypes } from "@medusajs/types" + import { moduleDefinition } from "../module-definition" import { InitializeModuleInjectableDependencies } from "../types" export const initialize = async ( - options?: ModulesSdkTypes.ModuleBootstrapDeclaration, + options?: + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + | ExternalModuleDeclaration + | InternalModuleDeclaration, injectedDependencies?: InitializeModuleInjectableDependencies ): Promise => { const loaded = await MedusaModule.bootstrap({ diff --git a/packages/payment/src/joiner-config.ts b/packages/payment/src/joiner-config.ts index a04381a6ef..4bffc28e73 100644 --- a/packages/payment/src/joiner-config.ts +++ b/packages/payment/src/joiner-config.ts @@ -1,10 +1,11 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" import { MapToConfig } from "@medusajs/utils" -import { Payment } from "@models" +import { Payment, PaymentCollection } from "@models" export const LinkableKeys = { payment_id: Payment.name, + payment_collection_id: PaymentCollection.name, } const entityLinkableKeysMap: MapToConfig = {} diff --git a/packages/payment/src/services/index.ts b/packages/payment/src/services/index.ts index a4f5ea37ae..01f593ba08 100644 --- a/packages/payment/src/services/index.ts +++ b/packages/payment/src/services/index.ts @@ -1 +1,2 @@ export { default as PaymentModuleService } from "./payment-module" +export { default as PaymentCollectionService } from "./payment-collection" diff --git a/packages/payment/src/services/payment-collection.ts b/packages/payment/src/services/payment-collection.ts new file mode 100644 index 0000000000..abeef3e662 --- /dev/null +++ b/packages/payment/src/services/payment-collection.ts @@ -0,0 +1,26 @@ +import { PaymentCollection } from "@models" +import { + CreatePaymentCollectionDTO, + DAL, + UpdatePaymentCollectionDTO, +} from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" + +type InjectedDependencies = { + paymentCollectionRepository: DAL.RepositoryService +} + +export default class PaymentCollectionService< + TEntity extends PaymentCollection = PaymentCollection +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreatePaymentCollectionDTO + update: UpdatePaymentCollectionDTO + } +>(PaymentCollection) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/payment/src/services/payment-module.ts b/packages/payment/src/services/payment-module.ts index 9dc1a7a406..93d4a1310e 100644 --- a/packages/payment/src/services/payment-module.ts +++ b/packages/payment/src/services/payment-module.ts @@ -1,29 +1,274 @@ import { + Context, + CreatePaymentCollectionDTO, + CreatePaymentDTO, + CreatePaymentSessionDTO, DAL, + FilterablePaymentCollectionProps, + FindConfig, InternalModuleDeclaration, + IPaymentModuleService, ModuleJoinerConfig, + PaymentCollectionDTO, + PaymentDTO, + SetPaymentSessionsDTO, + UpdatePaymentCollectionDTO, + UpdatePaymentDTO, } from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + MedusaContext, +} from "@medusajs/utils" -import { Payment } from "@models" +import * as services from "@services" import { joinerConfig } from "../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService + paymentCollectionService: services.PaymentCollectionService } -// TODO: implement IPaymentModule -export default class PaymentModule { +export default class PaymentModuleService implements IPaymentModuleService { protected baseRepository_: DAL.RepositoryService + protected paymentCollectionService_: services.PaymentCollectionService constructor( - { baseRepository }: InjectedDependencies, + { baseRepository, paymentCollectionService }: InjectedDependencies, protected readonly moduleDeclaration: InternalModuleDeclaration ) { this.baseRepository_ = baseRepository + + this.paymentCollectionService_ = paymentCollectionService } __joinerConfig(): ModuleJoinerConfig { return joinerConfig } + + createPaymentCollection( + data: CreatePaymentCollectionDTO, + sharedContext?: Context + ): Promise + + createPaymentCollection( + data: CreatePaymentCollectionDTO[], + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async createPaymentCollection( + data: CreatePaymentCollectionDTO | CreatePaymentCollectionDTO[], + @MedusaContext() sharedContext?: Context + ): Promise { + const input = Array.isArray(data) ? data : [data] + + const collections = await this.paymentCollectionService_.create( + input, + sharedContext + ) + + return await this.baseRepository_.serialize( + Array.isArray(data) ? collections : collections[0], + { + populate: true, + } + ) + } + + updatePaymentCollection( + data: UpdatePaymentCollectionDTO[], + sharedContext?: Context + ): Promise + updatePaymentCollection( + data: UpdatePaymentCollectionDTO, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async updatePaymentCollection( + data: UpdatePaymentCollectionDTO | UpdatePaymentCollectionDTO[], + sharedContext?: Context + ): Promise { + const input = Array.isArray(data) ? data : [data] + const result = await this.paymentCollectionService_.update( + input, + sharedContext + ) + + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0], + { + populate: true, + } + ) + } + + deletePaymentCollection( + paymentCollectionId: string[], + sharedContext?: Context + ): Promise + deletePaymentCollection( + paymentCollectionId: string, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async deletePaymentCollection( + ids: string | string[], + @MedusaContext() sharedContext?: Context + ): Promise { + const paymentCollectionIds = Array.isArray(ids) ? ids : [ids] + await this.paymentCollectionService_.delete( + paymentCollectionIds, + sharedContext + ) + } + + @InjectManager("baseRepository_") + async retrievePaymentCollection( + paymentCollectionId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const paymentCollection = await this.paymentCollectionService_.retrieve( + paymentCollectionId, + config, + sharedContext + ) + + return await this.baseRepository_.serialize( + paymentCollection, + { populate: true } + ) + } + + @InjectManager("baseRepository_") + async listPaymentCollections( + filters: FilterablePaymentCollectionProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext?: Context + ): Promise { + const paymentCollections = await this.paymentCollectionService_.list( + filters, + config, + sharedContext + ) + + return await this.baseRepository_.serialize( + paymentCollections, + { populate: true } + ) + } + + @InjectManager("baseRepository_") + async listAndCountPaymentCollections( + filters: FilterablePaymentCollectionProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext?: Context + ): Promise<[PaymentCollectionDTO[], number]> { + const [paymentCollections, count] = + await this.paymentCollectionService_.listAndCount( + filters, + config, + sharedContext + ) + + return [ + await this.baseRepository_.serialize( + paymentCollections, + { populate: true } + ), + count, + ] + } + + /** + * TODO + */ + + authorizePaymentCollection( + paymentCollectionId: string, + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + completePaymentCollection( + paymentCollectionId: string, + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + createPayment(data: CreatePaymentDTO): Promise + createPayment(data: CreatePaymentDTO[]): Promise + createPayment(data: unknown): Promise { + throw new Error("Method not implemented.") + } + capturePayment( + paymentId: string, + amount: number, + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + refundPayment( + paymentId: string, + amount: number, + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + updatePayment( + data: UpdatePaymentDTO, + sharedContext?: Context | undefined + ): Promise + updatePayment( + data: UpdatePaymentDTO[], + sharedContext?: Context | undefined + ): Promise + updatePayment( + data: unknown, + sharedContext?: unknown + ): Promise { + throw new Error("Method not implemented.") + } + createPaymentSession( + paymentCollectionId: string, + data: CreatePaymentSessionDTO, + sharedContext?: Context | undefined + ): Promise + createPaymentSession( + paymentCollectionId: string, + data: CreatePaymentSessionDTO[], + sharedContext?: Context | undefined + ): Promise + createPaymentSession( + paymentCollectionId: unknown, + data: unknown, + sharedContext?: unknown + ): Promise { + throw new Error("Method not implemented.") + } + authorizePaymentSessions( + paymentCollectionId: string, + sessionIds: string[], + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + completePaymentSessions( + paymentCollectionId: string, + sessionIds: string[], + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } + setPaymentSessions( + paymentCollectionId: string, + data: SetPaymentSessionsDTO[], + sharedContext?: Context | undefined + ): Promise { + throw new Error("Method not implemented.") + } } diff --git a/packages/payment/src/types/repositories.ts b/packages/payment/src/types/repositories.ts new file mode 100644 index 0000000000..cc84166e9a --- /dev/null +++ b/packages/payment/src/types/repositories.ts @@ -0,0 +1,18 @@ +import { + DAL, + CreatePaymentCollectionDTO, + UpdatePaymentCollectionDTO, +} from "@medusajs/types" + +import { PaymentCollection } from "@models" + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IPaymentCollectionRepository< + TEntity extends PaymentCollection = PaymentCollection +> extends DAL.RepositoryService< + TEntity, + { + create: CreatePaymentCollectionDTO + update: UpdatePaymentCollectionDTO + } + > {} diff --git a/packages/types/src/payment/common.ts b/packages/types/src/payment/common.ts index 01788395ed..5f4b303bff 100644 --- a/packages/types/src/payment/common.ts +++ b/packages/types/src/payment/common.ts @@ -3,6 +3,34 @@ import { OperatorMap } from "../dal/utils" /* ********** PAYMENT COLLECTION ********** */ +/** + * @enum + * + * The payment collection's status. + */ +export enum PaymentCollectionStatus { + /** + * The payment collection isn't paid. + */ + NOT_PAID = "not_paid", + /** + * The payment collection is awaiting payment. + */ + AWAITING = "awaiting", + /** + * The payment collection is authorized. + */ + AUTHORIZED = "authorized", + /** + * Some of the payments in the payment collection are authorized. + */ + PARTIALLY_AUTHORIZED = "partially_authorized", + /** + * The payment collection is canceled. + */ + CANCELED = "canceled", +} + export interface PaymentCollectionDTO { /** * The ID of the Payment Collection diff --git a/packages/types/src/payment/mutations.ts b/packages/types/src/payment/mutations.ts index 35f72d81df..c5cfa3e9b9 100644 --- a/packages/types/src/payment/mutations.ts +++ b/packages/types/src/payment/mutations.ts @@ -1,15 +1,29 @@ -/** - * TODO - */ +import { PaymentCollectionStatus } from "./common" +/** + * Payment Collection + */ export interface CreatePaymentCollectionDTO { region_id: string currency_code: string amount: number + + metadata?: Record } export interface UpdatePaymentCollectionDTO - extends Partial {} + extends Partial { + id: string + + authorized_amount?: number + refunded_amount?: number + completed_at?: number + status?: PaymentCollectionStatus +} + +/** + * Payment + */ export interface CreatePaymentDTO { amount: number @@ -30,6 +44,10 @@ export interface UpdatePaymentDTO { customer_id?: string } +/** + * Payment Session + */ + export interface CreatePaymentSessionDTO { amount: number currency_code: string