feat(medusa,medusa-react): PaymentCollection support (#2659)

* chore: medusa react, order edit complete fix and single payment session
This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2022-12-07 12:39:35 -03:00
1 parent 42d9c7222b
commit 15c667fbd3
54 files changed
+2001 -261

No files matched your search

@@ -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)
@@ -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
@@ -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,
@@ -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
@@ -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 = {
@@ -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
@@ -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
@@ -45,6 +45,11 @@ describe("GET /store/order-edits/:id/complete", () => {
`/store/order-edits/${orderEditId}/complete`,
{
flags: [OrderEditingFeatureFlag],
clientSession: {
jwt: {
user: IdMap.getId("lebron"),
},
},
}
)
})
@@ -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) {
@@ -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[]
}
@@ -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 })
}
@@ -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: []
@@ -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"
@@ -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[]
}
@@ -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
}
@@ -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
}
@@ -29,6 +29,7 @@ export const CustomerServiceMock = {
password_hash: "1234",
})
}
return Promise.resolve()
}),
retrieveByEmail: jest.fn().mockImplementation((email) => {
if (email === "lebron@james.com") {
@@ -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(
@@ -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<PaymentCollection> = {}
@@ -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<PaymentCollection> {
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<PaymentCollection>
@@ -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<PaymentCollection | undefined> {
@@ -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<PaymentCollection> {
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<PaymentCollection> {
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<PaymentCollectionSessionInput, "amount">
customerId: string
): Promise<PaymentSession> {
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<PaymentCollection> {
@@ -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<string, unknown> = {}
): Promise<PaymentCollection> {
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)
@@ -349,6 +349,15 @@ export default class PaymentProviderService extends TransactionBaseService {
})
}
async deleteSessionNew(paymentSession: PaymentSession): Promise<void> {
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
+30
View File
@@ -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<Payment> = {}
@@ -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<Payment> {
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<Payment> {
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,
@@ -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>
customer: Partial<Customer> | null
currency_code: string
provider_id: string
amount: number
cart_id?: string
cart?: Cart
metadata?: any
}
export const defaultPaymentCollectionRelations = [
"region",