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
2022-12-07 12:39:35 -03:00
committed by GitHub
parent 42d9c7222b
commit 15c667fbd3
54 changed files with 2001 additions and 261 deletions

View File

@@ -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

View File

@@ -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}`,

View File

@@ -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)

View File

@@ -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)
})
})
})

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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]
}

View File

@@ -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 = {

View File

@@ -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<string, any> = {}
): ResponsePromise<AdminPaymentCollectionRes> {
): ResponsePromise<AdminPaymentCollectionsRes> {
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<string, any> = {}
): ResponsePromise<AdminPaymentCollectionRes> {
): ResponsePromise<AdminPaymentCollectionsRes> {
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<string, any> = {}
): ResponsePromise<AdminPaymentCollectionRes> {
): ResponsePromise<AdminPaymentCollectionsRes> {
const path = `/admin/payment-collections/${id}/authorize`
return this.client.request("POST", path, undefined, {}, customHeaders)
}

View File

@@ -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<string, any> = {}
): ResponsePromise<StorePaymentCollectionRes> {
): ResponsePromise<StorePaymentCollectionsRes> {
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<string, any> = {}
): ResponsePromise<StorePaymentCollectionRes> {
const path = `/store/payment-collections/${id}/authorize`
): ResponsePromise<StorePaymentCollectionsRes> {
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<string, any> = {}
): ResponsePromise<StorePaymentCollectionRes> {
): ResponsePromise<StorePaymentCollectionsRes> {
const path = `/store/payment-collections/${id}/sessions/batch/authorize`
return this.client.request("POST", path, payload, {}, customHeaders)
}
managePaymentSessionsBatch(
id: string,
payload: StorePostPaymentCollectionsBatchSessionsReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<StorePaymentCollectionsRes> {
const path = `/store/payment-collections/${id}/sessions/batch`
return this.client.request("POST", path, payload, {}, customHeaders)
}
managePaymentSession(
id: string,
payload: StorePaymentCollectionSessionsReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<StorePaymentCollectionsRes> {
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<string, any> = {}
): ResponsePromise<StorePaymentCollectionSessionRes> {
const path = `/store/payment-collections/${id}/sessions/${session_id}/refresh`
return this.client.request("POST", path, payload, {}, customHeaders)
): ResponsePromise<StorePaymentCollectionsSessionRes> {
const path = `/store/payment-collections/${id}/sessions/${session_id}`
return this.client.request("POST", path, undefined, {}, customHeaders)
}
}

View File

@@ -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) => {

View File

@@ -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)) {

View File

@@ -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
}
}
}

View File

@@ -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,
},
})
)
}),
]

View File

@@ -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,
},
})
)
}
),
]

View File

@@ -30,3 +30,5 @@ export * from "./tax-rates"
export * from "./uploads"
export * from "./users"
export * from "./variants"
export * from "./payment-collections"
export * from "./payments"

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -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<AdminPaymentCollectionDeleteRes>,
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<AdminPaymentCollectionsRes>,
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<AdminPaymentCollectionsRes>,
Error,
void
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.paymentCollections.markAsAuthorized(id),
buildOptions(
queryClient,
[
adminPaymentCollectionQueryKeys.detail(id),
adminPaymentCollectionQueryKeys.lists(),
],
options
)
)
}

View File

@@ -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<AdminPaymentCollectionsRes>,
Error,
ReturnType<AdminPaymentCollectionKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminPaymentCollectionQueryKeys.detail(id),
() => client.admin.paymentCollections.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -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<Response<AdminPaymentRes>, 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<AdminRefundRes>,
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
)
)
}

View File

@@ -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<typeof PAYMENT_QUERY_KEY>(PAYMENT_QUERY_KEY)
type AdminPaymentKey = typeof adminPaymentQueryKeys
export const useAdminPayment = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminPaymentRes>,
Error,
ReturnType<AdminPaymentKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminPaymentQueryKeys.detail(id),
() => client.admin.payments.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -13,3 +13,4 @@ export * from "./returns/"
export * from "./gift-cards/"
export * from "./line-items/"
export * from "./collections"
export * from "./payment-collections"

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -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<StorePaymentCollectionsRes>,
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<StorePaymentCollectionsRes>,
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<StorePaymentCollectionsRes>,
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<StorePaymentCollectionsRes>,
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<StorePaymentCollectionsSessionRes>,
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
)
)
}

View File

@@ -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<StorePaymentCollectionsRes>,
Error,
ReturnType<PaymentCollectionKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
paymentCollectionQueryKeys.detail(id),
() => client.paymentCollections.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -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",
})
)
})
})

View File

@@ -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)
})
})

View File

@@ -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",
})
)
})
})

View File

@@ -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)
})
})

View File

@@ -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,
})
)
})
})

View File

@@ -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)
})
})

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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

View File

@@ -45,6 +45,11 @@ describe("GET /store/order-edits/:id/complete", () => {
`/store/order-edits/${orderEditId}/complete`,
{
flags: [OrderEditingFeatureFlag],
clientSession: {
jwt: {
user: IdMap.getId("lebron"),
},
},
}
)
})

View File

@@ -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) {

View File

@@ -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[]
}

View File

@@ -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 })
}

View File

@@ -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: []

View File

@@ -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"

View File

@@ -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[]
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -29,6 +29,7 @@ export const CustomerServiceMock = {
password_hash: "1234",
})
}
return Promise.resolve()
}),
retrieveByEmail: jest.fn().mockImplementation((email) => {
if (email === "lebron@james.com") {

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

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,

View File

@@ -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",