diff --git a/integration-tests/api/__tests__/admin/payment-collection.js b/integration-tests/api/__tests__/admin/payment-collection.js index e99a656724..1ce01e7456 100644 --- a/integration-tests/api/__tests__/admin/payment-collection.js +++ b/integration-tests/api/__tests__/admin/payment-collection.js @@ -27,7 +27,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment-collections", () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) const [process, connection] = await startServerWithEnvironment({ cwd, - env: { MEDUSA_FF_ORDER_EDITING: true }, + env: { MEDUSA_FF_ORDER_EDITING: true } }) dbConnection = connection medusaProcess = process diff --git a/integration-tests/api/__tests__/admin/payment.js b/integration-tests/api/__tests__/admin/payment.js index 291f7ae6a6..8f5d668010 100644 --- a/integration-tests/api/__tests__/admin/payment.js +++ b/integration-tests/api/__tests__/admin/payment.js @@ -66,14 +66,20 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment", () => { const api = useApi() // create payment session - await api.post(`/store/payment-collections/${payCol.id}/sessions`, { - sessions: { + const payColRes = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { provider_id: "test-pay", - customer_id: "customer", - amount: 10000, - }, - }) - await api.post(`/store/payment-collections/${payCol.id}/authorize`) + } + ) + await api.post( + `/store/payment-collections/${payCol.id}/sessions/batch/authorize`, + { + session_ids: payColRes.data.payment_collection.payment_sessions.map( + ({ id }) => id + ), + } + ) const paymentCollections = await api.get( `/admin/payment-collections/${payCol.id}`, @@ -85,6 +91,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment", () => { ) const payment = paymentCollections.data.payment_collection.payments[0] + expect(payment.captured_at).toBe(null) const response = await api.post( @@ -107,14 +114,20 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment", () => { const api = useApi() // create payment session - await api.post(`/store/payment-collections/${payCol.id}/sessions`, { - sessions: { + const payColRes = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { provider_id: "test-pay", - customer_id: "customer", - amount: 10000, - }, - }) - await api.post(`/store/payment-collections/${payCol.id}/authorize`) + } + ) + await api.post( + `/store/payment-collections/${payCol.id}/sessions/batch/authorize`, + { + session_ids: payColRes.data.payment_collection.payment_sessions.map( + ({ id }) => id + ), + } + ) const paymentCollections = await api.get( `/admin/payment-collections/${payCol.id}`, diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js index 6dc3c95cf1..b65a497c57 100644 --- a/integration-tests/api/__tests__/store/order-edit.js +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -5,6 +5,9 @@ const startServerWithEnvironment = const { useApi } = require("../../../helpers/use-api") const { useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") +const { + getClientAuthenticationCookie, +} = require("../../helpers/client-authentication") const { simpleOrderEditFactory, } = require("../../factories/simple-order-edit-factory") @@ -16,6 +19,7 @@ const { simpleLineItemFactory, simpleProductFactory, simpleOrderFactory, + simpleCustomerFactory, } = require("../../factories") const { OrderEditItemChangeType } = require("@medusajs/medusa") @@ -33,6 +37,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { }) dbConnection = connection medusaProcess = process + + await simpleCustomerFactory(dbConnection, { + id: "customer", + email: "test@medusajs.com", + }) }) afterAll(async () => { @@ -163,7 +172,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { it("gets order edit", async () => { const api = useApi() - const response = await api.get(`/store/order-edits/${orderEditId}`) + const response = await api.get(`/store/order-edits/${orderEditId}`, { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, + }) expect(response.status).toEqual(200) expect(response.data.order_edit).toEqual( @@ -217,7 +230,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { const api = useApi() const err = await api - .get(`/store/order-edits/${orderEditId}?fields=internal_note,order_id`) + .get( + `/store/order-edits/${orderEditId}?fields=internal_note,order_id`, + { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, + } + ) .catch((e) => e) expect(err.response.data.message).toBe( @@ -264,6 +284,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { `/store/order-edits/${declineableOrderEdit.id}/decline`, { declined_reason: "wrong color", + }, + { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, } ) @@ -282,6 +307,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { `/store/order-edits/${declinedOrderEdit.id}/decline`, { declined_reason: "wrong color", + }, + { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, } ) @@ -301,9 +331,17 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { const api = useApi() await api - .post(`/store/order-edits/${confirmedOrderEdit.id}/decline`, { - declined_reason: "wrong color", - }) + .post( + `/store/order-edits/${confirmedOrderEdit.id}/decline`, + { + declined_reason: "wrong color", + }, + { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, + } + ) .catch((err) => { expect(err.response.status).toEqual(400) expect(err.response.data.message).toEqual( @@ -345,13 +383,16 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { return await db.teardown() }) - // TODO once payment collection is done - /*it("complete an order edit", async () => {})*/ - it("idempotently complete an already confirmed order edit", async () => { const api = useApi() const result = await api.post( - `/store/order-edits/${confirmedOrderEdit.id}/complete` + `/store/order-edits/${confirmedOrderEdit.id}/complete`, + undefined, + { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, + } ) expect(result.status).toEqual(200) @@ -367,7 +408,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { it("fails to complete a non requested order edit", async () => { const api = useApi() const err = await api - .post(`/store/order-edits/${createdOrderEdit.id}/complete`) + .post(`/store/order-edits/${createdOrderEdit.id}/complete`, undefined, { + headers: { + Cookie: await getClientAuthenticationCookie(api), + }, + }) .catch((e) => e) expect(err.response.status).toEqual(400) diff --git a/integration-tests/api/__tests__/store/payment-collection.js b/integration-tests/api/__tests__/store/payment-collection.js index 47fe19a709..8ee0e33f71 100644 --- a/integration-tests/api/__tests__/store/payment-collection.js +++ b/integration-tests/api/__tests__/store/payment-collection.js @@ -5,6 +5,9 @@ const startServerWithEnvironment = const { useApi } = require("../../../helpers/use-api") const { useDb } = require("../../../helpers/use-db") const adminSeeder = require("../../helpers/admin-seeder") +const { + getClientAuthenticationCookie, +} = require("../../helpers/client-authentication") const { simplePaymentCollectionFactory, @@ -28,6 +31,11 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { }) dbConnection = connection medusaProcess = process + + await simpleCustomerFactory(dbConnection, { + id: "customer", + email: "test@medusajs.com", + }) }) afterAll(async () => { @@ -71,7 +79,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { }) }) - describe("Manage Payment Sessions", () => { + describe("Manage a Single Payment Session", () => { beforeEach(async () => { await adminSeeder(dbConnection) @@ -97,10 +105,106 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { const response = await api.post( `/store/payment-collections/${payCol.id}/sessions`, { - sessions: { - provider_id: "test-pay", - customer_id: "customer", - amount: 10000, + provider_id: "test-pay", + } + ) + + 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("update a payment session", async () => { + const api = useApi() + + let response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + provider_id: "test-pay", + } + ) + + expect(response.data.payment_collection.payment_sessions).toHaveLength(1) + + const paySessions = response.data.payment_collection.payment_sessions + response = await api.post( + `/store/payment-collections/${payCol.id}/sessions`, + { + provider_id: "test-pay", + } + ) + + expect(response.data.payment_collection.payment_sessions).toHaveLength(1) + expect(response.data.payment_collection).toEqual( + expect.objectContaining({ + id: payCol.id, + type: "order_edit", + amount: 10000, + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + id: paySessions[0].id, + amount: 10000, + status: "pending", + }), + ]), + }) + ) + + expect(response.status).toEqual(200) + }) + }) + + describe("Manage Multiple 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/batch`, + { + sessions: [ + { + provider_id: "test-pay", + amount: 10000, + }, + ], + }, + { + headers: { + Cookie: await getClientAuthenticationCookie( + api, + "test@customer.com" + ), }, } ) @@ -126,22 +230,19 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { const api = useApi() const response = await api.post( - `/store/payment-collections/${payCol.id}/sessions`, + `/store/payment-collections/${payCol.id}/sessions/batch`, { 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, }, ], @@ -178,22 +279,19 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { const api = useApi() let response = await api.post( - `/store/payment-collections/${payCol.id}/sessions`, + `/store/payment-collections/${payCol.id}/sessions/batch`, { 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, }, ], @@ -205,18 +303,16 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { const multipleSessions = response.data.payment_collection.payment_sessions response = await api.post( - `/store/payment-collections/${payCol.id}/sessions`, + `/store/payment-collections/${payCol.id}/sessions/batch`, { 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, }, @@ -249,7 +345,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { }) }) - describe("Authorize a Payment Sessions", () => { + describe("Authorize Payment Sessions", () => { beforeEach(async () => { await adminSeeder(dbConnection) @@ -269,19 +365,62 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { return await db.teardown() }) - it("Authorize a payment session", async () => { + it("Authorizes a payment session", async () => { const api = useApi() - await api.post(`/store/payment-collections/${payCol.id}/sessions`, { - sessions: { - provider_id: "test-pay", - customer_id: "customer", + const payColRes = await api.post( + `/store/payment-collections/${payCol.id}/sessions/batch`, + { + sessions: [ + { + provider_id: "test-pay", + amount: 10000, + }, + ], + } + ) + + const sessionId = payColRes.data.payment_collection.payment_sessions[0].id + const response = await api.post( + `/store/payment-collections/${payCol.id}/sessions/${sessionId}/authorize` + ) + + expect(response.data.payment_session).toEqual( + expect.objectContaining({ amount: 10000, - }, - }) + status: "authorized", + }) + ) + + expect(response.status).toEqual(200) + }) + + it("Authorize multiple payment sessions", async () => { + const api = useApi() + + const payColRes = await api.post( + `/store/payment-collections/${payCol.id}/sessions/batch`, + { + sessions: [ + { + provider_id: "test-pay", + amount: 5000, + }, + { + provider_id: "test-pay", + amount: 5000, + }, + ], + } + ) const response = await api.post( - `/store/payment-collections/${payCol.id}/authorize` + `/store/payment-collections/${payCol.id}/sessions/batch/authorize`, + { + session_ids: payColRes.data.payment_collection.payment_sessions.map( + ({ id }) => id + ), + } ) expect(response.data.payment_collection).toEqual( @@ -291,14 +430,18 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/payment-collections", () => { amount: 10000, payment_sessions: expect.arrayContaining([ expect.objectContaining({ - amount: 10000, + amount: 5000, + status: "authorized", + }), + expect.objectContaining({ + amount: 5000, status: "authorized", }), ]), }) ) - expect(response.status).toEqual(200) + expect(response.status).toEqual(207) }) }) }) diff --git a/integration-tests/api/factories/index.ts b/integration-tests/api/factories/index.ts index 1ed9578557..277ec463c0 100644 --- a/integration-tests/api/factories/index.ts +++ b/integration-tests/api/factories/index.ts @@ -19,3 +19,6 @@ export * from "./simple-batch-job-factory" export * from "./simple-sales-channel-factory" export * from "./simple-custom-shipping-option-factory" export * from "./simple-payment-collection-factory" +export * from "./simple-order-edit-factory" +export * from "./simple-order-item-change-factory" +export * from "./simple-customer-factory" diff --git a/integration-tests/api/factories/simple-customer-factory.ts b/integration-tests/api/factories/simple-customer-factory.ts index 011c68b90e..1a42f1ac5e 100644 --- a/integration-tests/api/factories/simple-customer-factory.ts +++ b/integration-tests/api/factories/simple-customer-factory.ts @@ -11,6 +11,7 @@ export type CustomerFactoryData = { email?: string groups?: CustomerGroupFactoryData[] password_hash?: string + has_account?: boolean } export const simpleCustomerFactory = async ( @@ -28,6 +29,10 @@ export const simpleCustomerFactory = async ( const c = manager.create(Customer, { id: customerId, email: data.email, + password_hash: + data.password_hash ?? + "c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test" + has_account: data.has_account ?? true, }) if (data.password_hash) { diff --git a/integration-tests/api/helpers/client-authentication.ts b/integration-tests/api/helpers/client-authentication.ts new file mode 100644 index 0000000000..80de5a1a65 --- /dev/null +++ b/integration-tests/api/helpers/client-authentication.ts @@ -0,0 +1,20 @@ +const AUTH_COOKIE = {} +export async function getClientAuthenticationCookie( + api, + email = null, + password = null +) { + const user = { + email: email ?? "test@medusajs.com", + password: password ?? "test", + } + + if (AUTH_COOKIE[user.email]) { + return AUTH_COOKIE[user.email] + } + + const authResponse = await api.post("/store/auth", user) + AUTH_COOKIE[user.email] = authResponse.headers["set-cookie"][0].split(";") + + return AUTH_COOKIE[user.email] +} diff --git a/packages/medusa-core-utils/src/errors.ts b/packages/medusa-core-utils/src/errors.ts index c9412e7587..cd84f3095b 100644 --- a/packages/medusa-core-utils/src/errors.ts +++ b/packages/medusa-core-utils/src/errors.ts @@ -13,6 +13,7 @@ export const MedusaErrorTypes = { NOT_ALLOWED: "not_allowed", UNEXPECTED_STATE: "unexpected_state", CONFLICT: "conflict", + PAYMENT_AUTHORIZATION_ERROR: "payment_authorization_error", } export const MedusaErrorCodes = { diff --git a/packages/medusa-js/src/resources/admin/payment-collections.ts b/packages/medusa-js/src/resources/admin/payment-collections.ts index bc6d983fef..15bd741dee 100644 --- a/packages/medusa-js/src/resources/admin/payment-collections.ts +++ b/packages/medusa-js/src/resources/admin/payment-collections.ts @@ -1,7 +1,7 @@ import { - AdminUpdatePaymentCollectionRequest, + AdminUpdatePaymentCollectionsReq, AdminPaymentCollectionDeleteRes, - AdminPaymentCollectionRes, + AdminPaymentCollectionsRes, GetPaymentCollectionsParams, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" @@ -13,7 +13,7 @@ class AdminPaymentCollectionsResource extends BaseResource { id: string, query?: GetPaymentCollectionsParams, customHeaders: Record = {} - ): ResponsePromise { + ): ResponsePromise { let path = `/admin/payment-collections/${id}` if (query) { @@ -26,9 +26,9 @@ class AdminPaymentCollectionsResource extends BaseResource { update( id: string, - payload: AdminUpdatePaymentCollectionRequest, + payload: AdminUpdatePaymentCollectionsReq, customHeaders: Record = {} - ): ResponsePromise { + ): ResponsePromise { const path = `/admin/payment-collections/${id}` return this.client.request("POST", path, payload, {}, customHeaders) } @@ -44,7 +44,7 @@ class AdminPaymentCollectionsResource extends BaseResource { markAsAuthorized( id: string, customHeaders: Record = {} - ): ResponsePromise { + ): ResponsePromise { const path = `/admin/payment-collections/${id}/authorize` return this.client.request("POST", path, undefined, {}, customHeaders) } diff --git a/packages/medusa-js/src/resources/payment-collections.ts b/packages/medusa-js/src/resources/payment-collections.ts index 8850a63809..8150013f4e 100644 --- a/packages/medusa-js/src/resources/payment-collections.ts +++ b/packages/medusa-js/src/resources/payment-collections.ts @@ -1,9 +1,10 @@ import { GetPaymentCollectionsParams, - StoreManagePaymentCollectionSessionRequest, - StoreRefreshPaymentCollectionSessionRequest, - StorePaymentCollectionSessionRes, - StorePaymentCollectionRes, + StorePostPaymentCollectionsBatchSessionsReq, + StorePostPaymentCollectionsBatchSessionsAuthorizeReq, + StorePaymentCollectionSessionsReq, + StorePaymentCollectionsSessionRes, + StorePaymentCollectionsRes, } from "@medusajs/medusa" import { ResponsePromise } from "../typings" import BaseResource from "./base" @@ -14,7 +15,7 @@ class PaymentCollectionsResource extends BaseResource { id: string, query?: GetPaymentCollectionsParams, customHeaders: Record = {} - ): ResponsePromise { + ): ResponsePromise { let path = `/store/payment-collections/${id}` if (query) { @@ -25,19 +26,38 @@ class PaymentCollectionsResource extends BaseResource { return this.client.request("GET", path, undefined, {}, customHeaders) } - authorize( + authorizePaymentSession( id: string, + session_id: string, customHeaders: Record = {} - ): ResponsePromise { - const path = `/store/payment-collections/${id}/authorize` + ): ResponsePromise { + const path = `/store/payment-collections/${id}/sessions/${session_id}/authorize` return this.client.request("POST", path, undefined, {}, customHeaders) } - manageSessions( + authorizePaymentSessionsBatch( id: string, - payload: StoreManagePaymentCollectionSessionRequest, + payload: StorePostPaymentCollectionsBatchSessionsAuthorizeReq, customHeaders: Record = {} - ): ResponsePromise { + ): ResponsePromise { + const path = `/store/payment-collections/${id}/sessions/batch/authorize` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + managePaymentSessionsBatch( + id: string, + payload: StorePostPaymentCollectionsBatchSessionsReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/store/payment-collections/${id}/sessions/batch` + return this.client.request("POST", path, payload, {}, customHeaders) + } + + managePaymentSession( + id: string, + payload: StorePaymentCollectionSessionsReq, + customHeaders: Record = {} + ): ResponsePromise { const path = `/store/payment-collections/${id}/sessions` return this.client.request("POST", path, payload, {}, customHeaders) } @@ -45,11 +65,10 @@ class PaymentCollectionsResource extends BaseResource { refreshPaymentSession( id: string, session_id: string, - payload: StoreRefreshPaymentCollectionSessionRequest, customHeaders: Record = {} - ): ResponsePromise { - const path = `/store/payment-collections/${id}/sessions/${session_id}/refresh` - return this.client.request("POST", path, payload, {}, customHeaders) + ): ResponsePromise { + const path = `/store/payment-collections/${id}/sessions/${session_id}` + return this.client.request("POST", path, undefined, {}, customHeaders) } } diff --git a/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js b/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js index 5fc48ea7c4..cb1e3ad6cb 100644 --- a/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js +++ b/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js @@ -73,7 +73,7 @@ export default async (req, res) => { async function autorizePaymentCollection(req, id, orderId) { const manager = req.scope.resolve("manager") const paymentCollectionService = req.scope.resolve( - "paymentCollectonService" + "paymentCollectionService" ) await manager.transaction(async (manager) => { diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index 37abdeaa87..47ea452408 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -181,7 +181,7 @@ class StripeProviderService extends AbstractPaymentService { async createPaymentNew(paymentInput, intentRequestData = {}) { const { customer, currency_code, amount, resource_id, cart } = paymentInput - const { id: customer_id, email } = customer + const { id: customer_id, email } = customer ?? {} const intentRequest = { description: @@ -205,7 +205,7 @@ class StripeProviderService extends AbstractPaymentService { intentRequest.customer = stripeCustomer.id } - } else { + } else if (email) { const stripeCustomer = await this.createCustomer({ email, }) @@ -302,7 +302,7 @@ class StripeProviderService extends AbstractPaymentService { try { const stripeId = paymentInput.customer?.metadata?.stripe_id - if (stripeId !== paymentInput.customer_id) { + if (stripeId !== paymentSessionData.customer) { return await this.createPaymentNew(paymentInput, intentRequestData) } else { if (paymentSessionData.amount === Math.round(paymentInput.amount)) { diff --git a/packages/medusa-react/mocks/data/fixtures.json b/packages/medusa-react/mocks/data/fixtures.json index 4ef6309f11..d19b9c093d 100644 --- a/packages/medusa-react/mocks/data/fixtures.json +++ b/packages/medusa-react/mocks/data/fixtures.json @@ -1154,8 +1154,8 @@ "publishable_api_key": { "id": "pubkey_1234", "created_by": "admin_user", - "created_at": "2021-11-08 11:58:56.975971+01", - "updated_at": "2021-11-08 11:58:56.975971+01", + "created_at": "2021-11-08 11:58:56.975971+01", + "updated_at": "2021-11-08 11:58:56.975971+01", "revoked_by": null, "revoked_at": null }, @@ -1324,6 +1324,76 @@ "created_at": "2022-07-05T15:16:01.959Z", "deleted_at": null } - ] + ], + "payment_collection": { + "id": "paycol_01GJK7P9MRHM6XWCJG70JFB8PF", + "created_at": "2022-11-23T21:53:19.367Z", + "updated_at": "2022-11-24T12:57:08.652Z", + "deleted_at": null, + "type": "order_edit", + "status": "authorized", + "description": null, + "amount": 900, + "authorized_amount": 900, + "region_id": "test-region", + "currency_code": "usd", + "metadata": null, + "created_by": "admin_user", + "payment_sessions": [ + { + "id": "ps_01GJMVD71Z4WF9FXXGT7DHGPCM", + "created_at": "2022-11-24T12:57:07.883Z", + "updated_at": "2022-11-24T12:57:08.652Z", + "cart_id": null, + "provider_id": "test-pay", + "is_selected": null, + "status": "authorized", + "data": {}, + "idempotency_key": null, + "amount": 900, + "payment_authorized_at": "2022-11-24T12:57:08.672Z" + } + ] + }, + "payment_sessions": { + "id": "ps_01GJMVD71Z4WF9FXXGT7DHGPCM", + "created_at": "2022-11-24T12:57:07.883Z", + "updated_at": "2022-11-24T12:57:08.652Z", + "cart_id": null, + "provider_id": "test-pay", + "is_selected": null, + "status": "authorized", + "data": {}, + "idempotency_key": null, + "amount": 900, + "payment_authorized_at": "2022-11-24T12:57:08.672Z" + }, + "payment": { + "id": "pay_A1GJEVD71Z4WF9FXFGT7D1GP15", + "swap_id": null, + "cart_id": null, + "order_id": null, + "amount": 900, + "currency_code": "usd", + "amount_refunded": 0, + "amount_captured": 900, + "provider_id": "manual", + "data": {}, + "captured_at": "2022-11-17T21:24:35.871Z", + "canceled_at": null, + "created_at": "2022-11-16T21:24:35.871Z", + "updated_at": "2022-11-16T21:24:35.871Z", + "metadata": null, + "idempotency_key": null + }, + "refund": { + "order_id": null, + "payment_id": "pay_A1GJEVD71Z4WF9FXFGT7D1GP15", + "amount": 900, + "note": null, + "reason": "return", + "metadata": null, + "idempotency_key": null + } } } diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index df2162cc6d..cf2fd8ba4f 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -2104,4 +2104,103 @@ export const adminHandlers = [ }) ) }), + + rest.get("/admin/payment-collections/:id", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + }), + + rest.delete("/admin/payment-collections/:id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + id: req.params.id, + object: "payment_collection", + deleted: true, + }) + ) + }), + + rest.post("/admin/payment-collections/:id", (req, res, ctx) => { + const { id } = req.params + const { description, metadata } = req.body as any + + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + description, + metadata, + id, + }, + }) + ) + }), + + rest.post("/admin/payment-collections/:id/authorize", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + }), + + rest.get("/admin/payments/:id", (req, res, ctx) => { + const { id } = req.params + + return res( + ctx.status(200), + ctx.json({ + payment: { + ...fixtures.get("payment"), + id, + }, + }) + ) + }), + + rest.post("/admin/payments/:id/capture", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment: { + ...fixtures.get("payment"), + id, + }, + }) + ) + }), + + rest.post("/admin/payments/:id/refund", (req, res, ctx) => { + const { id } = req.params + const { amount, reason, note } = req.body as any + + return res( + ctx.status(200), + ctx.json({ + refund: { + ...fixtures.get("refund"), + payment_id: id, + amount, + reason, + note, + }, + }) + ) + }), ] diff --git a/packages/medusa-react/mocks/handlers/store.ts b/packages/medusa-react/mocks/handlers/store.ts index 21fdbcee92..e014975b07 100644 --- a/packages/medusa-react/mocks/handlers/store.ts +++ b/packages/medusa-react/mocks/handlers/store.ts @@ -446,4 +446,100 @@ export const storeHandlers = [ }) ) }), + + rest.get("/store/payment-collections/:id", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + }), + + rest.post( + "/store/payment-collections/:id/sessions/batch", + (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + } + ), + + rest.post( + "/store/payment-collections/:id/sessions/batch/authorize", + (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(207), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + } + ), + + rest.post("/store/payment-collections/:id/sessions", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + payment_collection: { + ...fixtures.get("payment_collection"), + id, + }, + }) + ) + }), + + rest.post( + "/store/payment-collections/:id/sessions/:session_id", + (req, res, ctx) => { + const { id, session_id } = req.params + const payCol: any = { ...fixtures.get("payment_collection") } + + payCol.payment_sessions[0].id = `new_${session_id}` + const session = { + payment_session: payCol.payment_sessions[0], + } + + return res( + ctx.status(200), + ctx.json({ + ...session, + }) + ) + } + ), + + rest.post( + "/store/payment-collections/:id/sessions/:session_id/authorize", + (req, res, ctx) => { + const { session_id } = req.params + + const session = fixtures.get("payment_collection").payment_sessions[0] + return res( + ctx.status(200), + ctx.json({ + payment_session: { + ...session, + id: session_id, + }, + }) + ) + } + ), ] diff --git a/packages/medusa-react/src/hooks/admin/index.ts b/packages/medusa-react/src/hooks/admin/index.ts index 3e09bd4eb2..86a63c3e09 100644 --- a/packages/medusa-react/src/hooks/admin/index.ts +++ b/packages/medusa-react/src/hooks/admin/index.ts @@ -30,3 +30,5 @@ export * from "./tax-rates" export * from "./uploads" export * from "./users" export * from "./variants" +export * from "./payment-collections" +export * from "./payments" diff --git a/packages/medusa-react/src/hooks/admin/payment-collections/index.ts b/packages/medusa-react/src/hooks/admin/payment-collections/index.ts new file mode 100644 index 0000000000..a494946b87 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payment-collections/index.ts @@ -0,0 +1,2 @@ +export * from "./queries" +export * from "./mutations" diff --git a/packages/medusa-react/src/hooks/admin/payment-collections/mutations.ts b/packages/medusa-react/src/hooks/admin/payment-collections/mutations.ts new file mode 100644 index 0000000000..e4ecf102e7 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payment-collections/mutations.ts @@ -0,0 +1,85 @@ +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { Response } from "@medusajs/medusa-js" + +import { + AdminPaymentCollectionDeleteRes, + AdminPaymentCollectionsRes, + AdminUpdatePaymentCollectionsReq, +} from "@medusajs/medusa" + +import { buildOptions } from "../../utils/buildOptions" +import { useMedusa } from "../../../contexts" +import { adminPaymentCollectionQueryKeys } from "." + +export const useAdminDeletePaymentCollection = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + void + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.paymentCollections.delete(id), + buildOptions( + queryClient, + [ + adminPaymentCollectionQueryKeys.detail(id), + adminPaymentCollectionQueryKeys.lists(), + ], + options + ) + ) +} + +export const useAdminUpdatePaymentCollection = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminUpdatePaymentCollectionsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminUpdatePaymentCollectionsReq) => + client.admin.paymentCollections.update(id, payload), + buildOptions( + queryClient, + [ + adminPaymentCollectionQueryKeys.detail(id), + adminPaymentCollectionQueryKeys.lists(), + ], + options + ) + ) +} + +export const useAdminMarkPaymentCollectionAsAuthorized = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + void + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.paymentCollections.markAsAuthorized(id), + buildOptions( + queryClient, + [ + adminPaymentCollectionQueryKeys.detail(id), + adminPaymentCollectionQueryKeys.lists(), + ], + options + ) + ) +} diff --git a/packages/medusa-react/src/hooks/admin/payment-collections/queries.ts b/packages/medusa-react/src/hooks/admin/payment-collections/queries.ts new file mode 100644 index 0000000000..9a7c857fc4 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payment-collections/queries.ts @@ -0,0 +1,32 @@ +import { queryKeysFactory } from "../../utils" +import { AdminPaymentCollectionsRes } from "@medusajs/medusa" +import { useQuery } from "react-query" +import { useMedusa } from "../../../contexts" +import { UseQueryOptionsWrapper } from "../../../types" +import { Response } from "@medusajs/medusa-js" + +const PAYMENT_COLLECTION_QUERY_KEY = `paymentCollection` as const + +export const adminPaymentCollectionQueryKeys = queryKeysFactory< + typeof PAYMENT_COLLECTION_QUERY_KEY +>(PAYMENT_COLLECTION_QUERY_KEY) + +type AdminPaymentCollectionKey = typeof adminPaymentCollectionQueryKeys + +export const useAdminPaymentCollection = ( + id: string, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminPaymentCollectionQueryKeys.detail(id), + () => client.admin.paymentCollections.retrieve(id), + options + ) + + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/src/hooks/admin/payments/index.ts b/packages/medusa-react/src/hooks/admin/payments/index.ts new file mode 100644 index 0000000000..a494946b87 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payments/index.ts @@ -0,0 +1,2 @@ +export * from "./queries" +export * from "./mutations" diff --git a/packages/medusa-react/src/hooks/admin/payments/mutations.ts b/packages/medusa-react/src/hooks/admin/payments/mutations.ts new file mode 100644 index 0000000000..a021c1de64 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payments/mutations.ts @@ -0,0 +1,51 @@ +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { Response } from "@medusajs/medusa-js" + +import { + AdminPaymentRes, + AdminPostPaymentRefundsReq, + AdminRefundRes, +} from "@medusajs/medusa" + +import { buildOptions } from "../../utils/buildOptions" +import { useMedusa } from "../../../contexts" +import { adminPaymentQueryKeys } from "." + +export const useAdminPaymentsCapturePayment = ( + id: string, + options?: UseMutationOptions, Error, void> +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.payments.capturePayment(id), + buildOptions( + queryClient, + [adminPaymentQueryKeys.detail(id), adminPaymentQueryKeys.lists()], + options + ) + ) +} + +export const useAdminPaymentsRefundPayment = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostPaymentRefundsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostPaymentRefundsReq) => + client.admin.payments.refundPayment(id, payload), + buildOptions( + queryClient, + [adminPaymentQueryKeys.detail(id), adminPaymentQueryKeys.lists()], + options + ) + ) +} diff --git a/packages/medusa-react/src/hooks/admin/payments/queries.ts b/packages/medusa-react/src/hooks/admin/payments/queries.ts new file mode 100644 index 0000000000..c87f3fc7bb --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/payments/queries.ts @@ -0,0 +1,31 @@ +import { queryKeysFactory } from "../../utils" +import { AdminPaymentRes } from "@medusajs/medusa" +import { useQuery } from "react-query" +import { useMedusa } from "../../../contexts" +import { UseQueryOptionsWrapper } from "../../../types" +import { Response } from "@medusajs/medusa-js" + +const PAYMENT_QUERY_KEY = `payment` as const + +export const adminPaymentQueryKeys = + queryKeysFactory(PAYMENT_QUERY_KEY) + +type AdminPaymentKey = typeof adminPaymentQueryKeys + +export const useAdminPayment = ( + id: string, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminPaymentQueryKeys.detail(id), + () => client.admin.payments.retrieve(id), + options + ) + + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/src/hooks/store/index.ts b/packages/medusa-react/src/hooks/store/index.ts index ca2227f8f2..7bdb3f82c8 100644 --- a/packages/medusa-react/src/hooks/store/index.ts +++ b/packages/medusa-react/src/hooks/store/index.ts @@ -13,3 +13,4 @@ export * from "./returns/" export * from "./gift-cards/" export * from "./line-items/" export * from "./collections" +export * from "./payment-collections" diff --git a/packages/medusa-react/src/hooks/store/payment-collections/index.ts b/packages/medusa-react/src/hooks/store/payment-collections/index.ts new file mode 100644 index 0000000000..a494946b87 --- /dev/null +++ b/packages/medusa-react/src/hooks/store/payment-collections/index.ts @@ -0,0 +1,2 @@ +export * from "./queries" +export * from "./mutations" diff --git a/packages/medusa-react/src/hooks/store/payment-collections/mutations.ts b/packages/medusa-react/src/hooks/store/payment-collections/mutations.ts new file mode 100644 index 0000000000..f5dd16116f --- /dev/null +++ b/packages/medusa-react/src/hooks/store/payment-collections/mutations.ts @@ -0,0 +1,139 @@ +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { Response } from "@medusajs/medusa-js" + +import { + StorePaymentCollectionsRes, + StorePostPaymentCollectionsBatchSessionsReq, + StorePostPaymentCollectionsBatchSessionsAuthorizeReq, + StorePaymentCollectionSessionsReq, + StorePaymentCollectionsSessionRes, +} from "@medusajs/medusa" + +import { buildOptions } from "../../utils/buildOptions" +import { useMedusa } from "../../../contexts" +import { paymentCollectionQueryKeys } from "." + +export const useManageMultiplePaymentSessions = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + StorePostPaymentCollectionsBatchSessionsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: StorePostPaymentCollectionsBatchSessionsReq) => + client.paymentCollections.managePaymentSessionsBatch(id, payload), + buildOptions( + queryClient, + [ + paymentCollectionQueryKeys.lists(), + paymentCollectionQueryKeys.detail(id), + ], + options + ) + ) +} + +export const useManagePaymentSession = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + StorePaymentCollectionSessionsReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: StorePaymentCollectionSessionsReq) => + client.paymentCollections.managePaymentSession(id, payload), + buildOptions( + queryClient, + [ + paymentCollectionQueryKeys.lists(), + paymentCollectionQueryKeys.detail(id), + ], + options + ) + ) +} + +export const useAuthorizePaymentSession = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + string + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (session_id: string) => + client.paymentCollections.authorizePaymentSession(id, session_id), + buildOptions( + queryClient, + [ + paymentCollectionQueryKeys.lists(), + paymentCollectionQueryKeys.detail(id), + ], + options + ) + ) +} + +export const useAuthorizePaymentSessionsBatch = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + StorePostPaymentCollectionsBatchSessionsAuthorizeReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload) => + client.paymentCollections.authorizePaymentSessionsBatch(id, payload), + buildOptions( + queryClient, + [ + paymentCollectionQueryKeys.lists(), + paymentCollectionQueryKeys.detail(id), + ], + options + ) + ) +} + +export const usePaymentCollectionRefreshPaymentSession = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + string + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (session_id: string) => + client.paymentCollections.refreshPaymentSession(id, session_id), + buildOptions( + queryClient, + [ + paymentCollectionQueryKeys.lists(), + paymentCollectionQueryKeys.detail(id), + ], + options + ) + ) +} diff --git a/packages/medusa-react/src/hooks/store/payment-collections/queries.ts b/packages/medusa-react/src/hooks/store/payment-collections/queries.ts new file mode 100644 index 0000000000..7055025784 --- /dev/null +++ b/packages/medusa-react/src/hooks/store/payment-collections/queries.ts @@ -0,0 +1,32 @@ +import { queryKeysFactory } from "../../utils" +import { StorePaymentCollectionsRes } from "@medusajs/medusa" +import { useQuery } from "react-query" +import { useMedusa } from "../../../contexts" +import { UseQueryOptionsWrapper } from "../../../types" +import { Response } from "@medusajs/medusa-js" + +const PAYMENT_COLLECTION_QUERY_KEY = `paymentCollection` as const + +export const paymentCollectionQueryKeys = queryKeysFactory< + typeof PAYMENT_COLLECTION_QUERY_KEY +>(PAYMENT_COLLECTION_QUERY_KEY) + +type PaymentCollectionKey = typeof paymentCollectionQueryKeys + +export const usePaymentCollection = ( + id: string, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + paymentCollectionQueryKeys.detail(id), + () => client.paymentCollections.retrieve(id), + options + ) + + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/test/hooks/admin/payment-collections/mutations.test.ts b/packages/medusa-react/test/hooks/admin/payment-collections/mutations.test.ts new file mode 100644 index 0000000000..82fb0c679b --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/payment-collections/mutations.test.ts @@ -0,0 +1,80 @@ +import { + useAdminDeletePaymentCollection, + useAdminUpdatePaymentCollection, + useAdminMarkPaymentCollectionAsAuthorized, +} from "../../../../src" +import { renderHook } from "@testing-library/react-hooks" +import { createWrapper } from "../../../utils" + +describe("useAdminDeletePaymentCollection hook", () => { + test("Delete a payment collection", async () => { + const { result, waitFor } = renderHook( + () => useAdminDeletePaymentCollection("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + deleted: true, + }) + ) + }) +}) + +describe("useAdminUpdatePaymentCollection hook", () => { + test("Update a Payment Collection", async () => { + const { result, waitFor } = renderHook( + () => useAdminUpdatePaymentCollection("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + description: "new description", + metadata: { demo: "obj" }, + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.payment_collection).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + description: "new description", + metadata: { demo: "obj" }, + }) + ) + }) +}) + +describe("useAdminMarkPaymentCollectionAsAuthorized hook", () => { + test("Mark a Payment Collection as Authorized", async () => { + const { result, waitFor } = renderHook( + () => useAdminMarkPaymentCollectionAsAuthorized("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.payment_collection).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + status: "authorized", + }) + ) + }) +}) diff --git a/packages/medusa-react/test/hooks/admin/payment-collections/queries.test.ts b/packages/medusa-react/test/hooks/admin/payment-collections/queries.test.ts new file mode 100644 index 0000000000..bbc616534e --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/payment-collections/queries.test.ts @@ -0,0 +1,21 @@ +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" +import { useAdminPaymentCollection } from "../../../../src/hooks/admin/payment-collections" + +describe("useAdminPaymentCollection hook", () => { + test("returns a payment collection", async () => { + const payment_collection = fixtures.get("payment_collection") + const { result, waitFor } = renderHook( + () => useAdminPaymentCollection(payment_collection.id), + { + wrapper: createWrapper(), + } + ) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.payment_collection).toEqual(payment_collection) + }) +}) diff --git a/packages/medusa-react/test/hooks/admin/payments/mutations.test.ts b/packages/medusa-react/test/hooks/admin/payments/mutations.test.ts new file mode 100644 index 0000000000..05e671bb3b --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/payments/mutations.test.ts @@ -0,0 +1,58 @@ +import { + useAdminPaymentsCapturePayment, + useAdminPaymentsRefundPayment, +} from "../../../../src" +import { renderHook } from "@testing-library/react-hooks" +import { createWrapper } from "../../../utils" +import { RefundReason } from "@medusajs/medusa" + +describe("useAdminPaymentsCapturePayment hook", () => { + test("Capture a payment", async () => { + const { result, waitFor } = renderHook( + () => useAdminPaymentsCapturePayment("payment_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.payment).toEqual( + expect.objectContaining({ + amount_captured: 900, + }) + ) + }) +}) + +describe("useAdminPaymentsRefundPayment hook", () => { + test("Update a Payment Collection", async () => { + const { result, waitFor } = renderHook( + () => useAdminPaymentsRefundPayment("payment_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + amount: 500, + reason: RefundReason.DISCOUNT, + note: "note to refund", + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.refund).toEqual( + expect.objectContaining({ + payment_id: "payment_id", + amount: 500, + reason: RefundReason.DISCOUNT, + note: "note to refund", + }) + ) + }) +}) diff --git a/packages/medusa-react/test/hooks/admin/payments/queries.test.ts b/packages/medusa-react/test/hooks/admin/payments/queries.test.ts new file mode 100644 index 0000000000..e979cd2a17 --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/payments/queries.test.ts @@ -0,0 +1,18 @@ +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" +import { useAdminPayment } from "../../../../src/hooks/admin/payments" + +describe("useAdminPayment hook", () => { + test("returns a payment collection", async () => { + const payment = fixtures.get("payment") + const { result, waitFor } = renderHook(() => useAdminPayment(payment.id), { + wrapper: createWrapper(), + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.payment).toEqual(payment) + }) +}) diff --git a/packages/medusa-react/test/hooks/store/payment-collections/mutations.test.ts b/packages/medusa-react/test/hooks/store/payment-collections/mutations.test.ts new file mode 100644 index 0000000000..766aad2ae4 --- /dev/null +++ b/packages/medusa-react/test/hooks/store/payment-collections/mutations.test.ts @@ -0,0 +1,138 @@ +import { + useManageMultiplePaymentSessions, + useManagePaymentSession, + useAuthorizePaymentSession, + useAuthorizePaymentSessionsBatch, + usePaymentCollectionRefreshPaymentSession, +} from "../../../../src" +import { renderHook } from "@testing-library/react-hooks" +import { createWrapper } from "../../../utils" + +describe("useManageMultiplePaymentSessions hook", () => { + test("Manage multiple payment sessions of a payment collection", async () => { + const { result, waitFor } = renderHook( + () => useManageMultiplePaymentSessions("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + sessions: { + provider_id: "manual", + amount: 900, + }, + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data?.payment_collection).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + amount: 900, + }) + ) + }) +}) + +describe("useManagePaymentSession hook", () => { + test("Manage payment session of a payment collection", async () => { + const { result, waitFor } = renderHook( + () => useManagePaymentSession("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + provider_id: "manual", + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data?.payment_collection).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + amount: 900, + }) + ) + }) +}) + +describe("useAuthorizePaymentSession hook", () => { + test("Authorize a payment session of a Payment Collection", async () => { + const { result, waitFor } = renderHook( + () => useAuthorizePaymentSession("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate("123") + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.payment_session).toEqual( + expect.objectContaining({ + id: "123", + amount: 900, + }) + ) + }) +}) + +describe("authorizePaymentSessionsBatch hook", () => { + test("Authorize all payment sessions of a Payment Collection", async () => { + const { result, waitFor } = renderHook( + () => useAuthorizePaymentSessionsBatch("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ + session_ids: ["abc"], + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(207) + + expect(result.current.data.payment_collection).toEqual( + expect.objectContaining({ + id: "payment_collection_id", + payment_sessions: expect.arrayContaining([ + expect.objectContaining({ + amount: 900, + }), + ]), + }) + ) + }) +}) + +describe("usePaymentCollectionRefreshPaymentSession hook", () => { + test("Refresh a payment sessions of a Payment Collection", async () => { + const { result, waitFor } = renderHook( + () => usePaymentCollectionRefreshPaymentSession("payment_collection_id"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate("session_id") + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.payment_session).toEqual( + expect.objectContaining({ + id: "new_session_id", + amount: 900, + }) + ) + }) +}) diff --git a/packages/medusa-react/test/hooks/store/payment-collections/queries.test.ts b/packages/medusa-react/test/hooks/store/payment-collections/queries.test.ts new file mode 100644 index 0000000000..f5910865db --- /dev/null +++ b/packages/medusa-react/test/hooks/store/payment-collections/queries.test.ts @@ -0,0 +1,21 @@ +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" +import { usePaymentCollection } from "../../../../src/hooks/store/payment-collections" + +describe("usePaymentCollection hook", () => { + test("returns a payment collection", async () => { + const payment_collection = fixtures.get("payment_collection") + const { result, waitFor } = renderHook( + () => usePaymentCollection(payment_collection.id), + { + wrapper: createWrapper(), + } + ) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.payment_collection).toEqual(payment_collection) + }) +}) diff --git a/packages/medusa/src/api/middlewares/authenticate-customer.ts b/packages/medusa/src/api/middlewares/authenticate-customer.ts index 604ab22e47..766db36c7a 100644 --- a/packages/medusa/src/api/middlewares/authenticate-customer.ts +++ b/packages/medusa/src/api/middlewares/authenticate-customer.ts @@ -13,7 +13,11 @@ export default (): RequestHandler => { if (err) { return next(err) } - req.user = user + + if (user) { + req.user = user + } + return next() } )(req, res, next) diff --git a/packages/medusa/src/api/middlewares/error-handler.ts b/packages/medusa/src/api/middlewares/error-handler.ts index a7da41f4fd..382544e009 100644 --- a/packages/medusa/src/api/middlewares/error-handler.ts +++ b/packages/medusa/src/api/middlewares/error-handler.ts @@ -46,6 +46,9 @@ export default () => { case MedusaError.Types.UNAUTHORIZED: statusCode = 401 break + case MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR: + statusCode = 422 + break case MedusaError.Types.DUPLICATE_ERROR: statusCode = 422 errObj.code = INVALID_REQUEST_ERROR diff --git a/packages/medusa/src/api/middlewares/require-customer-authentication.ts b/packages/medusa/src/api/middlewares/require-customer-authentication.ts index 20cd53fb3c..6ad2af1a9b 100644 --- a/packages/medusa/src/api/middlewares/require-customer-authentication.ts +++ b/packages/medusa/src/api/middlewares/require-customer-authentication.ts @@ -3,6 +3,10 @@ import passport from "passport" export default (): RequestHandler => { return (req: Request, res: Response, next: NextFunction): void => { + if (req.user) { + return next() + } + passport.authenticate(["store-jwt", "bearer"], { session: false })( req, res, diff --git a/packages/medusa/src/api/routes/admin/payment-collections/get-payment-collection.ts b/packages/medusa/src/api/routes/admin/payment-collections/get-payment-collection.ts index ffe1d62cc6..4e417296f6 100644 --- a/packages/medusa/src/api/routes/admin/payment-collections/get-payment-collection.ts +++ b/packages/medusa/src/api/routes/admin/payment-collections/get-payment-collection.ts @@ -3,7 +3,7 @@ import { FindParams } from "../../../../types/common" /** * @oas [get] /payment-collections/{id} - * operationId: "GetPaymentCollectonsPaymentCollection" + * operationId: "GetPaymentCollectionsPaymentCollection" * summary: "Retrieve an PaymentCollection" * description: "Retrieves a PaymentCollection." * x-authenticated: true 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 ec14d5b8cf..3a9c3a5678 100644 --- a/packages/medusa/src/api/routes/admin/payment-collections/index.ts +++ b/packages/medusa/src/api/routes/admin/payment-collections/index.ts @@ -8,7 +8,7 @@ import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-edi import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import { GetPaymentCollectionsParams } from "./get-payment-collection" -import { AdminUpdatePaymentCollectionRequest } from "./update-payment-collection" +import { AdminUpdatePaymentCollectionsReq } from "./update-payment-collection" import { PaymentCollection } from "../../../../models" const route = Router() @@ -32,7 +32,7 @@ export default (app, container) => { route.post( "/:id", - transformBody(AdminUpdatePaymentCollectionRequest), + transformBody(AdminUpdatePaymentCollectionsReq), middlewares.wrap(require("./update-payment-collection").default) ) @@ -68,7 +68,7 @@ export const defaulPaymentCollectionRelations = [ "payments", ] -export type AdminPaymentCollectionRes = { +export type AdminPaymentCollectionsRes = { payment_collection: PaymentCollection } export type AdminPaymentCollectionDeleteRes = { 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 index 63c8f9033a..9ebe66054b 100644 --- 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 @@ -3,7 +3,7 @@ import { PaymentCollectionService } from "../../../../services" /** * @oas [post] /payment-collections/{id}/authorize - * operationId: "MarkAuthorizedPaymentCollectionsPaymentCollection" + * operationId: "PostPaymentCollectionsPaymentCollectionAuthorize" * summary: "Set the status of PaymentCollection as Authorized" * description: "Sets the status of PaymentCollection as Authorized." * x-authenticated: true diff --git a/packages/medusa/src/api/routes/admin/payment-collections/update-payment-collection.ts b/packages/medusa/src/api/routes/admin/payment-collections/update-payment-collection.ts index 2c5bd98339..b86d601330 100644 --- a/packages/medusa/src/api/routes/admin/payment-collections/update-payment-collection.ts +++ b/packages/medusa/src/api/routes/admin/payment-collections/update-payment-collection.ts @@ -75,7 +75,7 @@ import { PaymentCollectionService } from "../../../../services" */ export default async (req, res) => { const { id } = req.params - const data = req.validatedBody as AdminUpdatePaymentCollectionRequest + const data = req.validatedBody as AdminUpdatePaymentCollectionsReq const paymentCollectionService: PaymentCollectionService = req.scope.resolve( "paymentCollectionService" @@ -93,7 +93,7 @@ export default async (req, res) => { res.status(200).json({ payment_collection: paymentCollection }) } -export class AdminUpdatePaymentCollectionRequest { +export class AdminUpdatePaymentCollectionsReq { @IsString() @IsOptional() description?: string diff --git a/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts index b48f2b7d67..f86549b3fc 100644 --- a/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts @@ -45,6 +45,11 @@ describe("GET /store/order-edits/:id/complete", () => { `/store/order-edits/${orderEditId}/complete`, { flags: [OrderEditingFeatureFlag], + clientSession: { + jwt: { + user: IdMap.getId("lebron"), + }, + }, } ) }) diff --git a/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts index 29a9ba18ca..bd2faaf340 100644 --- a/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts @@ -77,7 +77,7 @@ export default async (req: Request, res: Response) => { paymentProviderService.withTransaction(manager) const orderEdit = await orderEditServiceTx.retrieve(id, { - relations: ["payment_collection"], + relations: ["payment_collection", "payment_collection.payments"], }) if (orderEdit.status === OrderEditStatus.CONFIRMED) { diff --git a/packages/medusa/src/api/routes/store/payment-collections/authorize-payment-collection.ts b/packages/medusa/src/api/routes/store/payment-collections/authorize-batch-payment-sessions.ts similarity index 58% rename from packages/medusa/src/api/routes/store/payment-collections/authorize-payment-collection.ts rename to packages/medusa/src/api/routes/store/payment-collections/authorize-batch-payment-sessions.ts index 0688bc3447..3d026929e2 100644 --- a/packages/medusa/src/api/routes/store/payment-collections/authorize-payment-collection.ts +++ b/packages/medusa/src/api/routes/store/payment-collections/authorize-batch-payment-sessions.ts @@ -1,13 +1,24 @@ +import { IsArray, IsString } from "class-validator" import { PaymentCollectionService } from "../../../../services" /** - * @oas [post] /payment-collections/{id}/authorize - * operationId: "PostPaymentCollectionsAuthorize" - * summary: "Authorize a Payment Collections" - * description: "Authorizes a Payment Collections." - * x-authenticated: true + * @oas [post] /payment-collections/{id}/sessions/batch/authorize + * operationId: "PostPaymentCollectionsSessionsBatchAuthorize" + * summary: "Authorize Payment Sessions of a Payment Collection" + * description: "Authorizes Payment Sessions of a Payment Collection." + * x-authenticated: false * parameters: * - (path) id=* {string} The ID of the Payment Collections. + * requestBody: + * content: + * application/json: + * schema: + * properties: + * session_ids: + * description: "List of Payment Session IDs to authorize." + * type: array + * items: + * type: string * x-codeSamples: * - lang: JavaScript * label: JS Client @@ -22,8 +33,7 @@ import { PaymentCollectionService } from "../../../../services" * - lang: Shell * label: cURL * source: | - * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/authorize' \ - * --header 'Authorization: Bearer {api_token}' + * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions/batch/authorize' * security: * - api_token: [] * - cookie_auth: [] @@ -35,7 +45,6 @@ import { PaymentCollectionService } from "../../../../services" * content: * application/json: * schema: - * type: object * properties: * payment_collection: * $ref: "#/components/schemas/payment_collection" @@ -53,15 +62,26 @@ import { PaymentCollectionService } from "../../../../services" * $ref: "#/components/responses/500_error" */ export default async (req, res) => { - const { payment_id } = req.params + const { id } = req.params + const data = + req.validatedBody as StorePostPaymentCollectionsBatchSessionsAuthorizeReq const paymentCollectionService: PaymentCollectionService = req.scope.resolve( "paymentCollectionService" ) - const payment_collection = await paymentCollectionService.authorize( - payment_id - ) + const payment_collection = + await paymentCollectionService.authorizePaymentSessions( + id, + data.session_ids, + req.request_context + ) - res.status(200).json({ payment_collection }) + res.status(207).json({ payment_collection }) +} + +export class StorePostPaymentCollectionsBatchSessionsAuthorizeReq { + @IsArray() + @IsString({ each: true }) + session_ids: string[] } diff --git a/packages/medusa/src/api/routes/store/payment-collections/authorize-payment-session.ts b/packages/medusa/src/api/routes/store/payment-collections/authorize-payment-session.ts new file mode 100644 index 0000000000..66004ef61d --- /dev/null +++ b/packages/medusa/src/api/routes/store/payment-collections/authorize-payment-session.ts @@ -0,0 +1,82 @@ +import { MedusaError } from "medusa-core-utils" +import { PaymentSessionStatus } from "../../../../models" +import { PaymentCollectionService } from "../../../../services" + +/** + * @oas [post] /payment-collections/{id}/sessions/{session_id}/authorize + * operationId: "PostPaymentCollectionsSessionsSessionAuthorize" + * summary: "Authorize a Payment Session of a Payment Collection" + * description: "Authorizes a Payment Session of a Payment Collection." + * x-authenticated: false + * parameters: + * - (path) id=* {string} The ID of the Payment Collections. + * - (path) session_id=* {string} The ID of the Payment Session. + * 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.paymentCollections.authorize(payment_id, session_id) + * .then(({ payment_collection }) => { + * console.log(payment_collection.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions/{session_id}/authorize' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Payment + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * payment_session: + * $ref: "#/components/schemas/payment_session" + * "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, session_id } = req.params + + const paymentCollectionService: PaymentCollectionService = req.scope.resolve( + "paymentCollectionService" + ) + + const payment_collection = + await paymentCollectionService.authorizePaymentSessions( + id, + [session_id], + req.request_context + ) + + const session = payment_collection.payment_sessions.find( + ({ id }) => id === session_id + ) + + if (session?.status !== PaymentSessionStatus.AUTHORIZED) { + throw new MedusaError( + MedusaError.Types.PAYMENT_AUTHORIZATION_ERROR, + `Failed to authorize Payment Session id "${id}"` + ) + } + + res.status(200).json({ payment_session: session }) +} diff --git a/packages/medusa/src/api/routes/store/payment-collections/get-payment-collection.ts b/packages/medusa/src/api/routes/store/payment-collections/get-payment-collection.ts index 90fda97592..c5e39a1ee2 100644 --- a/packages/medusa/src/api/routes/store/payment-collections/get-payment-collection.ts +++ b/packages/medusa/src/api/routes/store/payment-collections/get-payment-collection.ts @@ -3,10 +3,10 @@ import { FindParams } from "../../../../types/common" /** * @oas [get] /payment-collections/{id} - * operationId: "GetPaymentCollectonsPaymentCollection" + * operationId: "GetPaymentCollectionsPaymentCollection" * summary: "Retrieve an PaymentCollection" * description: "Retrieves a PaymentCollection." - * x-authenticated: true + * x-authenticated: false * parameters: * - (path) id=* {string} The ID of the PaymentCollection. * - (query) expand {string} Comma separated list of relations to include in the results. @@ -25,8 +25,7 @@ import { FindParams } from "../../../../types/common" * - lang: Shell * label: cURL * source: | - * curl --location --request GET 'https://medusa-url.com/store/payment-collections/{id}' \ - * --header 'Authorization: Bearer {api_token}' + * curl --location --request GET 'https://medusa-url.com/store/payment-collections/{id}' * security: * - api_token: [] * - cookie_auth: [] diff --git a/packages/medusa/src/api/routes/store/payment-collections/index.ts b/packages/medusa/src/api/routes/store/payment-collections/index.ts index 63468e44c6..013ad4fab2 100644 --- a/packages/medusa/src/api/routes/store/payment-collections/index.ts +++ b/packages/medusa/src/api/routes/store/payment-collections/index.ts @@ -8,10 +8,11 @@ import middlewares, { import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" -import { StoreManagePaymentCollectionSessionRequest } from "./manage-payment-sessions" -import { StoreRefreshPaymentCollectionSessionRequest } from "./refresh-payment-session" +import { StorePostPaymentCollectionsBatchSessionsReq } from "./manage-batch-payment-sessions" import { GetPaymentCollectionsParams } from "./get-payment-collection" import { PaymentCollection, PaymentSession } from "../../../../models" +import { StorePaymentCollectionSessionsReq } from "./manage-payment-session" +import { StorePostPaymentCollectionsBatchSessionsAuthorizeReq } from "./authorize-batch-payment-sessions" const route = Router() @@ -33,22 +34,33 @@ export default (app, container) => { ) route.post( - "/:id/authorize", - middlewares.wrap(require("./authorize-payment-collection").default) + "/:id/sessions/batch", + transformBody(StorePostPaymentCollectionsBatchSessionsReq), + middlewares.wrap(require("./manage-batch-payment-sessions").default) + ) + + route.post( + "/:id/sessions/batch/authorize", + transformBody(StorePostPaymentCollectionsBatchSessionsAuthorizeReq), + middlewares.wrap(require("./authorize-batch-payment-sessions").default) ) route.post( "/:id/sessions", - transformBody(StoreManagePaymentCollectionSessionRequest), - middlewares.wrap(require("./manage-payment-sessions").default) + transformBody(StorePaymentCollectionSessionsReq), + middlewares.wrap(require("./manage-payment-session").default) ) route.post( - "/:id/sessions/:session_id/refresh", - transformBody(StoreRefreshPaymentCollectionSessionRequest), + "/:id/sessions/:session_id", middlewares.wrap(require("./refresh-payment-session").default) ) + route.post( + "/:id/sessions/:session_id/authorize", + middlewares.wrap(require("./authorize-payment-session").default) + ) + return app } @@ -66,14 +78,16 @@ export const defaultPaymentCollectionFields = [ export const defaulPaymentCollectionRelations = ["region", "payment_sessions"] -export type StorePaymentCollectionRes = { +export type StorePaymentCollectionsRes = { payment_collection: PaymentCollection } -export type StorePaymentCollectionSessionRes = { +export type StorePaymentCollectionsSessionRes = { payment_session: PaymentSession } export * from "./get-payment-collection" -export * from "./manage-payment-sessions" +export * from "./manage-payment-session" +export * from "./manage-batch-payment-sessions" export * from "./refresh-payment-session" +export * from "./authorize-batch-payment-sessions" diff --git a/packages/medusa/src/api/routes/store/payment-collections/manage-payment-sessions.ts b/packages/medusa/src/api/routes/store/payment-collections/manage-batch-payment-sessions.ts similarity index 68% rename from packages/medusa/src/api/routes/store/payment-collections/manage-payment-sessions.ts rename to packages/medusa/src/api/routes/store/payment-collections/manage-batch-payment-sessions.ts index b93c774e1d..560d4f641e 100644 --- a/packages/medusa/src/api/routes/store/payment-collections/manage-payment-sessions.ts +++ b/packages/medusa/src/api/routes/store/payment-collections/manage-batch-payment-sessions.ts @@ -5,34 +5,29 @@ import { EntityManager } from "typeorm" import { PaymentCollectionService } from "../../../../services" /** - * @oas [post] /payment-collections/{id}/sessions - * operationId: "PostPaymentCollectionsSessions" - * summary: "Manage Payment Sessions from Payment Collections" - * description: "Manages Payment Sessions from Payment Collections." - * x-authenticated: true + * @oas [post] /payment-collections/{id}/sessions/batch + * operationId: "PostPaymentCollectionsPaymentCollectionSessionsBatch" + * summary: "Manage Multiple Payment Sessions from Payment Collections" + * description: "Manages Multiple Payment Sessions from Payment Collections." + * x-authenticated: false * parameters: * - (path) id=* {string} The ID of the Payment Collections. * requestBody: * content: * application/json: * schema: - * type: object * properties: * sessions: - * description: "An array or a single entry of payment sessions related to the Payment Collection. If the session_id is not provided the existing sessions not present will be deleted and the provided ones will be created." + * description: "An array of payment sessions related to the Payment Collection. If the session_id is not provided, existing sessions not present will be deleted and the provided ones will be created." * type: array * items: * required: * - provider_id - * - customer_id * - amount * properties: * provider_id: * type: string * description: The ID of the Payment Provider. - * customer_id: - * type: string - * description: "The ID of the Customer." * amount: * type: integer * description: "The amount ." @@ -50,15 +45,13 @@ import { PaymentCollectionService } from "../../../../services" * // Total amount = 10000 * * // Adding two new sessions - * medusa.paymentCollections.manageSessions(payment_id, [ + * medusa.paymentCollections.managePaymentSessionsBatch(payment_id, [ * { * provider_id: "stripe", - * customer_id: "cus_123", * amount: 5000, * }, * { * provider_id: "manual", - * customer_id: "cus_123", * amount: 5000, * }, * ]) @@ -67,20 +60,20 @@ import { PaymentCollectionService } from "../../../../services" * }); * * // Updating one session and removing the other - * medusa.paymentCollections.manageSessions(payment_id, { + * medusa.paymentCollections.managePaymentSessionsBatch(payment_id, [ + * { * provider_id: "stripe", - * customer_id: "cus_123", * amount: 10000, * session_id: "ps_123456" - * }) + * }, + * ]) * .then(({ payment_collection }) => { * console.log(payment_collection.id); * }); * - lang: Shell * label: cURL * source: | - * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions' \ - * --header 'Authorization: Bearer {api_token}' + * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions/batch' * security: * - api_token: [] * - cookie_auth: [] @@ -92,7 +85,6 @@ import { PaymentCollectionService } from "../../../../services" * content: * application/json: * schema: - * type: object * properties: * payment_collection: * $ref: "#/components/schemas/payment_collection" @@ -110,9 +102,11 @@ import { PaymentCollectionService } from "../../../../services" * $ref: "#/components/responses/500_error" */ export default async (req, res) => { - const data = req.validatedBody as StoreManagePaymentCollectionSessionRequest + const data = req.validatedBody as StorePostPaymentCollectionsBatchSessionsReq const { id } = req.params + const customerId = req.user?.customer_id + const paymentCollectionService: PaymentCollectionService = req.scope.resolve( "paymentCollectionService" ) @@ -122,20 +116,17 @@ export default async (req, res) => { async (transactionManager) => { return await paymentCollectionService .withTransaction(transactionManager) - .setPaymentSessions(id, data.sessions) + .setPaymentSessionsBatch(id, data.sessions, customerId) } ) res.status(200).json({ payment_collection: paymentCollection }) } -export class PaymentCollectionSessionInputRequest { +export class StorePostPaymentCollectionsSessionsReq { @IsString() provider_id: string - @IsString() - customer_id: string - @IsInt() @IsNotEmpty() amount: number @@ -145,12 +136,7 @@ export class PaymentCollectionSessionInputRequest { session_id?: string } -export class StoreManagePaymentCollectionSessionRequest { - @IsType([ - PaymentCollectionSessionInputRequest, - [PaymentCollectionSessionInputRequest], - ]) - sessions: - | PaymentCollectionSessionInputRequest - | PaymentCollectionSessionInputRequest[] +export class StorePostPaymentCollectionsBatchSessionsReq { + @IsType([[StorePostPaymentCollectionsSessionsReq]]) + sessions: StorePostPaymentCollectionsSessionsReq[] } diff --git a/packages/medusa/src/api/routes/store/payment-collections/manage-payment-session.ts b/packages/medusa/src/api/routes/store/payment-collections/manage-payment-session.ts new file mode 100644 index 0000000000..115cd0a283 --- /dev/null +++ b/packages/medusa/src/api/routes/store/payment-collections/manage-payment-session.ts @@ -0,0 +1,97 @@ +import { IsString } from "class-validator" + +import { EntityManager } from "typeorm" +import { PaymentCollectionService } from "../../../../services" + +/** + * @oas [post] /payment-collections/{id}/sessions + * operationId: "PostPaymentCollectionsSessions" + * summary: "Manage Payment Sessions from Payment Collections" + * description: "Manages Payment Sessions from Payment Collections." + * x-authenticated: false + * parameters: + * - (path) id=* {string} The ID of the Payment Collection. + * requestBody: + * content: + * application/json: + * schema: + * required: + * - provider_id + * properties: + * provider_id: + * type: string + * description: The ID of the Payment Provider. + * 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 + * + * // Total amount = 10000 + * + * // Adding a payment session + * medusa.paymentCollections.managePaymentSession(payment_id, { provider_id: "stripe" }) + * .then(({ payment_collection }) => { + * console.log(payment_collection.id); + * }); + * + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - Payment + * 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 data = req.validatedBody as StorePaymentCollectionSessionsReq + const { id } = req.params + + const customerId = req.user?.customer_id + + 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) + .setPaymentSession(id, data, customerId) + } + ) + + res.status(200).json({ payment_collection: paymentCollection }) +} + +export class StorePaymentCollectionSessionsReq { + @IsString() + provider_id: string +} diff --git a/packages/medusa/src/api/routes/store/payment-collections/refresh-payment-session.ts b/packages/medusa/src/api/routes/store/payment-collections/refresh-payment-session.ts index f3eb41c054..5e61af151e 100644 --- a/packages/medusa/src/api/routes/store/payment-collections/refresh-payment-session.ts +++ b/packages/medusa/src/api/routes/store/payment-collections/refresh-payment-session.ts @@ -4,10 +4,11 @@ import { EntityManager } from "typeorm" import { PaymentCollectionService } from "../../../../services" /** - * @oas [post] /payment-collections/{id}/sessions/{session_id}/refresh + * @oas [post] /payment-collections/{id}/sessions/{session_id} * operationId: PostPaymentCollectionsPaymentCollectionPaymentSessionsSession * summary: Refresh a Payment Session * description: "Refreshes a Payment Session to ensure that it is in sync with the Payment Collection." + * x-authenticated: false * parameters: * - (path) id=* {string} The id of the PaymentCollection. * - (path) session_id=* {string} The id of the Payment Session to be refreshed. @@ -33,13 +34,13 @@ import { PaymentCollectionService } from "../../../../services" * import Medusa from "@medusajs/medusa-js" * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) * medusa.paymentCollections.refreshPaymentSession(payment_collection_id, session_id, payload) - * .then(({ payment_collection }) => { - * console.log(payment_collection.id); + * .then(({ payment_session }) => { + * console.log(payment_session.id); * }); * - lang: Shell * label: cURL * source: | - * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions/{session_id}/refresh' + * curl --location --request POST 'https://medusa-url.com/store/payment-collections/{id}/sessions/{session_id}' * tags: * - PaymentCollection * responses: @@ -64,29 +65,22 @@ import { PaymentCollectionService } from "../../../../services" * $ref: "#/components/responses/500_error" */ export default async (req, res) => { - const data = req.validatedBody as StoreRefreshPaymentCollectionSessionRequest const { id, session_id } = req.params const paymentCollectionService: PaymentCollectionService = req.scope.resolve( "paymentCollectionService" ) + const customerId = req.user?.customer_id + const manager: EntityManager = req.scope.resolve("manager") const paymentSession = await manager.transaction( async (transactionManager) => { return await paymentCollectionService .withTransaction(transactionManager) - .refreshPaymentSession(id, session_id, data) + .refreshPaymentSession(id, session_id, customerId) } ) res.status(200).json({ payment_session: paymentSession }) } - -export class StoreRefreshPaymentCollectionSessionRequest { - @IsString() - provider_id: string - - @IsString() - customer_id: string -} diff --git a/packages/medusa/src/services/__mocks__/customer.js b/packages/medusa/src/services/__mocks__/customer.js index 067c0d97dc..d6d80a8dd7 100644 --- a/packages/medusa/src/services/__mocks__/customer.js +++ b/packages/medusa/src/services/__mocks__/customer.js @@ -29,6 +29,7 @@ export const CustomerServiceMock = { password_hash: "1234", }) } + return Promise.resolve() }), retrieveByEmail: jest.fn().mockImplementation((email) => { if (email === "lebron@james.com") { diff --git a/packages/medusa/src/services/__tests__/payment-collection.ts b/packages/medusa/src/services/__tests__/payment-collection.ts index 169bebaaf7..ad2de73bfb 100644 --- a/packages/medusa/src/services/__tests__/payment-collection.ts +++ b/packages/medusa/src/services/__tests__/payment-collection.ts @@ -17,7 +17,7 @@ import { PaymentProviderServiceMock, } from "../__mocks__/payment-provider" import { CustomerServiceMock } from "../__mocks__/customer" -import { PaymentCollectionSessionInput } from "../../types/payment-collection" +import { PaymentCollectionsSessionsBatchInput } from "../../types/payment-collection" describe("PaymentCollectionService", () => { afterEach(() => { @@ -376,20 +376,18 @@ describe("PaymentCollectionService", () => { expect(entity).rejects.toThrow(Error) }) - describe("Manage Payment Sessions", () => { + describe("Manage Single Payment Session", () => { afterEach(() => { jest.clearAllMocks() }) it("should throw error if payment collection doesn't have the correct status", async () => { - const inp: PaymentCollectionSessionInput = { - amount: 100, - provider_id: IdMap.getId("region1_provider1"), - customer_id: "customer1", - } - const ret = paymentCollectionService.setPaymentSessions( + const ret = paymentCollectionService.setPaymentSession( IdMap.getId("payment-collection-id2"), - inp + { + provider_id: IdMap.getId("region1_provider1"), + }, + "customer1" ) expect(ret).rejects.toThrowError( @@ -400,15 +398,120 @@ describe("PaymentCollectionService", () => { expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0) }) - it("should throw error if amount is different than requested", async () => { - const inp: PaymentCollectionSessionInput = { - amount: 101, - provider_id: IdMap.getId("region1_provider1"), - customer_id: "customer1", - } - const ret = paymentCollectionService.setPaymentSessions( + it("should ignore session if provider doesn't belong to the region", async () => { + const multiRet = paymentCollectionService.setPaymentSession( IdMap.getId("payment-collection-id1"), - inp + { + provider_id: IdMap.getId("region1_invalid_provider"), + }, + "customer1" + ) + + expect(multiRet).rejects.toThrow(`Payment provider not found`) + expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0) + }) + + it("should add a new session", async () => { + await paymentCollectionService.setPaymentSession( + IdMap.getId("payment-collection-id1"), + { + provider_id: IdMap.getId("region1_provider2"), + }, + "lebron" + ) + + expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( + 1 + ) + expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) + }) + + it("should update an existing one", async () => { + await paymentCollectionService.setPaymentSession( + IdMap.getId("payment-collection-id1"), + { + provider_id: IdMap.getId("region1_provider1"), + }, + "lebron" + ) + + expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( + 0 + ) + expect(PaymentProviderServiceMock.updateSessionNew).toHaveBeenCalledTimes( + 1 + ) + expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) + }) + + it("should add a new session and delete existing one", async () => { + const inp: PaymentCollectionsSessionsBatchInput[] = [ + { + amount: 100, + provider_id: IdMap.getId("region1_provider1"), + }, + ] + await paymentCollectionService.setPaymentSessionsBatch( + IdMap.getId("payment-collection-session"), + inp, + IdMap.getId("lebron") + ) + + expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( + 1 + ) + expect(PaymentProviderServiceMock.updateSessionNew).toHaveBeenCalledTimes( + 0 + ) + expect(paymentCollectionRepository.deleteMultiple).toHaveBeenCalledTimes( + 1 + ) + + expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1) + }) + }) + + describe("Manage Multiple Payment Sessions", () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it("should throw error if payment collection doesn't have the correct status", async () => { + const inp: PaymentCollectionsSessionsBatchInput[] = [ + { + amount: 100, + provider_id: IdMap.getId("region1_provider1"), + }, + ] + const ret = paymentCollectionService.setPaymentSessionsBatch( + IdMap.getId("payment-collection-id2"), + inp, + "customer1" + ) + + expect(ret).rejects.toThrowError( + new Error( + `Cannot set payment sessions for a payment collection with status ${PaymentCollectionStatus.AUTHORIZED}` + ) + ) + + expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0) + }) + + it("should throw error if amount is different than requested", async () => { + const inp: PaymentCollectionsSessionsBatchInput[] = [ + { + amount: 101, + provider_id: IdMap.getId("region1_provider1"), + }, + ] + + const ret = paymentCollectionService.setPaymentSessionsBatch( + IdMap.getId("payment-collection-id1"), + inp, + "customer1" ) expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( @@ -418,21 +521,20 @@ describe("PaymentCollectionService", () => { `The sum of sessions is not equal to 100 on Payment Collection` ) - const multInp: PaymentCollectionSessionInput[] = [ + const multInp: PaymentCollectionsSessionsBatchInput[] = [ { amount: 51, provider_id: IdMap.getId("region1_provider1"), - customer_id: "customer1", }, { amount: 50, provider_id: IdMap.getId("region1_provider2"), - customer_id: "customer1", }, ] - const multiRet = paymentCollectionService.setPaymentSessions( + const multiRet = paymentCollectionService.setPaymentSessionsBatch( IdMap.getId("payment-collection-id1"), - multInp + multInp, + "customer1" ) expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( @@ -444,21 +546,20 @@ describe("PaymentCollectionService", () => { }) it("should ignore sessions where provider doesn't belong to the region", async () => { - const multInp: PaymentCollectionSessionInput[] = [ + const multInp: PaymentCollectionsSessionsBatchInput[] = [ { amount: 50, provider_id: IdMap.getId("region1_provider1"), - customer_id: "customer1", }, { amount: 50, provider_id: IdMap.getId("region1_invalid_provider"), - customer_id: "customer1", }, ] - const multiRet = paymentCollectionService.setPaymentSessions( + const multiRet = paymentCollectionService.setPaymentSessionsBatch( IdMap.getId("payment-collection-id1"), - multInp + multInp, + "customer1" ) expect(multiRet).rejects.toThrow( @@ -468,22 +569,21 @@ describe("PaymentCollectionService", () => { }) it("should add a new session and update existing one", async () => { - const inp: PaymentCollectionSessionInput[] = [ + const inp: PaymentCollectionsSessionsBatchInput[] = [ { session_id: IdMap.getId("payCol_session1"), amount: 50, provider_id: IdMap.getId("region1_provider1"), - customer_id: IdMap.getId("lebron"), }, { amount: 50, provider_id: IdMap.getId("region1_provider1"), - customer_id: IdMap.getId("lebron"), }, ] - await paymentCollectionService.setPaymentSessions( + await paymentCollectionService.setPaymentSessionsBatch( IdMap.getId("payment-collection-session"), - inp + inp, + "lebron" ) expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( @@ -497,16 +597,16 @@ describe("PaymentCollectionService", () => { }) it("should add a new session and delete existing one", async () => { - const inp: PaymentCollectionSessionInput[] = [ + const inp: PaymentCollectionsSessionsBatchInput[] = [ { amount: 100, provider_id: IdMap.getId("region1_provider1"), - customer_id: IdMap.getId("lebron"), }, ] - await paymentCollectionService.setPaymentSessions( + await paymentCollectionService.setPaymentSessionsBatch( IdMap.getId("payment-collection-session"), - inp + inp, + IdMap.getId("lebron") ) expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes( @@ -526,10 +626,7 @@ describe("PaymentCollectionService", () => { await paymentCollectionService.refreshPaymentSession( IdMap.getId("payment-collection-session"), IdMap.getId("payCol_session1"), - { - customer_id: "customer1", - provider_id: IdMap.getId("region1_provider1"), - } + "customer1" ) expect( @@ -545,10 +642,7 @@ describe("PaymentCollectionService", () => { const sess = paymentCollectionService.refreshPaymentSession( IdMap.getId("payment-collection-session"), IdMap.getId("payCol_session-not-found"), - { - customer_id: "customer1", - provider_id: IdMap.getId("region1_provider1"), - } + "customer1" ) expect(sess).rejects.toThrow( @@ -567,19 +661,22 @@ describe("PaymentCollectionService", () => { jest.clearAllMocks() }) - it("should mark as paid if amount is 0", async () => { - await paymentCollectionService.authorize( - IdMap.getId("payment-collection-zero") + it("should mark as authorized if amount is 0", async () => { + const auth = await paymentCollectionService.authorizePaymentSessions( + IdMap.getId("payment-collection-zero"), + [] ) expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( 0 ) + expect(auth.status).toBe(PaymentCollectionStatus.AUTHORIZED) }) it("should reject payment collection without payment sessions", async () => { - const ret = paymentCollectionService.authorize( - IdMap.getId("payment-collection-no-session") + const ret = paymentCollectionService.authorizePaymentSessions( + IdMap.getId("payment-collection-no-session"), + [] ) expect(ret).rejects.toThrowError( @@ -590,8 +687,9 @@ describe("PaymentCollectionService", () => { }) it("should call authorizePayments for all sessions", async () => { - await paymentCollectionService.authorize( - IdMap.getId("payment-collection-not-authorized") + await paymentCollectionService.authorizePaymentSessions( + IdMap.getId("payment-collection-not-authorized"), + [IdMap.getId("payCol_session1"), IdMap.getId("payCol_session2")] ) expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( @@ -604,8 +702,9 @@ describe("PaymentCollectionService", () => { }) it("should skip authorized sessions - partially authorized", async () => { - await paymentCollectionService.authorize( - IdMap.getId("payment-collection-partial") + await paymentCollectionService.authorizePaymentSessions( + IdMap.getId("payment-collection-partial"), + [IdMap.getId("payCol_session1"), IdMap.getId("payCol_session2")] ) expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( @@ -618,8 +717,9 @@ describe("PaymentCollectionService", () => { }) it("should skip authorized sessions - fully authorized", async () => { - await paymentCollectionService.authorize( - IdMap.getId("payment-collection-fully") + await paymentCollectionService.authorizePaymentSessions( + IdMap.getId("payment-collection-fully"), + [IdMap.getId("payCol_session1"), IdMap.getId("payCol_session2")] ) expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes( diff --git a/packages/medusa/src/services/payment-collection.ts b/packages/medusa/src/services/payment-collection.ts index 53d1d7546c..4605e7cc98 100644 --- a/packages/medusa/src/services/payment-collection.ts +++ b/packages/medusa/src/services/payment-collection.ts @@ -1,11 +1,10 @@ -import { DeepPartial, EntityManager, Equal } from "typeorm" +import { DeepPartial, EntityManager } from "typeorm" import { MedusaError } from "medusa-core-utils" import { FindConfig } from "../types/common" import { buildQuery, isDefined, setMetadata } from "../utils" import { PaymentCollectionRepository } from "../repositories/payment-collection" import { - Customer, PaymentCollection, PaymentCollectionStatus, PaymentSession, @@ -16,12 +15,12 @@ import { CustomerService, EventBusService, PaymentProviderService, - PaymentService, } from "./index" import { CreatePaymentCollectionInput, - PaymentCollectionSessionInput, + PaymentCollectionsSessionsBatchInput, + PaymentCollectionsSessionsInput, PaymentProviderDataInput, } from "../types/payment-collection" @@ -66,6 +65,12 @@ export default class PaymentCollectionService extends TransactionBaseService { this.customerService_ = customerService } + /** + * Retrieves a payment collection by id. + * @param paymentCollectionId - the id of the payment collection + * @param config - the config to retrieve the payment collection + * @return the payment collection. + */ async retrieve( paymentCollectionId: string, config: FindConfig = {} @@ -75,9 +80,11 @@ export default class PaymentCollectionService extends TransactionBaseService { this.paymentCollectionRepository_ ) - const query = buildQuery({ id: paymentCollectionId }, config) - - const paymentCollection = await paymentCollectionRepository.find(query) + let paymentCollection: PaymentCollection[] = [] + if (paymentCollectionId) { + const query = buildQuery({ id: paymentCollectionId }, config) + paymentCollection = await paymentCollectionRepository.find(query) + } if (!paymentCollection.length) { throw new MedusaError( @@ -89,6 +96,11 @@ export default class PaymentCollectionService extends TransactionBaseService { return paymentCollection[0] } + /** + * Creates a new payment collection. + * @param data - info to create the payment collection + * @return the payment collection created. + */ async create(data: CreatePaymentCollectionInput): Promise { return await this.atomicPhase_(async (manager) => { const paymentCollectionRepository = manager.getCustomRepository( @@ -118,6 +130,12 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } + /** + * Updates a payment collection. + * @param paymentCollectionId - the id of the payment collection to update + * @param data - info to be updated + * @return the payment collection updated. + */ async update( paymentCollectionId: string, data: DeepPartial @@ -147,6 +165,11 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } + /** + * Deletes a payment collection. + * @param paymentCollectionId - the id of the payment collection to be removed + * @return the payment collection removed. + */ async delete( paymentCollectionId: string ): Promise { @@ -187,18 +210,24 @@ export default class PaymentCollectionService extends TransactionBaseService { private isValidTotalAmount( total: number, - sessionsInput: PaymentCollectionSessionInput[] + sessionsInput: PaymentCollectionsSessionsBatchInput[] ): boolean { const sum = sessionsInput.reduce((cur, sess) => cur + sess.amount, 0) return total === sum } - async setPaymentSessions( + /** + * Manages multiple payment sessions of a payment collection. + * @param paymentCollectionId - the id of the payment collection + * @param sessionsInput - array containing payment session info + * @param customerId - the id of the customer + * @return the payment collection and its payment sessions. + */ + async setPaymentSessionsBatch( paymentCollectionId: string, - sessions: PaymentCollectionSessionInput[] | PaymentCollectionSessionInput + sessionsInput: PaymentCollectionsSessionsBatchInput[], + customerId: string ): Promise { - let sessionsInput = Array.isArray(sessions) ? sessions : [sessions] - return await this.atomicPhase_(async (manager: EntityManager) => { const paymentCollectionRepository = manager.getCustomRepository( this.paymentCollectionRepository_ @@ -228,20 +257,19 @@ export default class PaymentCollectionService extends TransactionBaseService { ) } - let customer: Customer | undefined = undefined + const customer = !isDefined(customerId) + ? null + : await this.customerService_ + .withTransaction(manager) + .retrieve(customerId, { + select: ["id", "email", "metadata"], + }) + .catch(() => null) const selectedSessionIds: string[] = [] const paymentSessions: PaymentSession[] = [] for (const session of sessionsInput) { - if (!customer) { - customer = await this.customerService_ - .withTransaction(manager) - .retrieve(session.customer_id, { - select: ["id", "email", "metadata"], - }) - } - const existingSession = payCol.payment_sessions?.find( (sess) => session.session_id === sess?.id ) @@ -252,9 +280,6 @@ export default class PaymentCollectionService extends TransactionBaseService { amount: session.amount, provider_id: session.provider_id, customer, - metadata: { - resource_id: payCol.id, - }, } if (existingSession) { @@ -275,12 +300,22 @@ export default class PaymentCollectionService extends TransactionBaseService { } if (payCol.payment_sessions?.length) { - const removeIds: string[] = payCol.payment_sessions - .map((sess) => sess.id) - .filter((id) => !selectedSessionIds.includes(id)) + const removeSessions: PaymentSession[] = payCol.payment_sessions.filter( + ({ id }) => !selectedSessionIds.includes(id) + ) - if (removeIds.length) { - await paymentCollectionRepository.deleteMultiple(removeIds) + if (removeSessions.length) { + await paymentCollectionRepository.deleteMultiple( + removeSessions.map((sess) => sess.id) + ) + + Promise.all( + removeSessions.map(async (sess) => + this.paymentProviderService_ + .withTransaction(manager) + .deleteSessionNew(sess) + ) + ).catch(() => void 0) } } @@ -290,10 +325,116 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } + /** + * Manages a single payment sessions of a payment collection. + * @param paymentCollectionId - the id of the payment collection + * @param sessionsInput - object containing payment session info + * @param customerId - the id of the customer + * @return the payment collection and its payment session. + */ + async setPaymentSession( + paymentCollectionId: string, + sessionInput: PaymentCollectionsSessionsInput, + customerId: string + ): Promise { + return await this.atomicPhase_(async (manager: EntityManager) => { + const paymentCollectionRepository = manager.getCustomRepository( + this.paymentCollectionRepository_ + ) + + const payCol = await this.retrieve(paymentCollectionId, { + relations: ["region", "region.payment_providers", "payment_sessions"], + }) + + if (payCol.status !== PaymentCollectionStatus.NOT_PAID) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot set payment sessions for a payment collection with status ${payCol.status}` + ) + } + + const hasProvider = payCol?.region?.payment_providers + .map((p) => p.id) + .includes(sessionInput.provider_id) + + if (!hasProvider) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Payment provider not found" + ) + } + + const customer = !isDefined(customerId) + ? null + : await this.customerService_ + .withTransaction(manager) + .retrieve(customerId, { + select: ["id", "email", "metadata"], + }) + .catch(() => null) + + const paymentSessions: PaymentSession[] = [] + const inputData: PaymentProviderDataInput = { + resource_id: payCol.id, + currency_code: payCol.currency_code, + amount: payCol.amount, + provider_id: sessionInput.provider_id, + customer, + } + + const existingSession = payCol.payment_sessions?.find( + (sess) => sessionInput.provider_id === sess?.provider_id + ) + + if (existingSession) { + const paymentSession = await this.paymentProviderService_ + .withTransaction(manager) + .updateSessionNew(existingSession, inputData) + + paymentSessions.push(paymentSession) + } else { + const paymentSession = await this.paymentProviderService_ + .withTransaction(manager) + .createSessionNew(inputData) + + paymentSessions.push(paymentSession) + + const removeSessions: PaymentSession[] = payCol.payment_sessions.filter( + ({ id }) => id != paymentSession.id + ) + + if (removeSessions.length) { + await paymentCollectionRepository.deleteMultiple( + removeSessions.map((sess) => sess.id) + ) + + Promise.all( + removeSessions.map(async (sess) => + this.paymentProviderService_ + .withTransaction(manager) + .deleteSessionNew(sess) + ) + ).catch(() => void 0) + } + } + + payCol.payment_sessions = paymentSessions + + return await paymentCollectionRepository.save(payCol) + }) + } + + /** + * Removes and recreate a payment session of a payment collection. + * @param paymentCollectionId - the id of the payment collection + * @param sessionId - the id of the payment session to be replaced + * @param customerId - the id of the customer + * @return the new payment session created. + */ async refreshPaymentSession( paymentCollectionId: string, sessionId: string, - sessionInput: Omit + customerId: string ): Promise { return await this.atomicPhase_(async (manager: EntityManager) => { const paymentCollectionRepository = manager.getCustomRepository( @@ -330,11 +471,14 @@ export default class PaymentCollectionService extends TransactionBaseService { ) } - const customer = await this.customerService_ - .withTransaction(manager) - .retrieve(sessionInput.customer_id, { - select: ["id", "email", "metadata"], - }) + const customer = !isDefined(customerId) + ? null + : await this.customerService_ + .withTransaction(manager) + .retrieve(customerId, { + select: ["id", "email", "metadata"], + }) + .catch(() => null) const inputData: PaymentProviderDataInput = { resource_id: payCol.id, @@ -365,6 +509,11 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } + /** + * Marks a payment collection as authorized bypassing the payment flow. + * @param paymentCollectionId - the id of the payment collection + * @return the payment session authorized. + */ async markAsAuthorized( paymentCollectionId: string ): Promise { @@ -387,8 +536,16 @@ export default class PaymentCollectionService extends TransactionBaseService { }) } - async authorize( + /** + * Authorizes the payment sessions of a payment collection. + * @param paymentCollectionId - the id of the payment collection + * @param sessionIds - array of payment session ids to be authorized + * @param context - additional data required by payment providers + * @return the payment collection and its payment session. + */ + async authorizePaymentSessions( paymentCollectionId: string, + sessionIds: string[], context: Record = {} ): Promise { return await this.atomicPhase_(async (manager: EntityManager) => { @@ -404,9 +561,9 @@ export default class PaymentCollectionService extends TransactionBaseService { return payCol } - // If cart total is 0, we don't perform anything payment related if (payCol.amount <= 0) { payCol.authorized_amount = 0 + payCol.status = PaymentCollectionStatus.AUTHORIZED return await paymentCollectionRepository.save(payCol) } @@ -426,6 +583,10 @@ export default class PaymentCollectionService extends TransactionBaseService { continue } + if (!sessionIds.includes(session.id)) { + continue + } + const auth = await this.paymentProviderService_ .withTransaction(manager) .authorizePayment(session, context) diff --git a/packages/medusa/src/services/payment-provider.ts b/packages/medusa/src/services/payment-provider.ts index 430c54bbc3..c1002ee44f 100644 --- a/packages/medusa/src/services/payment-provider.ts +++ b/packages/medusa/src/services/payment-provider.ts @@ -349,6 +349,15 @@ export default class PaymentProviderService extends TransactionBaseService { }) } + async deleteSessionNew(paymentSession: PaymentSession): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const provider = this.retrieveProvider(paymentSession.provider_id) + return await provider + .withTransaction(transactionManager) + .deletePayment(paymentSession) + }) + } + /** * Finds a provider given an id * @param {string} providerId - the id of the provider to get diff --git a/packages/medusa/src/services/payment.ts b/packages/medusa/src/services/payment.ts index 056f8f4a5c..c6f5252495 100644 --- a/packages/medusa/src/services/payment.ts +++ b/packages/medusa/src/services/payment.ts @@ -52,6 +52,12 @@ export default class PaymentService extends TransactionBaseService { this.eventBusService_ = eventBusService } + /** + * Retrieves a payment by id. + * @param paymentId - the id of the payment + * @param config - the config to retrieve the payment + * @return the payment. + */ async retrieve( paymentId: string, config: FindConfig = {} @@ -75,6 +81,11 @@ export default class PaymentService extends TransactionBaseService { return payment[0] } + /** + * Created a new payment. + * @param paymentInput - info to create the payment + * @return the payment created. + */ async create(paymentInput: PaymentDataInput): Promise { return await this.atomicPhase_(async (manager: EntityManager) => { const { data, currency_code, amount, provider_id } = paymentInput @@ -100,6 +111,12 @@ export default class PaymentService extends TransactionBaseService { }) } + /** + * Updates a payment in order to link it to an order or a swap. + * @param paymentId - the id of the payment + * @param data - order_id or swap_id to link the payment + * @return the payment updated. + */ async update( paymentId: string, data: { order_id?: string; swap_id?: string } @@ -129,6 +146,11 @@ export default class PaymentService extends TransactionBaseService { }) } + /** + * Captures a payment. + * @param paymentOrId - the id or the class instance of the payment + * @return the payment captured. + */ async capture(paymentOrId: string | Payment): Promise { const payment = typeof paymentOrId === "string" @@ -170,6 +192,14 @@ export default class PaymentService extends TransactionBaseService { }) } + /** + * refunds a payment. + * @param paymentOrId - the id or the class instance of the payment + * @param amount - the amount to be refunded from the payment + * @param reason - the refund reason + * @param note - additional note of the refund + * @return the refund created. + */ async refund( paymentOrId: string | Payment, amount: number, diff --git a/packages/medusa/src/types/payment-collection.ts b/packages/medusa/src/types/payment-collection.ts index 549469c7fa..0463b990c3 100644 --- a/packages/medusa/src/types/payment-collection.ts +++ b/packages/medusa/src/types/payment-collection.ts @@ -15,22 +15,24 @@ export type CreatePaymentCollectionInput = { description?: string } -export type PaymentCollectionSessionInput = { +export type PaymentCollectionsSessionsBatchInput = { provider_id: string amount: number session_id?: string - customer_id: string +} + +export type PaymentCollectionsSessionsInput = { + provider_id: string } export type PaymentProviderDataInput = { resource_id: string - customer: Partial + customer: Partial | null currency_code: string provider_id: string amount: number cart_id?: string cart?: Cart - metadata?: any } export const defaultPaymentCollectionRelations = [ "region",