From 01a879ac9483e7115b27f7c93e7d5ff5cb5f8a60 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Fri, 18 Nov 2022 11:23:35 -0300 Subject: [PATCH] chore: markAsAuthorized and payment collection tests (#2620) * chore: markAsAuthorized and payment collection tests --- .../api/__tests__/admin/payment-collection.js | 223 +++++++++++++ .../api/__tests__/admin/payment.js | 163 ++++++++++ .../api/__tests__/store/payment-collection.js | 305 ++++++++++++++++++ .../simple-payment-collection-factory.ts | 4 + .../resources/admin/payment-collections.ts | 8 + .../routes/admin/payment-collections/index.ts | 5 + .../mark-authorized-payment-collection.ts | 72 +++++ .../medusa/src/services/payment-collection.ts | 22 ++ 8 files changed, 802 insertions(+) create mode 100644 integration-tests/api/__tests__/admin/payment-collection.js create mode 100644 integration-tests/api/__tests__/admin/payment.js create mode 100644 integration-tests/api/__tests__/store/payment-collection.js create mode 100644 packages/medusa/src/api/routes/admin/payment-collections/mark-authorized-payment-collection.ts diff --git a/integration-tests/api/__tests__/admin/payment-collection.js b/integration-tests/api/__tests__/admin/payment-collection.js new file mode 100644 index 0000000000..aeb089c4cb --- /dev/null +++ b/integration-tests/api/__tests__/admin/payment-collection.js @@ -0,0 +1,223 @@ +const path = require("path") + +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../helpers/use-api") +const { useDb } = require("../../../helpers/use-db") +const adminSeeder = require("../../helpers/admin-seeder") + +const { + simplePaymentCollectionFactory, +} = require("../../factories/simple-payment-collection-factory") + +jest.setTimeout(30000) + +const adminHeaders = { + headers: { + Authorization: "Bearer test_token", + }, +} + +describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment-collections", () => { + let medusaProcess + let dbConnection + + let payCol = null + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + const [process, connection] = await startServerWithEnvironment({ + cwd, + env: { MEDUSA_FF_ORDER_EDITING: true }, + verbose: true, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("GET /admin/payment-collections/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("gets payment collection", async () => { + const api = useApi() + + const response = await api.get( + `/admin/payment-collections/${payCol.id}`, + adminHeaders + ) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + status: "not_paid", + description: "paycol description", + amount: 10000, + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("POST /admin/payment-collections/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("updates a payment collection", async () => { + const api = useApi() + + const response = await api.post( + `/admin/payment-collections/${payCol.id}`, + { + description: "new description", + metadata: { + a: 1, + b: [1, 2, "3"], + }, + }, + adminHeaders + ) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + description: "new description", + metadata: { + a: 1, + b: [1, 2, "3"], + }, + authorized_amount: null, + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("POST /admin/payment-collections/:id/authorize", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("marks a payment collection as authorized", async () => { + const api = useApi() + + const response = await api.post( + `/admin/payment-collections/${payCol.id}/authorize`, + undefined, + adminHeaders + ) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + status: "authorized", + description: "paycol description", + amount: 10000, + authorized_amount: 10000, + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("DELETE /admin/payment-collections/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("delete a payment collection", async () => { + const api = useApi() + + const response = await api.delete( + `/admin/payment-collections/${payCol.id}`, + adminHeaders + ) + + expect(response.data).toEqual({ + id: `${payCol.id}`, + deleted: true, + object: "payment_collection", + }) + + expect(response.status).toEqual(200) + }) + + it("throws error when deleting an authorized payment collection", async () => { + const api = useApi() + + await api.post( + `/admin/payment-collections/${payCol.id}/authorize`, + undefined, + adminHeaders + ) + + try { + await api.delete( + `/admin/payment-collections/${payCol.id}`, + adminHeaders + ) + + expect(1).toBe(2) // should be ignored + } catch (res) { + expect(res.response.data.message).toBe( + "Cannot delete payment collection with status authorized" + ) + } + }) + }) +}) diff --git a/integration-tests/api/__tests__/admin/payment.js b/integration-tests/api/__tests__/admin/payment.js new file mode 100644 index 0000000000..f8b58f4e66 --- /dev/null +++ b/integration-tests/api/__tests__/admin/payment.js @@ -0,0 +1,163 @@ +const path = require("path") + +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../helpers/use-api") +const { useDb } = require("../../../helpers/use-db") +const adminSeeder = require("../../helpers/admin-seeder") + +const { + simplePaymentCollectionFactory, +} = require("../../factories/simple-payment-collection-factory") +const { + simpleCustomerFactory, +} = require("../../factories/simple-customer-factory") + +jest.setTimeout(30000) + +const adminHeaders = { + headers: { + Authorization: "Bearer test_token", + }, +} + +describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment", () => { + let medusaProcess + let dbConnection + + let payCol = null + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + const [process, connection] = await startServerWithEnvironment({ + cwd, + env: { MEDUSA_FF_ORDER_EDITING: true }, + verbose: false, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("POST /admin/payment-collections/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + await simpleCustomerFactory(dbConnection, { + id: "customer", + email: "test@customer.com", + }) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("Captures an authorized payment", async () => { + const api = useApi() + + // create payment session + await api.post(`/store/payment-collections/${payCol.id}/sessions`, { + sessions: { + provider_id: "test-pay", + customer_id: "customer", + amount: 10000, + }, + }) + await api.post(`/store/payment-collections/${payCol.id}/authorize`) + + const paymentCollections = await api.get( + `/admin/payment-collections/${payCol.id}`, + adminHeaders + ) + + expect(paymentCollections.data.payment_collection.payments).toHaveLength( + 1 + ) + + const payment = paymentCollections.data.payment_collection.payments[0] + expect(payment.captured_at).toBe(null) + + const response = await api.post( + `/admin/payments/${payment.id}/capture`, + undefined, + adminHeaders + ) + + expect(response.data.payment).toEqual( + expect.objectContaining({ + id: payment.id, + captured_at: expect.any(String), + amount: 10000, + }) + ) + expect(response.status).toEqual(200) + }) + + it("Refunds an captured payment", async () => { + const api = useApi() + + // create payment session + await api.post(`/store/payment-collections/${payCol.id}/sessions`, { + sessions: { + provider_id: "test-pay", + customer_id: "customer", + amount: 10000, + }, + }) + await api.post(`/store/payment-collections/${payCol.id}/authorize`) + + const paymentCollections = await api.get( + `/admin/payment-collections/${payCol.id}`, + adminHeaders + ) + const payment = paymentCollections.data.payment_collection.payments[0] + await api.post( + `/admin/payments/${payment.id}/capture`, + undefined, + adminHeaders + ) + + // refund + const response = await api.post( + `/admin/payments/${payment.id}/refund`, + { + amount: 5000, + reason: "return", + note: "Do not like it", + }, + adminHeaders + ) + + expect(response.data.refund).toEqual( + expect.objectContaining({ + payment_id: payment.id, + reason: "return", + amount: 5000, + }) + ) + expect(response.status).toEqual(200) + + const savedPayment = await api.get( + `/admin/payments/${payment.id}`, + adminHeaders + ) + + expect(savedPayment.data.payment).toEqual( + expect.objectContaining({ + amount_refunded: 5000, + }) + ) + }) + }) +}) diff --git a/integration-tests/api/__tests__/store/payment-collection.js b/integration-tests/api/__tests__/store/payment-collection.js new file mode 100644 index 0000000000..8beeb968d3 --- /dev/null +++ b/integration-tests/api/__tests__/store/payment-collection.js @@ -0,0 +1,305 @@ +const path = require("path") + +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../helpers/use-api") +const { useDb } = require("../../../helpers/use-db") +const adminSeeder = require("../../helpers/admin-seeder") + +const { + simplePaymentCollectionFactory, +} = require("../../factories/simple-payment-collection-factory") +const { + simpleCustomerFactory, +} = require("../../factories/simple-customer-factory") + +jest.setTimeout(30000) + +describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { + let medusaProcess + let dbConnection + + let payCol = null + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + const [process, connection] = await startServerWithEnvironment({ + cwd, + env: { MEDUSA_FF_ORDER_EDITING: true }, + verbose: false, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("GET /store/payment-collections/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("gets payment collection", async () => { + const api = useApi() + + const response = await api.get(`/store/payment-collections/${payCol.id}`) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + status: "not_paid", + description: "paycol description", + amount: 10000, + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("Manage Payment Sessions", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + + await simpleCustomerFactory(dbConnection, { + id: "customer", + email: "test@customer.com", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("Set a payment session", async () => { + const api = useApi() + + const response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + sessions: { + provider_id: "test-pay", + customer_id: "customer", + amount: 10000, + }, + } + ) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + amount: 10000, + status: "pending", + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) + + it("Set multiple payment sessions", async () => { + const api = useApi() + + const response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + sessions: [ + { + provider_id: "test-pay", + customer_id: "customer", + amount: 2000, + }, + { + provider_id: "test-pay", + customer_id: "customer", + amount: 5000, + }, + { + provider_id: "test-pay", + customer_id: "customer", + amount: 3000, + }, + ], + } + ) + + expect(response.data.payment_collection.payment_sessions).toHaveLength(3) + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + amount: 2000, + status: "pending", + }), + expect.objectContaining({ + amount: 5000, + status: "pending", + }), + expect.objectContaining({ + amount: 3000, + status: "pending", + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) + + it("update multiple payment sessions", async () => { + const api = useApi() + + let response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + sessions: [ + { + provider_id: "test-pay", + customer_id: "customer", + amount: 2000, + }, + { + provider_id: "test-pay", + customer_id: "customer", + amount: 5000, + }, + { + provider_id: "test-pay", + customer_id: "customer", + amount: 3000, + }, + ], + } + ) + + expect(response.data.payment_collection.payment_sessions).toHaveLength(3) + + const multipleSessions = response.data.payment_collection.payment_sessions + + response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + sessions: [ + { + provider_id: "test-pay", + customer_id: "customer", + amount: 5000, + session_id: multipleSessions[0].id, + }, + { + provider_id: "test-pay", + customer_id: "customer", + amount: 5000, + session_id: multipleSessions[1].id, + }, + ], + } + ) + + expect(response.data.payment_collection.payment_sessions).toHaveLength(2) + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + amount: 5000, + status: "pending", + id: multipleSessions[0].id, + }), + expect.objectContaining({ + amount: 5000, + status: "pending", + id: multipleSessions[1].id, + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("Authorize a Payment Sessions", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + + payCol = await simplePaymentCollectionFactory(dbConnection, { + description: "paycol description", + amount: 10000, + }) + + await simpleCustomerFactory(dbConnection, { + id: "customer", + email: "test@customer.com", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("Authorize a payment session", async () => { + const api = useApi() + + await api.post(`/store/payment-collections/${payCol.id}/sessions`, { + sessions: { + provider_id: "test-pay", + customer_id: "customer", + amount: 10000, + }, + }) + + const response = await api.post( + `/store/payment-collections/${payCol.id}/authorize` + ) + + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + amount: 10000, + status: "authorized", + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) + }) +}) diff --git a/integration-tests/api/factories/simple-payment-collection-factory.ts b/integration-tests/api/factories/simple-payment-collection-factory.ts index d86be9b355..599f53b3ba 100644 --- a/integration-tests/api/factories/simple-payment-collection-factory.ts +++ b/integration-tests/api/factories/simple-payment-collection-factory.ts @@ -12,6 +12,10 @@ export const simplePaymentCollectionFactory = async ( const defaultData = { currency_code: data.currency_code ?? "usd", + type: data.type ?? "order_edit", + status: data.status ?? "not_paid", + amount: data.amount ?? 1000, + created_by: data.created_by ?? "admin_user", } if (!data.region && !data.region_id) { diff --git a/packages/medusa-js/src/resources/admin/payment-collections.ts b/packages/medusa-js/src/resources/admin/payment-collections.ts index d0def6aaab..bc6d983fef 100644 --- a/packages/medusa-js/src/resources/admin/payment-collections.ts +++ b/packages/medusa-js/src/resources/admin/payment-collections.ts @@ -40,6 +40,14 @@ class AdminPaymentCollectionsResource extends BaseResource { const path = `/admin/payment-collections/${id}` return this.client.request("DELETE", path, undefined, {}, customHeaders) } + + markAsAuthorized( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/payment-collections/${id}/authorize` + return this.client.request("POST", path, undefined, {}, customHeaders) + } } export default AdminPaymentCollectionsResource diff --git a/packages/medusa/src/api/routes/admin/payment-collections/index.ts b/packages/medusa/src/api/routes/admin/payment-collections/index.ts index 774fa186dc..ec14d5b8cf 100644 --- a/packages/medusa/src/api/routes/admin/payment-collections/index.ts +++ b/packages/medusa/src/api/routes/admin/payment-collections/index.ts @@ -36,6 +36,11 @@ export default (app, container) => { middlewares.wrap(require("./update-payment-collection").default) ) + route.post( + "/:id/authorize", + middlewares.wrap(require("./mark-authorized-payment-collection").default) + ) + route.delete( "/:id", middlewares.wrap(require("./delete-payment-collection").default) diff --git a/packages/medusa/src/api/routes/admin/payment-collections/mark-authorized-payment-collection.ts b/packages/medusa/src/api/routes/admin/payment-collections/mark-authorized-payment-collection.ts new file mode 100644 index 0000000000..6da8fe1280 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/payment-collections/mark-authorized-payment-collection.ts @@ -0,0 +1,72 @@ +import { EntityManager } from "typeorm" +import { PaymentCollectionService } from "../../../../services" + +/** + * @oas [post] /payment-collections/{id}/authorize + * operationId: "MarkAuthorizedPaymentCollectionsPaymentCollection" + * summary: "Set the status of PaymentCollection as Authorized" + * description: "Sets the status of PaymentCollection as Authorized." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the PaymentCollection. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.paymentCollections.markAsAuthorized(payment_collection_id) + * .then(({ payment_collection }) => { + * console.log(payment_collection.id) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/payment-collections/{id}/authorize' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - PaymentCollection + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * payment_collection: + * $ref: "#/components/schemas/payment_collection" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req, res) => { + const { id } = req.params + + const paymentCollectionService: PaymentCollectionService = req.scope.resolve( + "paymentCollectionService" + ) + + const manager: EntityManager = req.scope.resolve("manager") + const paymentCollection = await manager.transaction( + async (transactionManager) => { + return await paymentCollectionService + .withTransaction(transactionManager) + .markAsAuthorized(id) + } + ) + + res.status(200).json({ payment_collection: paymentCollection }) +} diff --git a/packages/medusa/src/services/payment-collection.ts b/packages/medusa/src/services/payment-collection.ts index 5177e9d514..53d1d7546c 100644 --- a/packages/medusa/src/services/payment-collection.ts +++ b/packages/medusa/src/services/payment-collection.ts @@ -365,6 +365,28 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } + async markAsAuthorized( + paymentCollectionId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { + const paymentCollectionRepo = manager.getCustomRepository( + this.paymentCollectionRepository_ + ) + + const paymentCollection = await this.retrieve(paymentCollectionId) + paymentCollection.status = PaymentCollectionStatus.AUTHORIZED + paymentCollection.authorized_amount = paymentCollection.amount + + const result = await paymentCollectionRepo.save(paymentCollection) + + await this.eventBusService_ + .withTransaction(manager) + .emit(PaymentCollectionService.Events.PAYMENT_AUTHORIZED, result) + + return result + }) + } + async authorize( paymentCollectionId: string, context: Record = {}