feat(medusa): manage payment sessions from payment collection (#2402)

* feat: manage payment sessions from payment collection
This commit is contained in:
Carlos R. L. Rodrigues authored and GitHub committed 2022-11-02 13:34:29 -03:00
1 parent a014e3623c
commit c3f8297599
28 files changed
+2002 -1315

No files matched your search

@@ -7,6 +7,7 @@ import {
PaymentSessionStatus,
} from "../models"
import { PaymentService } from "medusa-interfaces"
import { PaymentProviderDataInput } from "../types/payment-collection"
export type Data = Record<string, unknown>
export type PaymentData = Data
@@ -76,6 +77,9 @@ export abstract class AbstractPaymentService
): Promise<PaymentSessionData>
public abstract createPayment(cart: Cart): Promise<PaymentSessionData>
public abstract createPaymentNew(
paymentInput: PaymentProviderDataInput
): Promise<PaymentSessionData>
public abstract retrievePayment(paymentData: PaymentData): Promise<Data>
@@ -84,6 +88,11 @@ export abstract class AbstractPaymentService
cart: Cart
): Promise<PaymentSessionData>
public abstract updatePaymentNew(
paymentSessionData: PaymentSessionData,
paymentInput: PaymentProviderDataInput
): Promise<PaymentSessionData>
public abstract authorizePayment(
paymentSession: PaymentSession,
context: Data
@@ -27,6 +27,7 @@ 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,
@@ -71,6 +72,12 @@ export class paymentCollection1664880666982 implements MigrationInterface {
ALTER TABLE "order_edit" ADD CONSTRAINT "FK_order_edit_payment_collection_id" FOREIGN KEY ("payment_collection_id") REFERENCES "payment_collection"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
ALTER TABLE payment_session ADD COLUMN payment_authorized_at timestamp WITH time zone NULL;
ALTER TABLE payment_session ADD COLUMN amount integer NULL;
ALTER TABLE payment_session ALTER COLUMN cart_id DROP NOT NULL;
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;
`)
// Add missing indexes
@@ -85,6 +92,12 @@ export class paymentCollection1664880666982 implements MigrationInterface {
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP INDEX "IDX_order_edit_payment_collection_id";
ALTER TABLE order_edit DROP CONSTRAINT "FK_order_edit_payment_collection_id";
DROP INDEX "IDX_refund_payment_id";
ALTER TABLE refund DROP CONSTRAINT "FK_refund_payment_id";
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";
ALTER TABLE payment_collection_sessions DROP CONSTRAINT "FK_payment_collection_sessions_payment_session_id";
@@ -92,6 +105,9 @@ export class paymentCollection1664880666982 implements MigrationInterface {
ALTER TABLE payment_collection_payments DROP CONSTRAINT "FK_payment_collection_payments_payment_id";
ALTER TABLE order_edit DROP COLUMN payment_collection_id;
ALTER TABLE payment_session DROP COLUMN payment_authorized_at;
ALTER TABLE payment_session DROP COLUMN amount;
ALTER TABLE payment_session ALTER COLUMN cart_id SET NOT NULL;
ALTER TABLE refund DROP COLUMN payment_id;
DROP TABLE payment_collection;
DROP TABLE payment_collection_sessions;
@@ -99,10 +115,6 @@ export class paymentCollection1664880666982 implements MigrationInterface {
DROP TYPE "PAYMENT_COLLECTION_TYPE_ENUM";
DROP TYPE "PAYMENT_COLLECTION_STATUS_ENUM";
DROP INDEX "IDX_order_edit_payment_collection_id";
ALTER TABLE order_edit DROP CONSTRAINT "FK_order_edit_payment_collection_id";
`)
await queryRunner.query(`
@@ -50,6 +50,9 @@ export class PaymentCollection extends SoftDeletableEntity {
@Column({ type: "int", nullable: true })
authorized_amount: number
@Column({ type: "int", nullable: true })
captured_amount: number
@Column({ type: "int", nullable: true })
refunded_amount: number
@@ -28,7 +28,7 @@ export enum PaymentSessionStatus {
@Entity()
export class PaymentSession extends BaseEntity {
@Index()
@Column()
@Column({ nullable: true })
cart_id: string
@ManyToOne(() => Cart, (cart) => cart.payment_sessions)
@@ -51,6 +51,11 @@ export class PaymentSession extends BaseEntity {
@Column({ nullable: true })
idempotency_key: string
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Column({ type: "integer", nullable: true }),
])
amount: number
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Column({ type: resolveDbType("timestamptz"), nullable: true }),
])
+17 -1
View File
@@ -5,12 +5,16 @@ import {
Index,
JoinColumn,
ManyToOne,
OneToOne,
} from "typeorm"
import { BaseEntity } from "../interfaces/models/base-entity"
import { DbAwareColumn } from "../utils/db-aware-column"
import { Order } from "./order"
import { generateEntityId } from "../utils/generate-entity-id"
import { Payment } from "./payment"
import { FeatureFlagDecorators } from "../utils/feature-flag-decorators"
import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing"
export enum RefundReason {
DISCOUNT = "discount",
@@ -23,13 +27,25 @@ export enum RefundReason {
@Entity()
export class Refund extends BaseEntity {
@Index()
@Column()
@Column({ nullable: true })
order_id: string
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
Index(),
Column({ nullable: true }),
])
payment_id: string
@ManyToOne(() => Order, (order) => order.payments)
@JoinColumn({ name: "order_id" })
order: Order
@FeatureFlagDecorators(OrderEditingFeatureFlag.key, [
OneToOne(() => Payment, { nullable: true }),
JoinColumn({ name: "payment_id" }),
])
payment: Payment
@Column({ type: "int" })
amount: number
@@ -1,6 +1,72 @@
import { MedusaError } from "medusa-core-utils"
import { PaymentCollection } from "./../models/payment-collection"
import { EntityRepository, Repository } from "typeorm"
import { FindConfig } from "../types/common"
import { PaymentSession } from "../models"
@EntityRepository(PaymentCollection)
// eslint-disable-next-line max-len
export class PaymentCollectionRepository extends Repository<PaymentCollection> {}
export class PaymentCollectionRepository extends Repository<PaymentCollection> {
async getPaymentCollectionIdBySessionId(
sessionId: string,
config: FindConfig<PaymentCollection> = {}
): Promise<PaymentCollection> {
const paymentCollection = await this.find({
join: {
alias: "payment_col",
innerJoin: { payment_sessions: "payment_col.payment_sessions" },
},
where: (qb) => {
qb.where(
"payment_col_payment_sessions.payment_session_id = :sessionId",
{ sessionId }
)
},
relations: config.relations,
select: config.select,
})
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection related to Payment Session id ${sessionId} was not found`
)
}
return paymentCollection[0]
}
async getPaymentCollectionIdByPaymentId(
paymentId: string,
config: FindConfig<PaymentCollection> = {}
): Promise<PaymentCollection> {
const paymentCollection = await this.find({
join: {
alias: "payment_col",
innerJoin: { payments: "payment_col.payments" },
},
where: (qb) => {
qb.where("payment_col_payments.payment_id = :paymentId", { paymentId })
},
relations: config.relations,
select: config.select,
})
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection related to Payment id ${paymentId} was not found`
)
}
return paymentCollection[0]
}
async deleteMultiple(ids: string[]): Promise<void> {
await this.createQueryBuilder()
.delete()
.from(PaymentSession)
.where("id IN (:...ids)", { ids })
.execute()
}
}
@@ -1,5 +1,5 @@
export const DefaultProviderMock = {
getStatus: jest.fn().mockImplementation(data => {
getStatus: jest.fn().mockImplementation((data) => {
if (data.money_id === "success") {
return Promise.resolve("authorized")
}
@@ -10,7 +10,7 @@ export const DefaultProviderMock = {
return Promise.resolve("initial")
}),
retrievePayment: jest.fn().mockImplementation(data => {
retrievePayment: jest.fn().mockImplementation((data) => {
return Promise.resolve(data)
}),
list: jest.fn().mockImplementation(() => {
@@ -19,15 +19,26 @@ export const DefaultProviderMock = {
capturePayment: jest.fn().mockReturnValue(Promise.resolve()),
refundPayment: jest.fn().mockReturnValue(Promise.resolve()),
cancelPayment: jest.fn().mockReturnValue(Promise.resolve({})),
deletePayment: jest.fn().mockReturnValue(Promise.resolve({})),
authorizePayment: jest.fn().mockReturnValue(Promise.resolve({})),
}
export const PaymentProviderServiceMock = {
withTransaction: function () {
return this
},
updateSession: jest.fn().mockImplementation((session, cart) => {
return Promise.resolve({
...session.data,
id: `${session.data.id}_updated`,
})
}),
updateSessionNew: jest.fn().mockImplementation((session, sessionInput) => {
return Promise.resolve({
...session,
id: `${session.id}_updated`,
})
}),
list: jest.fn().mockImplementation(() => {
return Promise.resolve()
}),
@@ -40,12 +51,31 @@ export const PaymentProviderServiceMock = {
cartId: cart._id,
})
}),
retrieveProvider: jest.fn().mockImplementation(providerId => {
createSessionNew: jest.fn().mockImplementation((sessionInput) => {
return Promise.resolve({
id: `${sessionInput.providerId}_session`,
})
}),
retrieveProvider: jest.fn().mockImplementation((providerId) => {
if (providerId === "default_provider") {
return DefaultProviderMock
}
throw new Error("Provider Not Found")
}),
refreshSessionNew: jest.fn().mockImplementation((session, inputData) => {
DefaultProviderMock.deletePayment()
PaymentProviderServiceMock.createSessionNew(inputData)
return Promise.resolve({
...session,
id: `${session.id}_refreshed`,
})
}),
authorizePayment: jest
.fn()
.mockReturnValue(Promise.resolve({ status: "authorized" })),
createPaymentNew: jest.fn().mockImplementation((session, inputData) => {
Promise.resolve(inputData)
}),
}
const mock = jest.fn().mockImplementation(() => {
@@ -1,7 +1,22 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { EventBusService, PaymentCollectionService } from "../index"
import { PaymentCollectionStatus, PaymentCollectionType } from "../../models"
import {
CustomerService,
EventBusService,
PaymentCollectionService,
PaymentProviderService,
} from "../index"
import {
PaymentCollectionStatus,
PaymentCollectionType,
PaymentCollection,
} from "../../models"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import {
DefaultProviderMock,
PaymentProviderServiceMock,
} from "../__mocks__/payment-provider"
import { CustomerServiceMock } from "../__mocks__/customer"
import { PaymentCollectionSessionInput } from "../../types/payment-collection"
describe("PaymentCollectionService", () => {
afterEach(() => {
@@ -11,6 +26,23 @@ describe("PaymentCollectionService", () => {
const paymentCollectionSample = {
id: IdMap.getId("payment-collection-id1"),
region_id: IdMap.getId("region1"),
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
{
id: IdMap.getId("region1_provider2"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 100,
},
],
amount: 100,
created_at: new Date(),
metadata: {
@@ -19,6 +51,28 @@ describe("PaymentCollectionService", () => {
status: PaymentCollectionStatus.NOT_PAID,
}
const paymentCollectionWithSessions = {
id: IdMap.getId("payment-collection-session"),
region_id: IdMap.getId("region1"),
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 100,
},
],
amount: 100,
created_at: new Date(),
status: PaymentCollectionStatus.NOT_PAID,
} as PaymentCollection
const paymentCollectionAuthorizedSample = {
id: IdMap.getId("payment-collection-id2"),
region_id: IdMap.getId("region1"),
@@ -26,16 +80,123 @@ describe("PaymentCollectionService", () => {
status: PaymentCollectionStatus.AUTHORIZED,
}
const zeroSample = {
id: IdMap.getId("payment-collection-zero"),
region_id: IdMap.getId("region1"),
amount: 0,
status: PaymentCollectionStatus.NOT_PAID,
}
const noSessionSample = {
id: IdMap.getId("payment-collection-zero"),
region_id: IdMap.getId("region1"),
amount: 10000,
status: PaymentCollectionStatus.NOT_PAID,
}
const fullyAuthorizedSample = {
id: IdMap.getId("payment-collection-fully"),
region_id: IdMap.getId("region1"),
amount: 35000,
authorized_amount: 35000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
payment_authorized_at: Date.now(),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [
{
id: IdMap.getId("payment-123"),
amount: 35000,
captured_amount: 0,
},
],
status: PaymentCollectionStatus.AUTHORIZED,
} as unknown as PaymentCollection
const partiallyAuthorizedSample = {
id: IdMap.getId("payment-collection-partial"),
region_id: IdMap.getId("region1"),
amount: 70000,
authorized_amount: 35000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
{
id: IdMap.getId("payCol_session2"),
payment_authorized_at: Date.now(),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [],
status: PaymentCollectionStatus.PARTIALLY_AUTHORIZED,
}
const notAuthorizedSample = {
id: IdMap.getId("payment-collection-not-authorized"),
region_id: IdMap.getId("region1"),
amount: 70000,
region: {
payment_providers: [
{
id: IdMap.getId("region1_provider1"),
},
],
},
payment_sessions: [
{
id: IdMap.getId("payCol_session1"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
{
id: IdMap.getId("payCol_session2"),
provider_id: IdMap.getId("region1_provider1"),
amount: 35000,
},
],
payments: [],
status: PaymentCollectionStatus.PARTIALLY_AUTHORIZED,
} as unknown as PaymentCollection
const paymentCollectionRepository = MockRepository({
findOne: (query) => {
find: (query) => {
const map = {
[IdMap.getId("payment-collection-id1")]: paymentCollectionSample,
[IdMap.getId("payment-collection-id2")]:
paymentCollectionAuthorizedSample,
[IdMap.getId("payment-collection-session")]:
paymentCollectionWithSessions,
[IdMap.getId("payment-collection-zero")]: zeroSample,
[IdMap.getId("payment-collection-no-session")]: noSessionSample,
[IdMap.getId("payment-collection-fully")]: fullyAuthorizedSample,
[IdMap.getId("payment-collection-partial")]: partiallyAuthorizedSample,
[IdMap.getId("payment-collection-not-authorized")]: notAuthorizedSample,
}
if (map[query?.where?.id]) {
return { ...map[query?.where?.id] }
return [{ ...map[query?.where?.id] }]
}
return
},
@@ -45,20 +206,38 @@ describe("PaymentCollectionService", () => {
...data,
}
},
save: (data) => {
return data
},
})
paymentCollectionRepository.deleteMultiple = jest
.fn()
.mockImplementation(() => {
return Promise.resolve()
})
paymentCollectionRepository.getPaymentCollectionIdBySessionId = jest
.fn()
.mockImplementation(async () => {
return paymentCollectionWithSessions
})
const paymentCollectionService = new PaymentCollectionService({
manager: MockManager,
paymentCollectionRepository,
eventBusService: EventBusServiceMock as unknown as EventBusService,
paymentProviderService:
PaymentProviderServiceMock as unknown as PaymentProviderService,
customerService: CustomerServiceMock as unknown as CustomerService,
})
it("should retrieve a payment collection", async () => {
await paymentCollectionService.retrieve(
IdMap.getId("payment-collection-id1")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledWith({
expect(paymentCollectionRepository.find).toHaveBeenCalledTimes(1)
expect(paymentCollectionRepository.find).toHaveBeenCalledWith({
where: { id: IdMap.getId("payment-collection-id1") },
})
})
@@ -68,8 +247,8 @@ describe("PaymentCollectionService", () => {
IdMap.getId("payment-collection-non-existing-id")
)
expect(paymentCollectionRepository.findOne).toHaveBeenCalledTimes(1)
expect(payCol).rejects.toThrow(Error)
expect(paymentCollectionRepository.find).toBeCalledTimes(1)
})
it("should create a payment collection", async () => {
@@ -155,9 +334,10 @@ describe("PaymentCollectionService", () => {
IdMap.getId("payment-collection-non-existing"),
submittedChanges
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(0)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0)
expect(payCol).rejects.toThrow(Error)
expect(paymentCollectionRepository.save).toBeCalledTimes(0)
expect(EventBusServiceMock.emit).toBeCalledTimes(0)
})
it("should delete a payment collection", async () => {
@@ -197,4 +377,312 @@ describe("PaymentCollectionService", () => {
expect(entity).rejects.toThrow(Error)
})
describe("Manage Payment Sessions", () => {
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(
IdMap.getId("payment-collection-id2"),
inp
)
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: PaymentCollectionSessionInput = {
amount: 101,
provider_id: IdMap.getId("region1_provider1"),
customer_id: "customer1",
}
const ret = paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-id1"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
0
)
expect(ret).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
const multInp: PaymentCollectionSessionInput[] = [
{
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(
IdMap.getId("payment-collection-id1"),
multInp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
0
)
expect(multiRet).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
})
it("should ignore sessions where provider doesn't belong to the region", async () => {
const multInp: PaymentCollectionSessionInput[] = [
{
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(
IdMap.getId("payment-collection-id1"),
multInp
)
expect(multiRet).rejects.toThrow(
`The sum of sessions is not equal to 100 on Payment Collection`
)
expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0)
})
it("should add a new session and update existing one", async () => {
const inp: PaymentCollectionSessionInput[] = [
{
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(
IdMap.getId("payment-collection-session"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
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: PaymentCollectionSessionInput[] = [
{
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
customer_id: IdMap.getId("lebron"),
},
]
await paymentCollectionService.setPaymentSessions(
IdMap.getId("payment-collection-session"),
inp
)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.updateSessionNew).toHaveBeenCalledTimes(
0
)
expect(paymentCollectionRepository.deleteMultiple).toHaveBeenCalledTimes(
1
)
expect(paymentCollectionRepository.save).toHaveBeenCalledTimes(1)
})
it("should refresh a payment session", async () => {
await paymentCollectionService.refreshPaymentSession(
IdMap.getId("payment-collection-session"),
IdMap.getId("payCol_session1"),
{
customer_id: "customer1",
amount: 100,
provider_id: IdMap.getId("region1_provider1"),
}
)
expect(
PaymentProviderServiceMock.refreshSessionNew
).toHaveBeenCalledTimes(1)
expect(DefaultProviderMock.deletePayment).toHaveBeenCalledTimes(1)
expect(PaymentProviderServiceMock.createSessionNew).toHaveBeenCalledTimes(
1
)
})
it("should fail to refresh a payment session if the amount is different", async () => {
const sess = paymentCollectionService.refreshPaymentSession(
IdMap.getId("payment-collection-session"),
IdMap.getId("payCol_session1"),
{
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"
)
expect(PaymentProviderServiceMock.refreshSessionNew).toBeCalledTimes(0)
expect(DefaultProviderMock.deletePayment).toBeCalledTimes(0)
expect(PaymentProviderServiceMock.createSessionNew).toBeCalledTimes(0)
})
})
describe("Authorize Payments", () => {
afterEach(() => {
jest.clearAllMocks()
})
it("should mark as paid if amount is 0", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-zero")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
0
)
})
it("should reject payment collection without payment sessions", async () => {
const ret = paymentCollectionService.authorize(
IdMap.getId("payment-collection-no-session")
)
expect(ret).rejects.toThrowError(
new Error(
"You cannot complete a Payment Collection without a payment session."
)
)
})
it("should call authorizePayments for all sessions", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-not-authorized")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
2
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
2
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
})
it("should skip authorized sessions - partially authorized", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-partial")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
1
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
1
)
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1)
})
it("should skip authorized sessions - fully authorized", async () => {
await paymentCollectionService.authorize(
IdMap.getId("payment-collection-fully")
)
expect(PaymentProviderServiceMock.authorizePayment).toHaveBeenCalledTimes(
0
)
expect(PaymentProviderServiceMock.createPaymentNew).toHaveBeenCalledTimes(
0
)
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,19 +1,37 @@
import { DeepPartial, EntityManager, IsNull } from "typeorm"
import { DeepPartial, EntityManager, Equal } 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 { PaymentCollection, PaymentCollectionStatus } from "../models"
import {
Customer,
Payment,
PaymentCollection,
PaymentCollectionStatus,
PaymentSession,
PaymentSessionStatus,
Refund,
} from "../models"
import { TransactionBaseService } from "../interfaces"
import { EventBusService } from "./index"
import {
CustomerService,
EventBusService,
PaymentProviderService,
} from "./index"
import { CreatePaymentCollectionInput } from "../types/payment-collection"
import {
CreatePaymentCollectionInput,
PaymentCollectionSessionInput,
PaymentProviderDataInput,
} from "../types/payment-collection"
type InjectedDependencies = {
manager: EntityManager
paymentCollectionRepository: typeof PaymentCollectionRepository
paymentProviderService: PaymentProviderService
eventBusService: EventBusService
customerService: CustomerService
}
export default class PaymentCollectionService extends TransactionBaseService {
@@ -21,17 +39,26 @@ export default class PaymentCollectionService extends TransactionBaseService {
CREATED: "payment-collection.created",
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
protected transactionManager_: EntityManager | undefined
protected readonly eventBusService_: EventBusService
protected readonly paymentProviderService_: PaymentProviderService
protected readonly customerService_: CustomerService
// eslint-disable-next-line max-len
protected readonly paymentCollectionRepository_: typeof PaymentCollectionRepository
constructor({
manager,
paymentCollectionRepository,
paymentProviderService,
customerService,
eventBusService,
}: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params
@@ -39,7 +66,9 @@ export default class PaymentCollectionService extends TransactionBaseService {
this.manager_ = manager
this.paymentCollectionRepository_ = paymentCollectionRepository
this.paymentProviderService_ = paymentProviderService
this.eventBusService_ = eventBusService
this.customerService_ = customerService
}
async retrieve(
@@ -52,24 +81,24 @@ export default class PaymentCollectionService extends TransactionBaseService {
)
const query = buildQuery({ id: paymentCollectionId }, config)
const paymentCollection = await paymentCollectionRepository.findOne(query)
if (!paymentCollection) {
const paymentCollection = await paymentCollectionRepository.find(query)
if (!paymentCollection.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Payment collection with id ${paymentCollectionId} was not found`
)
}
return paymentCollection
return paymentCollection[0]
}
async create(data: CreatePaymentCollectionInput): Promise<PaymentCollection> {
return await this.atomicPhase_(async (transactionManager) => {
const paymentCollectionRepository =
transactionManager.getCustomRepository(
this.paymentCollectionRepository_
)
return await this.atomicPhase_(async (manager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const paymentCollectionToCreate = paymentCollectionRepository.create({
region_id: data.region_id,
@@ -87,7 +116,7 @@ export default class PaymentCollectionService extends TransactionBaseService {
)
await this.eventBusService_
.withTransaction(transactionManager)
.withTransaction(manager)
.emit(PaymentCollectionService.Events.CREATED, paymentCollection)
return paymentCollection
@@ -160,4 +189,493 @@ export default class PaymentCollectionService extends TransactionBaseService {
return paymentCollection
})
}
private isValidTotalAmount(
total: number,
sessionsInput: PaymentCollectionSessionInput[]
): boolean {
const sum = sessionsInput.reduce((cur, sess) => cur + sess.amount, 0)
return total === sum
}
async setPaymentSessions(
paymentCollectionId: string,
sessions: PaymentCollectionSessionInput[] | PaymentCollectionSessionInput
): Promise<PaymentCollection> {
let sessionsInput = Array.isArray(sessions) ? sessions : [sessions]
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}`
)
}
sessionsInput = sessionsInput.filter((session) => {
return !!payCol.region.payment_providers.find(({ id }) => {
return id === session.provider_id
})
})
if (!this.isValidTotalAmount(payCol.amount, sessionsInput)) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`The sum of sessions is not equal to ${payCol.amount} on Payment Collection`
)
}
let customer: Customer | undefined = undefined
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
)
const inputData: PaymentProviderDataInput = {
resource_id: payCol.id,
currency_code: payCol.currency_code,
amount: session.amount,
provider_id: session.provider_id,
customer,
metadata: {
resource_id: payCol.id,
},
}
if (existingSession) {
const paymentSession = await this.paymentProviderService_
.withTransaction(manager)
.updateSessionNew(existingSession, inputData)
selectedSessionIds.push(existingSession.id)
paymentSessions.push(paymentSession)
} else {
const paymentSession = await this.paymentProviderService_
.withTransaction(manager)
.createSessionNew(inputData)
selectedSessionIds.push(paymentSession.id)
paymentSessions.push(paymentSession)
}
}
if (payCol.payment_sessions?.length) {
const removeIds: string[] = payCol.payment_sessions
.map((sess) => sess.id)
.filter((id) => !selectedSessionIds.includes(id))
if (removeIds.length) {
await paymentCollectionRepository.deleteMultiple(removeIds)
}
}
payCol.payment_sessions = paymentSessions
return await paymentCollectionRepository.save(payCol)
})
}
async refreshPaymentSession(
paymentCollectionId: string,
sessionId: string,
sessionInput: PaymentCollectionSessionInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol =
await paymentCollectionRepository.getPaymentCollectionIdBySessionId(
sessionId,
{
relations: [
"region",
"region.payment_providers",
"payment_sessions",
],
}
)
if (paymentCollectionId !== payCol.id) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Payment Session ${sessionId} does not belong to Payment Collection ${paymentCollectionId}`
)
}
const session = payCol.payment_sessions.find(
(sess) => sessionId === sess?.id
)
if (session?.amount !== sessionInput.amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The amount has to be the same as the existing payment session"
)
}
const customer = await this.customerService_
.withTransaction(manager)
.retrieve(sessionInput.customer_id, {
select: ["id", "email", "metadata"],
})
const inputData: PaymentProviderDataInput = {
resource_id: payCol.id,
currency_code: payCol.currency_code,
amount: session.amount,
provider_id: session.provider_id,
customer,
}
const sessionRefreshed = await this.paymentProviderService_
.withTransaction(manager)
.refreshSessionNew(session, inputData)
payCol.payment_sessions = payCol.payment_sessions.map((sess) => {
if (sess.id === sessionId) {
return sessionRefreshed
}
return sess
})
if (session.payment_authorized_at) {
payCol.authorized_amount -= session.amount
}
await paymentCollectionRepository.save(payCol)
return sessionRefreshed
})
}
async authorize(
paymentCollectionId: string,
context: Record<string, unknown> = {}
): Promise<PaymentCollection> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const paymentCollectionRepository = manager.getCustomRepository(
this.paymentCollectionRepository_
)
const payCol = await this.retrieve(paymentCollectionId, {
relations: ["payment_sessions", "payments"],
})
if (payCol.authorized_amount === payCol.amount) {
return payCol
}
// If cart total is 0, we don't perform anything payment related
if (payCol.amount <= 0) {
payCol.authorized_amount = 0
return await paymentCollectionRepository.save(payCol)
}
if (!payCol.payment_sessions?.length) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"You cannot complete a Payment Collection without a payment session."
)
}
let authorizedAmount = 0
for (const session of payCol.payment_sessions) {
if (session.payment_authorized_at) {
authorizedAmount += session.amount
continue
}
const auth = await this.paymentProviderService_
.withTransaction(manager)
.authorizePayment(session, context)
if (auth?.status === PaymentSessionStatus.AUTHORIZED) {
authorizedAmount += session.amount
const inputData: Omit<PaymentProviderDataInput, "customer"> = {
amount: session.amount,
currency_code: payCol.currency_code,
provider_id: session.provider_id,
resource_id: payCol.id,
}
payCol.payments.push(
await this.paymentProviderService_
.withTransaction(manager)
.createPaymentNew(inputData)
)
}
}
if (authorizedAmount === 0) {
payCol.status = PaymentCollectionStatus.AWAITING
} else if (authorizedAmount < payCol.amount) {
payCol.status = PaymentCollectionStatus.PARTIALLY_AUTHORIZED
} else if (authorizedAmount === payCol.amount) {
payCol.status = PaymentCollectionStatus.AUTHORIZED
}
payCol.authorized_amount = authorizedAmount
const payColCopy = await paymentCollectionRepository.save(payCol)
await this.eventBusService_
.withTransaction(manager)
.emit(PaymentCollectionService.Events.PAYMENT_AUTHORIZED, payColCopy)
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
}
}
@@ -16,6 +16,7 @@ import {
PaymentSessionStatus,
Refund,
} from "../models"
import { PaymentProviderDataInput } from "../types/payment-collection"
type PaymentProviderKey = `pp_${string}` | "systemPaymentProviderService"
type InjectedDependencies = {
@@ -179,6 +180,33 @@ export default class PaymentProviderService extends TransactionBaseService {
})
}
async createSessionNew(
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (transactionManager) => {
const provider: AbstractPaymentService = this.retrieveProvider(
sessionInput.provider_id
)
const sessionData = await provider
.withTransaction(transactionManager)
.createPaymentNew(sessionInput)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
const toCreate = {
provider_id: sessionInput.provider_id,
data: sessionData,
status: "pending",
amount: sessionInput.amount,
} as PaymentSession
const created = sessionRepo.create(toCreate)
return await sessionRepo.save(created)
})
}
/**
* Refreshes a payment session with the given provider.
* This means, that we delete the current one and create a new.
@@ -219,6 +247,26 @@ export default class PaymentProviderService extends TransactionBaseService {
})
}
async refreshSessionNew(
paymentSession: PaymentSession,
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return this.atomicPhase_(async (transactionManager) => {
const session = await this.retrieveSession(paymentSession.id)
const provider = this.retrieveProvider(paymentSession.provider_id)
await provider.withTransaction(transactionManager).deletePayment(session)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
await sessionRepo.remove(session)
return await this.createSessionNew(sessionInput)
})
}
/**
* Updates an existing payment session.
* @param paymentSession - the payment session object to
@@ -240,7 +288,29 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
async updateSessionNew(
paymentSession: PaymentSession,
sessionInput: PaymentProviderDataInput
): Promise<PaymentSession> {
return await this.atomicPhase_(async (transactionManager) => {
const session = await this.retrieveSession(paymentSession.id)
const provider = this.retrieveProvider(paymentSession.provider_id)
session.amount = sessionInput.amount
paymentSession.data.amount = sessionInput.amount
session.data = await provider
.withTransaction(transactionManager)
.updatePaymentNew(paymentSession.data, sessionInput)
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return await sessionRepo.save(session)
})
}
@@ -265,7 +335,7 @@ export default class PaymentProviderService extends TransactionBaseService {
this.paymentSessionRepository_
)
return sessionRepo.remove(session)
return await sessionRepo.remove(session)
})
}
@@ -321,7 +391,34 @@ export default class PaymentProviderService extends TransactionBaseService {
cart_id: cart.id,
})
return paymentRepo.save(created)
return await paymentRepo.save(created)
})
}
async createPaymentNew(
paymentInput: Omit<PaymentProviderDataInput, "customer">
): Promise<Payment> {
return await this.atomicPhase_(async (transactionManager) => {
const { payment_session, currency_code, amount, provider_id } =
paymentInput
const provider = this.retrieveProvider(provider_id)
const paymentData = await provider
.withTransaction(transactionManager)
.getPaymentData(payment_session)
const paymentRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
const created = paymentRepo.create({
provider_id,
amount,
currency_code,
data: paymentData,
})
return await paymentRepo.save(created)
})
}
@@ -343,7 +440,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const payRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
return payRepo.save(payment)
return await payRepo.save(payment)
})
}
@@ -371,7 +468,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
@@ -392,7 +489,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const sessionRepo = transactionManager.getCustomRepository(
this.paymentSessionRepository_
)
return sessionRepo.save(session)
return await sessionRepo.save(session)
})
}
@@ -437,7 +534,7 @@ export default class PaymentProviderService extends TransactionBaseService {
const paymentRepo = transactionManager.getCustomRepository(
this.paymentRepository_
)
return paymentRepo.save(payment)
return await paymentRepo.save(payment)
})
}
@@ -521,7 +618,47 @@ export default class PaymentProviderService extends TransactionBaseService {
}
const created = refundRepo.create(toCreate)
return refundRepo.save(created)
return await refundRepo.save(created)
})
}
async refundFromPayment(
payment: Payment,
amount: number,
reason: string,
note?: string
): Promise<Refund> {
return await this.atomicPhase_(async (manager) => {
const refundable = payment.amount - payment.amount_refunded
if (refundable < amount) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Refund amount is higher that the refundable amount"
)
}
const provider = this.retrieveProvider(payment.provider_id)
payment.data = await provider
.withTransaction(manager)
.refundPayment(payment, amount)
payment.amount_refunded += amount
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
await paymentRepo.save(payment)
const refundRepo = manager.getCustomRepository(this.refundRepository_)
const toCreate = {
payment_id: payment.id,
amount,
reason,
note,
}
const created = refundRepo.create(toCreate)
return await refundRepo.save(created)
})
}
@@ -1,4 +1,10 @@
import { PaymentCollection, PaymentCollectionType } from "../models"
import {
Cart,
Customer,
PaymentCollection,
PaymentCollectionType,
PaymentSession,
} from "../models"
export type CreatePaymentCollectionInput = {
region_id: string
@@ -10,6 +16,25 @@ export type CreatePaymentCollectionInput = {
description?: string
}
export type PaymentCollectionSessionInput = {
provider_id: string
amount: number
session_id?: string
customer_id: string
}
export type PaymentProviderDataInput = {
resource_id: string
customer: Partial<Customer>
currency_code: string
provider_id: string
amount: number
payment_session?: PaymentSession
payment_description?: string
cart_id?: string
cart?: Cart
metadata?: any
}
export const defaultPaymentCollectionRelations = [
"region",
"region.payment_providers",