feat(medusa): Payment Collection endpoints (#2525)

This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2022-11-11 18:28:45 +01:00
1 parent 27184f3878
commit 755ba90c05
58 files changed
+1190 -484

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ export * from "./routes/admin/gift-cards"
export * from "./routes/admin/invites"
export * from "./routes/admin/notes"
export * from "./routes/admin/notifications"
export * from "./routes/admin/payment-collections"
export * from "./routes/admin/payments"
export * from "./routes/admin/order-edits"
export * from "./routes/admin/orders"
export * from "./routes/admin/price-lists"
@@ -33,6 +33,8 @@ import taxRateRoutes from "./tax-rates"
import uploadRoutes from "./uploads"
import userRoutes, { unauthenticatedUserRoutes } from "./users"
import variantRoutes from "./variants"
import paymentCollectionRoutes from "./payment-collections"
import paymentRoutes from "./payments"
const route = Router()
@@ -99,6 +101,8 @@ export default (app, container, config) => {
uploadRoutes(route)
userRoutes(route)
variantRoutes(route)
paymentCollectionRoutes(route)
paymentRoutes(route)
return app
}
@@ -1,6 +1,7 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit"
import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing"
describe("GET /admin/order-edits/:id", () => {
@@ -19,6 +20,9 @@ describe("GET /admin/order-edits/:id", () => {
},
},
flags: [OrderEditingFeatureFlag],
payload: {
payment_collection_description: "PayCol description",
},
}
)
})
@@ -33,6 +37,8 @@ describe("GET /admin/order-edits/:id", () => {
orderEditId,
{ loggedInUserId: IdMap.getId("admin_user") }
)
expect(orderEditServiceMock.update).toHaveBeenCalledTimes(1)
})
it("returns updated orderEdit", () => {
@@ -18,6 +18,7 @@ import { AdminPostOrderEditsEditLineItemsReq } from "./add-line-item"
import { AdminPostOrderEditsEditLineItemsLineItemReq } from "./update-order-edit-line-item"
import { GetOrderEditsParams } from "./list-order-edit"
import { GetOrderEditsOrderEditParams } from "./get-order-edit"
import { AdminPostOrderEditsRequestConfirmationReq } from "./request-confirmation"
const route = Router()
@@ -85,6 +86,7 @@ export default (app) => {
route.post(
"/:id/request",
transformBody(AdminPostOrderEditsRequestConfirmationReq),
middlewares.wrap(require("./request-confirmation").default)
)
@@ -121,3 +123,4 @@ export * from "./create-order-edit"
export * from "./get-order-edit"
export * from "./list-order-edit"
export * from "./add-line-item"
export * from "./request-confirmation"
@@ -1,9 +1,15 @@
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
import { IsOptional, IsString, IsObject } from "class-validator"
import {
OrderEditService,
OrderService,
PaymentCollectionService,
} from "../../../../services"
import {
defaultOrderEditFields,
defaultOrderEditRelations,
} from "../../../../types/order-edit"
import { PaymentCollectionType } from "../../../../models"
/**
* @oas [post] /order-edits/{id}/request
@@ -54,26 +60,71 @@ import {
*/
export default async (req, res) => {
const { id } = req.params
const validatedBody =
req.validatedBody as AdminPostOrderEditsRequestConfirmationReq
const orderEditService: OrderEditService =
req.scope.resolve("orderEditService")
const orderService: OrderService = req.scope.resolve("orderService")
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const loggedInUser = (req.user?.id ?? req.user?.userId) as string
await manager.transaction(async (transactionManager) => {
await orderEditService
.withTransaction(transactionManager)
.requestConfirmation(id, { loggedInUserId: loggedInUser })
const orderEditServiceTx =
orderEditService.withTransaction(transactionManager)
const orderEdit = await orderEditServiceTx.requestConfirmation(id, {
loggedInUserId: loggedInUser,
})
const total = await orderEditServiceTx.getTotals(orderEdit.id)
if (total.difference_due > 0) {
const order = await orderService
.withTransaction(transactionManager)
.retrieve(orderEdit.order_id, {
select: ["currency_code", "region_id"],
})
const paymentCollection = await paymentCollectionService
.withTransaction(transactionManager)
.create({
type: PaymentCollectionType.ORDER_EDIT,
amount: total.difference_due,
currency_code: order.currency_code,
region_id: order.region_id,
description: validatedBody.payment_collection_description,
created_by: loggedInUser,
})
orderEdit.payment_collection_id = paymentCollection.id
await orderEditServiceTx.update(orderEdit.id, {
payment_collection_id: paymentCollection.id,
})
}
})
const orderEdit = await orderEditService.retrieve(id, {
let orderEdit = await orderEditService.retrieve(id, {
relations: defaultOrderEditRelations,
select: defaultOrderEditFields,
})
orderEdit = await orderEditService.decorateTotals(orderEdit)
res.status(200).send({
order_edit: orderEdit,
})
}
export class AdminPostOrderEditsRequestConfirmationReq {
@IsString()
@IsOptional()
payment_collection_description?: string | undefined
}
@@ -0,0 +1,13 @@
import { PaymentCollectionService } from "../../../../services"
export default async (req, res) => {
const { id } = req.params
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
await paymentCollectionService.delete(id)
res.status(200).json({ id, deleted: true, object: "payment_collection" })
}
@@ -0,0 +1,20 @@
import { PaymentCollectionService } from "../../../../services"
import { FindParams } from "../../../../types/common"
export default async (req, res) => {
const { id } = req.params
const retrieveConfig = req.retrieveConfig
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const paymentCollection = await paymentCollectionService.retrieve(
id,
retrieveConfig
)
res.status(200).json({ payment_collection: paymentCollection })
}
export class GetPaymentCollectionsParams extends FindParams {}
@@ -0,0 +1,66 @@
import { Router } from "express"
import "reflect-metadata"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { GetPaymentCollectionsParams } from "./get-payment-collection"
import { AdminUpdatePaymentCollectionRequest } from "./update-payment-collection"
const route = Router()
export default (app, container) => {
app.use(
"/payment-collections",
isFeatureFlagEnabled(OrderEditingFeatureFlag.key),
route
)
route.get(
"/:id",
transformQuery(GetPaymentCollectionsParams, {
defaultFields: defaultPaymentCollectionFields,
defaultRelations: defaulPaymentCollectionRelations,
isList: false,
}),
middlewares.wrap(require("./get-payment-collection").default)
)
route.post(
"/:id",
transformBody(AdminUpdatePaymentCollectionRequest),
middlewares.wrap(require("./update-payment-collection").default)
)
route.delete(
"/:id",
middlewares.wrap(require("./delete-payment-collection").default)
)
return app
}
export const defaultPaymentCollectionFields = [
"id",
"type",
"status",
"description",
"amount",
"authorized_amount",
"region",
"currency_code",
"currency",
"metadata",
]
export const defaulPaymentCollectionRelations = [
"region",
"payment_sessions",
"payments",
]
export * from "./get-payment-collection"
export * from "./update-payment-collection"
@@ -0,0 +1,34 @@
import { IsObject, IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import { PaymentCollectionService } from "../../../../services"
export default async (req, res) => {
const { id } = req.params
const data = req.validatedBody as AdminUpdatePaymentCollectionRequest
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)
.update(id, data)
}
)
res.status(200).json({ payment_collection: paymentCollection })
}
export class AdminUpdatePaymentCollectionRequest {
@IsString()
@IsOptional()
description?: string
@IsObject()
@IsOptional()
metadata?: Record<string, unknown>
}
@@ -0,0 +1,11 @@
import { PaymentService } from "../../../../services"
export default async (req, res) => {
const { id } = req.params
const paymentService: PaymentService = req.scope.resolve("paymentService")
const payment = await paymentService.capture(id)
res.status(200).json({ payment })
}
@@ -0,0 +1,15 @@
import { PaymentService } from "../../../../services"
import { FindParams } from "../../../../types/common"
export default async (req, res) => {
const { id } = req.params
const retrieveConfig = req.retrieveConfig
const paymentService: PaymentService = req.scope.resolve("paymentService")
const payment = await paymentService.retrieve(id, retrieveConfig)
res.status(200).json({ payment: payment })
}
export class GetPaymentsParams extends FindParams {}
@@ -0,0 +1,57 @@
import { Router } from "express"
import "reflect-metadata"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { GetPaymentsParams } from "./get-payment"
import { AdminPostPaymentRefundsReq } from "./refund-payment"
const route = Router()
export default (app, container) => {
app.use("/payments", isFeatureFlagEnabled(OrderEditingFeatureFlag.key), route)
route.get(
"/:id",
transformQuery(GetPaymentsParams, {
defaultFields: defaultPaymentFields,
isList: false,
}),
middlewares.wrap(require("./get-payment").default)
)
route.post(
"/:id/capture",
middlewares.wrap(require("./capture-payment").default)
)
route.post(
"/:id/refund",
transformBody(AdminPostPaymentRefundsReq),
middlewares.wrap(require("./refund-payment").default)
)
return app
}
export const defaultPaymentFields = [
"id",
"swap_id",
"cart_id",
"order_id",
"amount",
"currency_code",
"amount_refunded",
"provider_id",
"data",
"captured_at",
"canceled_at",
"metadata",
]
export * from "./get-payment"
export * from "./refund-payment"
@@ -0,0 +1,41 @@
import {
IsEnum,
IsInt,
IsNotEmpty,
IsOptional,
IsString,
} from "class-validator"
import { RefundReason } from "../../../../models"
import { PaymentService } from "../../../../services"
export default async (req, res) => {
const { id } = req.params
const data = req.validatedBody as AdminPostPaymentRefundsReq
const paymentService: PaymentService = req.scope.resolve("paymentService")
const refund = await paymentService.refund(
id,
data.amount,
data.reason,
data.note
)
res.status(200).json({ refund })
}
export class AdminPostPaymentRefundsReq {
@IsInt()
@IsNotEmpty()
amount: number
@IsEnum(RefundReason)
@IsNotEmpty()
reason: RefundReason
@IsString()
@IsOptional()
note?: string
}
@@ -16,6 +16,7 @@ import returnRoutes from "./returns"
import shippingOptionRoutes from "./shipping-options"
import swapRoutes from "./swaps"
import variantRoutes from "./variants"
import paymentCollectionRoutes from "./payment-collections"
const route = Router()
@@ -47,6 +48,7 @@ export default (app, container, config) => {
returnRoutes(route)
giftCardRoutes(route)
returnReasonRoutes(route)
paymentCollectionRoutes(route)
return app
}
@@ -1,11 +1,15 @@
import { Request, Response } from "express"
import { EntityManager } from "typeorm"
import { OrderEditService } from "../../../../services"
import {
OrderEditService,
OrderService,
PaymentProviderService,
} from "../../../../services"
import {
defaultStoreOrderEditFields,
defaultStoreOrderEditRelations,
} from "../../../../types/order-edit"
import { OrderEditStatus } from "../../../../models"
import { OrderEditStatus, PaymentCollectionStatus } from "../../../../models"
import { MedusaError } from "medusa-core-utils"
/**
@@ -55,13 +59,25 @@ export default async (req: Request, res: Response) => {
const orderEditService: OrderEditService =
req.scope.resolve("orderEditService")
const orderService: OrderService = req.scope.resolve("orderService")
const paymentProviderService: PaymentProviderService = req.scope.resolve(
"paymentProviderService"
)
const manager: EntityManager = req.scope.resolve("manager")
const userId = req.user?.customer_id ?? req.user?.id ?? req.user?.userId
await manager.transaction(async (manager) => {
const orderEditServiceTx = orderEditService.withTransaction(manager)
const orderEdit = await orderEditServiceTx.retrieve(id)
const orderServiceTx = orderService.withTransaction(manager)
const paymentProviderServiceTx =
paymentProviderService.withTransaction(manager)
const orderEdit = await orderEditServiceTx.retrieve(id, {
relations: ["payment_collection"],
})
if (orderEdit.status === OrderEditStatus.CONFIRMED) {
return orderEdit
@@ -74,18 +90,31 @@ export default async (req: Request, res: Response) => {
)
}
// TODO once payment collection is done
/*const paymentCollection = await this.paymentCollectionService_.withTransaction(manager).retrieve(orderEdit.payment_collection_id)
if (!paymentCollection.authorized_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Unable to complete an order edit if the payment is not authorized"
)
}*/
if (orderEdit.payment_collection) {
if (
orderEdit.payment_collection.status !==
PaymentCollectionStatus.AUTHORIZED
) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Unable to complete an order edit if the payment is not authorized"
)
}
}
return await orderEditServiceTx.confirm(id, {
const returned = await orderEditServiceTx.confirm(id, {
loggedInUserId: userId,
})
if (orderEdit.payment_collection) {
for (const payment of orderEdit.payment_collection.payments) {
await paymentProviderServiceTx.updatePayment(payment.id, {
order_id: orderEdit.order_id,
})
}
}
return returned
})
let orderEdit = await orderEditService.retrieve(id, {
@@ -0,0 +1,15 @@
import { PaymentCollectionService } from "../../../../services"
export default async (req, res) => {
const { payment_id } = req.params
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const payment_collection = await paymentCollectionService.authorize(
payment_id
)
res.status(200).json({ payment_collection })
}
@@ -0,0 +1,20 @@
import { PaymentCollectionService } from "../../../../services"
import { FindParams } from "../../../../types/common"
export default async (req, res) => {
const { id } = req.params
const retrieveConfig = req.retrieveConfig
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const paymentCollection = await paymentCollectionService.retrieve(
id,
retrieveConfig
)
res.status(200).json({ payment_collection: paymentCollection })
}
export class GetPaymentCollectionsParams extends FindParams {}
@@ -0,0 +1,70 @@
import { Router } from "express"
import "reflect-metadata"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing"
import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled"
import { AdminManagePaymentCollectionSessionRequest } from "./manage-payment-sessions"
import { AdminRefreshPaymentCollectionSessionRequest } from "./refresh-payment-session"
import { GetPaymentCollectionsParams } from "./get-payment-collection"
const route = Router()
export default (app, container) => {
app.use(
"/payment-collections",
isFeatureFlagEnabled(OrderEditingFeatureFlag.key),
route
)
route.get(
"/:id",
transformQuery(GetPaymentCollectionsParams, {
defaultFields: defaultPaymentCollectionFields,
defaultRelations: defaulPaymentCollectionRelations,
isList: false,
}),
middlewares.wrap(require("./get-payment-collection").default)
)
route.post(
"/:id/authorize",
middlewares.wrap(require("./authorize-payment-collection").default)
)
route.post(
"/:id/sessions",
transformBody(AdminManagePaymentCollectionSessionRequest),
middlewares.wrap(require("./manage-payment-sessions").default)
)
route.post(
"/:id/sessions/:session_id/refresh",
transformBody(AdminRefreshPaymentCollectionSessionRequest),
middlewares.wrap(require("./refresh-payment-session").default)
)
return app
}
export const defaultPaymentCollectionFields = [
"id",
"type",
"status",
"description",
"amount",
"region",
"currency_code",
"currency",
"metadata",
]
export const defaulPaymentCollectionRelations = ["region", "payment_sessions"]
export * from "./get-payment-collection"
export * from "./manage-payment-sessions"
export * from "./refresh-payment-session"
@@ -0,0 +1,51 @@
import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"
import { IsType } from "../../../../utils/validators/is-type"
import { EntityManager } from "typeorm"
import { PaymentCollectionService } from "../../../../services"
export default async (req, res) => {
const data = req.validatedBody as AdminManagePaymentCollectionSessionRequest
const { id } = req.params
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const paymentCollection = await manager.transaction(
async (transactionManager) => {
return await paymentCollectionService
.withTransaction(transactionManager)
.setPaymentSessions(id, data.sessions)
}
)
res.status(200).json({ payment_collection: paymentCollection })
}
export class PaymentCollectionSessionInputRequest {
@IsString()
provider_id: string
@IsString()
customer_id: string
@IsInt()
@IsNotEmpty()
amount: number
@IsString()
@IsOptional()
session_id?: string
}
export class AdminManagePaymentCollectionSessionRequest {
@IsType([
PaymentCollectionSessionInputRequest,
[PaymentCollectionSessionInputRequest],
])
sessions:
| PaymentCollectionSessionInputRequest
| PaymentCollectionSessionInputRequest[]
}
@@ -0,0 +1,32 @@
import { IsInt, IsNotEmpty, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import { PaymentCollectionService } from "../../../../services"
export default async (req, res) => {
const data = req.validatedBody as AdminRefreshPaymentCollectionSessionRequest
const { id, session_id } = req.params
const paymentCollectionService: PaymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
const manager: EntityManager = req.scope.resolve("manager")
const paymentCollection = await manager.transaction(
async (transactionManager) => {
return await paymentCollectionService
.withTransaction(transactionManager)
.refreshPaymentSession(id, session_id, data)
}
)
res.status(200).json({ payment_collection: paymentCollection })
}
export class AdminRefreshPaymentCollectionSessionRequest {
@IsString()
provider_id: string
@IsString()
customer_id: string
}
@@ -111,7 +111,7 @@ export abstract class AbstractPaymentService
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async retrieveSavedMethods(customer: Customer): Promise<Data[]> {
return Promise.resolve([])
return []
}
public abstract getStatus(data: Data): Promise<PaymentSessionStatus>
@@ -12,8 +12,8 @@ export class paymentCollection1664880666982 implements MigrationInterface {
CREATE TYPE "PAYMENT_COLLECTION_TYPE_ENUM" AS ENUM ('order_edit');
CREATE TYPE "PAYMENT_COLLECTION_STATUS_ENUM" AS ENUM (
'not_paid', 'awaiting', 'authorized', 'partially_authorized', 'captured',
'partially_captured', 'refunded', 'partially_refunded', 'canceled', 'requires_action'
'not_paid', 'awaiting', 'authorized',
'partially_authorized', 'canceled'
);
CREATE TABLE IF NOT EXISTS payment_collection
@@ -27,8 +27,6 @@ export class paymentCollection1664880666982 implements MigrationInterface {
description text NULL,
amount integer NOT NULL,
authorized_amount integer NULL,
captured_amount integer NULL,
refunded_amount integer NULL,
region_id character varying NOT NULL,
currency_code character varying NOT NULL,
metadata jsonb NULL,
@@ -78,6 +76,7 @@ export class paymentCollection1664880666982 implements MigrationInterface {
ALTER TABLE refund ADD COLUMN payment_id character varying NULL;
CREATE INDEX "IDX_refund_payment_id" ON "refund" ("payment_id");
ALTER TABLE "refund" ADD CONSTRAINT "FK_refund_payment_id" FOREIGN KEY ("payment_id") REFERENCES "payment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE refund ALTER COLUMN order_id DROP NOT NULL;
`)
// Add missing indexes
@@ -97,6 +96,7 @@ export class paymentCollection1664880666982 implements MigrationInterface {
DROP INDEX "IDX_refund_payment_id";
ALTER TABLE refund DROP CONSTRAINT "FK_refund_payment_id";
ALTER TABLE refund ALTER COLUMN order_id SET NOT NULL;
ALTER TABLE payment_collection DROP CONSTRAINT "FK_payment_collection_region_id";
ALTER TABLE payment_collection_sessions DROP CONSTRAINT "FK_payment_collection_sessions_payment_collection_id";
@@ -21,12 +21,7 @@ export enum PaymentCollectionStatus {
AWAITING = "awaiting",
AUTHORIZED = "authorized",
PARTIALLY_AUTHORIZED = "partially_authorized",
CAPTURED = "captured",
PARTIALLY_CAPTURED = "partially_captured",
REFUNDED = "refunded",
PARTIALLY_REFUNDED = "partially_refunded",
CANCELED = "canceled",
REQUIRES_ACTION = "requires_action",
}
export enum PaymentCollectionType {
@@ -41,20 +36,14 @@ export class PaymentCollection extends SoftDeletableEntity {
@DbAwareColumn({ type: "enum", enum: PaymentCollectionStatus })
status: PaymentCollectionStatus
@Column({ nullable: true })
description: string
@Column({ type: "varchar", nullable: true })
description: string | null
@Column({ type: "int" })
amount: number
@Column({ type: "int", nullable: true })
authorized_amount: number
@Column({ type: "int", nullable: true })
captured_amount: number
@Column({ type: "int", nullable: true })
refunded_amount: number
authorized_amount: number | null
@Index()
@Column()
@@ -33,7 +33,7 @@ export class PublishableApiKey extends BaseEntity {
* type: string
* description: The key's ID
* example: pak_01G1G5V27GYX4QXNARRQCW1N8T
* created_by:
* created_by:
* type: string
* description: "The unique identifier of the user that created the key."
* example: usr_01G1G5V26F5TB3GPAPNJ8X1S3V
@@ -2,7 +2,7 @@ import { IdMap } from "medusa-test-utils"
export const orderEdit = {
id: IdMap.getId("testCreatedOrder"),
order_id: "empty-id",
order_id: IdMap.getId("test-order"),
internal_note: "internal note",
declined_reason: null,
declined_at: null,
@@ -106,6 +106,9 @@ export const orderEditServiceMock = {
created_by: context.loggedInUserId,
})
}),
update: jest.fn().mockImplementation((id, data) => {
return Promise.resolve(data)
}),
decline: jest.fn().mockImplementation((id, reason, userId) => {
return Promise.resolve({
id,
@@ -114,8 +117,17 @@ export const orderEditServiceMock = {
declined_at: new Date(),
})
}),
getTotals: jest.fn().mockImplementation(() => {
return Promise.resolve({})
getTotals: jest.fn().mockImplementation((id) => {
return Promise.resolve({
shipping_total: 10,
gift_card_total: 0,
gift_card_tax_total: 0,
discount_total: 0,
tax_total: 1,
subtotal: 2000,
difference_due: 1000,
total: 1000,
})
}),
delete: jest.fn().mockImplementation((_) => {
return Promise.resolve()
@@ -42,6 +42,7 @@ export const orders = {
},
],
regionid: IdMap.getId("testRegion"),
currency_code: "USD",
customerid: IdMap.getId("testCustomer"),
payment_method: {
providerid: "default_provider",
@@ -96,6 +97,7 @@ export const orders = {
},
],
regionid: IdMap.getId("region-france"),
currency_code: "EUR",
customerid: IdMap.getId("test-customer"),
payment_method: {
providerid: "default_provider",
@@ -0,0 +1,17 @@
import { IdMap } from "medusa-test-utils"
export const PaymentCollectionServiceMock = {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation((data) => {
const id = data.id ?? IdMap.getId("paycol_1")
return Promise.resolve({ ...data, id })
}),
}
const mock = jest.fn().mockImplementation(() => {
return PaymentCollectionServiceMock
})
export default mock
@@ -0,0 +1,15 @@
export const PaymentServiceMock = {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation((data) => {
const id = data.id ?? IdMap.getId("pay_1")
return Promise.resolve({ ...data, id })
}),
}
const mock = jest.fn().mockImplementation(() => {
return PaymentServiceMock
})
export default mock
@@ -182,7 +182,6 @@ describe("OrderEditService", () => {
}
},
})
const orderEditService = new OrderEditService({
manager: MockManager,
orderEditRepository,
@@ -331,6 +330,10 @@ describe("OrderEditService", () => {
let result
beforeEach(async () => {
jest.spyOn(orderEditService, "getTotals").mockResolvedValue({
difference_due: 1500,
} as any)
result = await orderEditService.requestConfirmation(orderEditId, {
loggedInUserId: userId,
})
@@ -4,6 +4,7 @@ import {
EventBusService,
PaymentCollectionService,
PaymentProviderService,
PaymentService,
} from "../index"
import {
PaymentCollectionStatus,
@@ -118,7 +119,6 @@ describe("PaymentCollectionService", () => {
{
id: IdMap.getId("payment-123"),
amount: 35000,
captured_amount: 0,
},
],
status: PaymentCollectionStatus.AUTHORIZED,
@@ -288,7 +288,6 @@ describe("PaymentCollectionService", () => {
it("should update a payment collection with the right arguments", async () => {
const submittedChanges = {
description: "updated description",
status: PaymentCollectionStatus.CAPTURED,
metadata: {
extra: 123,
arr: ["a", "b", "c"],
@@ -323,7 +322,6 @@ describe("PaymentCollectionService", () => {
it("should throw error to update a non-existing payment collection", async () => {
const submittedChanges = {
description: "updated description",
status: PaymentCollectionStatus.CAPTURED,
metadata: {
extra: 123,
arr: ["a", "b", "c"],
@@ -530,7 +528,6 @@ describe("PaymentCollectionService", () => {
IdMap.getId("payCol_session1"),
{
customer_id: "customer1",
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
}
)
@@ -544,19 +541,20 @@ describe("PaymentCollectionService", () => {
)
})
it("should fail to refresh a payment session if the amount is different", async () => {
it("should throw to refresh a payment session that doesn't exist", async () => {
const sess = paymentCollectionService.refreshPaymentSession(
IdMap.getId("payment-collection-session"),
IdMap.getId("payCol_session1"),
IdMap.getId("payCol_session-not-found"),
{
customer_id: "customer1",
amount: 80,
provider_id: IdMap.getId("region1_provider1"),
}
)
expect(sess).rejects.toThrow(
"The amount has to be the same as the existing payment session"
`Session with id ${IdMap.getId(
"payCol_session-not-found"
)} was not found`
)
expect(PaymentProviderServiceMock.refreshSessionNew).toBeCalledTimes(0)
expect(DefaultProviderMock.deletePayment).toBeCalledTimes(0)
@@ -633,56 +631,4 @@ describe("PaymentCollectionService", () => {
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
})
})
describe("Capture Payments", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should throw error if the status is not authorized", async () => {
paymentCollectionRepository.getPaymentCollectionIdByPaymentId = jest
.fn()
.mockReturnValue(Promise.resolve(notAuthorizedSample))
PaymentProviderServiceMock.capturePayment = jest
.fn()
.mockReturnValue(Promise.resolve())
const ret = paymentCollectionService.capture(
IdMap.getId("payment-collection-not-authorized")
)
expect(ret).rejects.toThrowError(
new Error(
`A Payment Collection with status ${PaymentCollectionStatus.PARTIALLY_AUTHORIZED} cannot capture payment`
)
)
expect(PaymentProviderServiceMock.capturePayment).toBeCalledTimes(0)
})
it("should emit PAYMENT_CAPTURE_FAILED if payment capture has failed", async () => {
paymentCollectionRepository.getPaymentCollectionIdByPaymentId = jest
.fn()
.mockReturnValue(Promise.resolve(fullyAuthorizedSample))
PaymentProviderServiceMock.retrievePayment = jest.fn().mockReturnValue(
Promise.resolve({
id: IdMap.getId("payment-123"),
amount: 35000,
captured_amount: 0,
})
)
PaymentProviderServiceMock.capturePayment = jest
.fn()
.mockRejectedValue("capture failed")
const ret = paymentCollectionService.capture(IdMap.getId("payment-123"))
expect(ret).rejects.toThrowError(
new Error(`Failed to capture Payment ${IdMap.getId("payment-123")}`)
)
})
})
})
@@ -1,6 +1,7 @@
import { MockManager, MockRepository } from "medusa-test-utils"
import PaymentProviderService from "../payment-provider"
import { testPayServiceMock } from "../__mocks__/test-pay"
import { FlagRouter } from "../../utils/flag-router"
describe("PaymentProviderService", () => {
describe("retrieveProvider", () => {
@@ -106,6 +107,10 @@ describe("PaymentProviderService", () => {
})
describe(`PaymentProviderService`, () => {
const featureFlagRouter = new FlagRouter({
order_editing: false,
})
const container = {
manager: MockManager,
paymentSessionRepository: MockRepository({
@@ -128,19 +133,22 @@ describe(`PaymentProviderService`, () => {
},
}),
find: () =>
Promise.resolve([{
id: "pay_jadazdjk",
provider_id: "default_provider",
data: {
id: "1234",
Promise.resolve([
{
id: "pay_jadazdjk",
provider_id: "default_provider",
data: {
id: "1234",
},
captured_at: new Date(),
amount: 100,
amount_refunded: 0,
},
captured_at: new Date(),
amount: 100,
amount_refunded: 0
}]),
]),
}),
refundRepository: MockRepository(),
pp_default_provider: testPayServiceMock,
featureFlagRouter,
}
const providerService = new PaymentProviderService(container)
@@ -206,29 +214,25 @@ describe(`PaymentProviderService`, () => {
})
it("successfully delete session", async () => {
await providerService.deleteSession(
{
id: "session",
provider_id: "default_provider",
data: {
id: "1234",
},
}
)
await providerService.deleteSession({
id: "session",
provider_id: "default_provider",
data: {
id: "1234",
},
})
expect(testPayServiceMock.deletePayment).toBeCalledTimes(1)
})
it("successfully delete session", async () => {
await providerService.deleteSession(
{
id: "session",
provider_id: "default_provider",
data: {
id: "1234",
},
}
)
await providerService.deleteSession({
id: "session",
provider_id: "default_provider",
data: {
id: "1234",
},
})
expect(testPayServiceMock.deletePayment).toBeCalledTimes(1)
})
@@ -265,22 +269,27 @@ describe(`PaymentProviderService`, () => {
it("successfully cancel payment", async () => {
await providerService.cancelPayment({
id: "pay_jadazdjk"
id: "pay_jadazdjk",
})
expect(testPayServiceMock.cancelPayment).toBeCalledTimes(1)
})
it("successfully capture payment", async () => {
await providerService.capturePayment({
id: "pay_jadazdjk"
id: "pay_jadazdjk",
})
expect(testPayServiceMock.capturePayment).toBeCalledTimes(1)
})
it("successfully refund payment", async () => {
await providerService.refundPayment([{
id: "pay_jadazdjk"
}], 50)
await providerService.refundPayment(
[
{
id: "pay_jadazdjk",
},
],
50
)
expect(testPayServiceMock.refundPayment).toBeCalledTimes(1)
})
})
+1
View File
@@ -28,6 +28,7 @@ export { default as OrderEditService } from "./order-edit"
export { default as OrderEditItemChangeService } from "./order-edit-item-change"
export { default as PaymentCollectionService } from "./payment-collection"
export { default as PaymentProviderService } from "./payment-provider"
export { default as PaymentService } from "./payment"
export { default as PriceListService } from "./price-list"
export { default as PricingService } from "./pricing"
export { default as ProductService } from "./product"
+1 -1
View File
@@ -661,7 +661,7 @@ export default class OrderEditService extends TransactionBaseService {
let orderEdit = await this.retrieve(orderEditId, {
relations: ["changes"],
select: ["id", "requested_at"],
select: ["id", "order_id", "requested_at"],
})
if (!orderEdit.changes?.length) {
@@ -6,18 +6,17 @@ import { buildQuery, isDefined, setMetadata } from "../utils"
import { PaymentCollectionRepository } from "../repositories/payment-collection"
import {
Customer,
Payment,
PaymentCollection,
PaymentCollectionStatus,
PaymentSession,
PaymentSessionStatus,
Refund,
} from "../models"
import { TransactionBaseService } from "../interfaces"
import {
CustomerService,
EventBusService,
PaymentProviderService,
PaymentService,
} from "./index"
import {
@@ -40,10 +39,6 @@ export default class PaymentCollectionService extends TransactionBaseService {
UPDATED: "payment-collection.updated",
DELETED: "payment-collection.deleted",
PAYMENT_AUTHORIZED: "payment-collection.payment_authorized",
PAYMENT_CAPTURED: "payment-collection.payment_captured",
PAYMENT_CAPTURE_FAILED: "payment-collection.payment_capture_failed",
REFUND_CREATED: "payment-collection.payment_refund_created",
REFUND_FAILED: "payment-collection.payment_refund_failed",
}
protected readonly manager_: EntityManager
@@ -298,7 +293,7 @@ export default class PaymentCollectionService extends TransactionBaseService {
async refreshPaymentSession(
paymentCollectionId: string,
sessionId: string,
sessionInput: PaymentCollectionSessionInput
sessionInput: Omit<PaymentCollectionSessionInput, "amount">
): Promise<PaymentSession> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
@@ -328,10 +323,10 @@ export default class PaymentCollectionService extends TransactionBaseService {
(sess) => sessionId === sess?.id
)
if (session?.amount !== sessionInput.amount) {
if (!session) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The amount has to be the same as the existing payment session"
`Session with id ${sessionId} was not found`
)
}
@@ -360,7 +355,7 @@ export default class PaymentCollectionService extends TransactionBaseService {
return sess
})
if (session.payment_authorized_at) {
if (session.payment_authorized_at && payCol.authorized_amount) {
payCol.authorized_amount -= session.amount
}
@@ -401,7 +396,9 @@ export default class PaymentCollectionService extends TransactionBaseService {
}
let authorizedAmount = 0
for (const session of payCol.payment_sessions) {
for (let i = 0; i < payCol.payment_sessions.length; i++) {
const session = payCol.payment_sessions[i]
if (session.payment_authorized_at) {
authorizedAmount += session.amount
continue
@@ -411,14 +408,21 @@ export default class PaymentCollectionService extends TransactionBaseService {
.withTransaction(manager)
.authorizePayment(session, context)
if (auth) {
payCol.payment_sessions[i] = auth
}
if (auth?.status === PaymentSessionStatus.AUTHORIZED) {
authorizedAmount += session.amount
const inputData: Omit<PaymentProviderDataInput, "customer"> = {
const inputData: Omit<PaymentProviderDataInput, "customer"> & {
payment_session: PaymentSession
} = {
amount: session.amount,
currency_code: payCol.currency_code,
provider_id: session.provider_id,
resource_id: payCol.id,
payment_session: auth,
}
payCol.payments.push(
@@ -447,235 +451,4 @@ export default class PaymentCollectionService extends TransactionBaseService {
return payCol
})
}
private async capturePayment(
payCol: PaymentCollection,
payment: Payment
): Promise<Payment> {
if (payment?.captured_at) {
return payment
}
return await this.atomicPhase_(async (manager: EntityManager) => {
const allowedStatuses = [
PaymentCollectionStatus.AUTHORIZED,
PaymentCollectionStatus.PARTIALLY_CAPTURED,
PaymentCollectionStatus.REQUIRES_ACTION,
]
if (!allowedStatuses.includes(payCol.status)) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`A Payment Collection with status ${payCol.status} cannot capture payment`
)
}
let captureError: Error | null = null
const capturedPayment = await this.paymentProviderService_
.withTransaction(manager)
.capturePayment(payment)
.catch((err) => {
captureError = err
})
payCol.captured_amount = payCol.captured_amount ?? 0
if (capturedPayment) {
payCol.captured_amount += payment.amount
}
if (payCol.captured_amount === 0) {
payCol.status = PaymentCollectionStatus.REQUIRES_ACTION
} else if (payCol.captured_amount === payCol.amount) {
payCol.status = PaymentCollectionStatus.CAPTURED
} else {
payCol.status = PaymentCollectionStatus.PARTIALLY_CAPTURED
}
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
await paymentCollectionRepository.save(payCol)
if (!capturedPayment) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_CAPTURE_FAILED, {
...payment,
error: captureError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to capture Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_CAPTURED, capturedPayment)
return capturedPayment
})
}
async capture(paymentId: string): Promise<Payment> {
const manager = this.transactionManager_ ?? this.manager_
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdByPaymentId(
paymentId,
{
relations: ["payments"],
}
)
const payment = payCol.payments.find((payment) => paymentId === payment?.id)
return await this.capturePayment(payCol, payment!)
}
async captureAll(paymentCollectionId: string): Promise<Payment[]> {
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payments"],
})
const allPayments: Payment[] = []
for (const payment of payCol.payments) {
const captured = await this.capturePayment(payCol, payment).catch(
() => void 0
)
if (captured) {
allPayments.push(captured)
}
}
return allPayments
}
private async refundPayment(
payCol: PaymentCollection,
payment: Payment,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
return await this.atomicPhase_(async (manager: EntityManager) => {
if (!payment.captured_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Payment ${payment.id} is not captured`
)
}
const refundable = payment.amount - payment.amount_refunded
if (amount > refundable) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Only ${refundable} can be refunded from Payment ${payment.id}`
)
}
let refundError: Error | null = null
const refund = await this.paymentProviderService_
.withTransaction(manager)
.refundFromPayment(payment, amount, reason, note)
.catch((err) => {
refundError = err
})
payCol.refunded_amount = payCol.refunded_amount ?? 0
if (refund) {
payCol.refunded_amount += refund.amount
}
if (payCol.refunded_amount === 0) {
payCol.status = PaymentCollectionStatus.REQUIRES_ACTION
} else if (payCol.refunded_amount === payCol.amount) {
payCol.status = PaymentCollectionStatus.REFUNDED
} else {
payCol.status = PaymentCollectionStatus.PARTIALLY_REFUNDED
}
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
await paymentCollectionRepository.save(payCol)
if (!refund) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.REFUND_FAILED, {
...payment,
error: refundError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to refund Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.REFUND_CREATED, refund)
return refund
})
}
async refund(
paymentId: string,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
const manager = this.transactionManager_ ?? this.manager_
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdByPaymentId(
paymentId
)
const payment = await this.paymentProviderService_.retrievePayment(
paymentId
)
return await this.refundPayment(payCol, payment, amount, reason, note)
}
async refundAll(
paymentCollectionId: string,
reason: string,
note?: string
): Promise<Refund[]> {
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payments"],
})
const allRefunds: Refund[] = []
for (const payment of payCol.payments) {
const refunded = await this.refundPayment(
payCol,
payment,
payment.amount,
reason,
note
).catch(() => void 0)
if (refunded) {
allRefunds.push(refunded)
}
}
return allRefunds
}
}
@@ -17,6 +17,9 @@ import {
Refund,
} from "../models"
import { PaymentProviderDataInput } from "../types/payment-collection"
import { FlagRouter } from "../utils/flag-router"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
import PaymentService from "./payment"
type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService"
type InjectedDependencies = {
@@ -25,6 +28,8 @@ type InjectedDependencies = {
paymentProviderRepository: typeof PaymentProviderRepository
paymentRepository: typeof PaymentRepository
refundRepository: typeof RefundRepository
paymentService: PaymentService
featureFlagRouter: FlagRouter
} & {
[key in `${PaymentProviderKey}`]:
| AbstractPaymentService
@@ -44,6 +49,8 @@ export default class PaymentProviderService extends TransactionBaseService {
protected readonly paymentRepository_: typeof PaymentRepository
protected readonly refundRepository_: typeof RefundRepository
protected readonly featureFlagRouter_: FlagRouter
constructor(container: InjectedDependencies) {
super(container)
@@ -53,6 +60,7 @@ export default class PaymentProviderService extends TransactionBaseService {
this.paymentProviderRepository_ = container.paymentProviderRepository
this.paymentRepository_ = container.paymentRepository
this.refundRepository_ = container.refundRepository
this.featureFlagRouter_ = container.featureFlagRouter
}
async registerInstalledProviders(providerIds: string[]): Promise<void> {
@@ -184,9 +192,7 @@ export default class PaymentProviderService extends TransactionBaseService {
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (transactionManager) => {
const provider: AbstractPaymentService = this.retrieveProvider(
sessionInput.provider_id
)
const provider = this.retrieveProvider(sessionInput.provider_id)
const sessionData = await provider
.withTransaction(transactionManager)
.createPaymentNew(sessionInput)
@@ -396,7 +402,9 @@ export default class PaymentProviderService extends TransactionBaseService {
}
async createPaymentNew(
paymentInput: Omit<PaymentProviderDataInput, "customer">
paymentInput: Omit<PaymentProviderDataInput, "customer"> & {
payment_session: PaymentSession
}
): Promise<Payment> {
return await this.atomicPhase_(async (transactionManager) => {
const { payment_session, currency_code, amount, provider_id } =
@@ -407,18 +415,13 @@ export default class PaymentProviderService extends TransactionBaseService {
.withTransaction(transactionManager)
.getPaymentData(payment_session)
const paymentRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
const created = paymentRepo.create({
const paymentService = this.container_.paymentService
return await paymentService.withTransaction(transactionManager).create({
provider_id,
amount,
currency_code,
data: paymentData,
})
return await paymentRepo.save(created)
})
}
@@ -427,20 +430,10 @@ export default class PaymentProviderService extends TransactionBaseService {
data: { order_id?: string; swap_id?: string }
): Promise<Payment> {
return await this.atomicPhase_(async (transactionManager) => {
const payment = await this.retrievePayment(paymentId)
if (data?.order_id) {
payment.order_id = data.order_id
}
if (data?.swap_id) {
payment.swap_id = data.swap_id
}
const payRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
return await payRepo.save(payment)
const paymentService = this.container_.paymentService
return await paymentService
.withTransaction(transactionManager)
.update(paymentId, data)
})
}
@@ -465,6 +458,13 @@ export default class PaymentProviderService extends TransactionBaseService {
session.data = data
session.status = status
if (
this.featureFlagRouter_.isFeatureEnabled(OrderEditingFeatureFlag.key) &&
status === PaymentSessionStatus.AUTHORIZED
) {
session.payment_authorized_at = new Date()
}
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
@@ -562,7 +562,7 @@ export default class PaymentProviderService extends TransactionBaseService {
if (refundable < amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Refund amount is higher that the refundable amount"
"Refund amount is greater that the refundable amount"
)
}
@@ -634,7 +634,7 @@ export default class PaymentProviderService extends TransactionBaseService {
if (refundable < amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Refund amount is higher that the refundable amount"
"Refund amount is greater that the refundable amount"
)
}
+229
View File
@@ -0,0 +1,229 @@
import { PaymentRepository } from "./../repositories/payment"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Payment, Refund } from "../models"
import { TransactionBaseService } from "../interfaces"
import { EventBusService, PaymentProviderService } from "./index"
import { buildQuery } from "../utils"
import { FindConfig } from "../types/common"
type InjectedDependencies = {
manager: EntityManager
paymentProviderService: PaymentProviderService
eventBusService: EventBusService
paymentRepository: typeof PaymentRepository
}
export type PaymentDataInput = {
currency_code: string
provider_id: string
amount: number
data: Record<string, unknown>
}
export default class PaymentService extends TransactionBaseService {
protected readonly manager_: EntityManager
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: EventBusService
protected readonly paymentProviderService_: PaymentProviderService
protected readonly paymentRepository_: typeof PaymentRepository
static readonly Events = {
CREATED: "payment.created",
UPDATED: "payment.updated",
PAYMENT_CAPTURED: "payment.payment_captured",
PAYMENT_CAPTURE_FAILED: "payment.payment_capture_failed",
REFUND_CREATED: "payment.payment_refund_created",
REFUND_FAILED: "payment.payment_refund_failed",
}
constructor({
manager,
paymentRepository,
paymentProviderService,
eventBusService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
super(arguments[0])
this.manager_ = manager
this.paymentRepository_ = paymentRepository
this.paymentProviderService_ = paymentProviderService
this.eventBusService_ = eventBusService
}
async retrieve(
paymentId: string,
config: FindConfig<Payment> = {}
): Promise<Payment> {
const manager = this.transactionManager_ ?? this.manager_
const paymentRepository = manager.getCustomRepository(
this.paymentRepository_
)
const query = buildQuery({ id: paymentId }, config)
const payment = await paymentRepository.find(query)
if (!payment.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment with id ${paymentId} was not found`
)
}
return payment[0]
}
async create(paymentInput: PaymentDataInput): Promise<Payment> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const { data, currency_code, amount, provider_id } = paymentInput
const paymentRepository = manager.getCustomRepository(
this.paymentRepository_
)
const created = paymentRepository.create({
provider_id,
amount,
currency_code,
data,
})
const saved = await paymentRepository.save(created)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.CREATED, saved)
return saved
})
}
async update(
paymentId: string,
data: { order_id?: string; swap_id?: string }
): Promise<Payment> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const payment = await this.retrieve(paymentId)
const paymentRepository = manager.getCustomRepository(
this.paymentRepository_
)
if (data?.order_id) {
payment.order_id = data.order_id
}
if (data?.swap_id) {
payment.swap_id = data.swap_id
}
const updated = await paymentRepository.save(payment)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.UPDATED, updated)
return updated
})
}
async capture(paymentOrId: string | Payment): Promise<Payment> {
const payment =
typeof paymentOrId === "string"
? await this.retrieve(paymentOrId)
: paymentOrId
if (payment?.captured_at) {
return payment
}
return await this.atomicPhase_(async (manager: EntityManager) => {
let captureError: Error | null = null
const capturedPayment = await this.paymentProviderService_
.withTransaction(manager)
.capturePayment(payment)
.catch((err) => {
captureError = err
})
if (!capturedPayment) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.PAYMENT_CAPTURE_FAILED, {
...payment,
error: captureError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to capture Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.PAYMENT_CAPTURED, capturedPayment)
return capturedPayment
})
}
async refund(
paymentOrId: string | Payment,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
const payment =
typeof paymentOrId === "string"
? await this.retrieve(paymentOrId)
: paymentOrId
return await this.atomicPhase_(async (manager: EntityManager) => {
if (!payment.captured_at) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Payment ${payment.id} is not captured`
)
}
const refundable = payment.amount - payment.amount_refunded
if (amount > refundable) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Only ${refundable} can be refunded from Payment ${payment.id}`
)
}
let refundError: Error | null = null
const refund = await this.paymentProviderService_
.withTransaction(manager)
.refundFromPayment(payment, amount, reason, note)
.catch((err) => {
refundError = err
})
if (!refund) {
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.REFUND_FAILED, {
...payment,
error: refundError,
})
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Failed to refund Payment ${payment.id}`
)
}
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentService.Events.REFUND_CREATED, refund)
return refund
})
}
}
+2
View File
@@ -26,6 +26,7 @@ export const defaultOrderEditRelations: string[] = [
"items",
"items.adjustments",
"items.tax_lines",
"payment_collection",
]
export const defaultOrderEditFields: (keyof OrderEdit)[] = [
@@ -45,6 +46,7 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [
"canceled_by",
"canceled_at",
"internal_note",
"payment_collection_id",
]
export const storeOrderEditNotAllowedFieldsAndRelations = [
@@ -3,7 +3,6 @@ import {
Customer,
PaymentCollection,
PaymentCollectionType,
PaymentSession,
} from "../models"
export type CreatePaymentCollectionInput = {
@@ -29,8 +28,6 @@ export type PaymentProviderDataInput = {
currency_code: string
provider_id: string
amount: number
payment_session?: PaymentSession
payment_description?: string
cart_id?: string
cart?: Cart
metadata?: any
@@ -48,7 +45,6 @@ export const defaultPaymentCollectionFields: (keyof PaymentCollection)[] = [
"description",
"amount",
"authorized_amount",
"refunded_amount",
"currency_code",
"metadata",
"region",