feat(medusa): Update payment session management (#2937)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: Update cart payment session management
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
import { AbstractCartCompletionStrategy } from "../../../../interfaces"
|
import { AbstractCartCompletionStrategy } from "../../../../interfaces"
|
||||||
import { IdempotencyKey } from "../../../../models/idempotency-key"
|
import { IdempotencyKey } from "../../../../models"
|
||||||
import { IdempotencyKeyService } from "../../../../services"
|
import { IdempotencyKeyService } from "../../../../services"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -195,11 +195,12 @@ export abstract class AbstractPaymentService
|
|||||||
* @param paymentSessionData
|
* @param paymentSessionData
|
||||||
* @param context The type of this argument is meant to be temporary and once the previous method signature
|
* @param context The type of this argument is meant to be temporary and once the previous method signature
|
||||||
* will be removed, the type will only be PaymentContext instead of Cart & PaymentContext
|
* will be removed, the type will only be PaymentContext instead of Cart & PaymentContext
|
||||||
|
* @return it return either a PaymentSessionResponse or PaymentSessionResponse["session_data"] to maintain backward compatibility
|
||||||
*/
|
*/
|
||||||
public abstract updatePayment(
|
public abstract updatePayment(
|
||||||
paymentSessionData: PaymentSessionData,
|
paymentSessionData: PaymentSessionData,
|
||||||
context: Cart & PaymentContext
|
context: Cart & PaymentContext
|
||||||
): Promise<PaymentSessionResponse>
|
): Promise<PaymentSessionResponse | PaymentSessionResponse["session_data"]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use updatePayment(paymentSessionData: PaymentSessionData, context: Cart & PaymentContext): Promise<PaymentSessionResponse> instead
|
* @deprecated use updatePayment(paymentSessionData: PaymentSessionData, context: Cart & PaymentContext): Promise<PaymentSessionResponse> instead
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class PaymentSessionIsInitiated1672906846560 implements MigrationInterface {
|
||||||
|
name = "paymentSessionIsInitiated1672906846560"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE payment_session ADD COLUMN is_initiated BOOLEAN NOT NULL DEFAULT false
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Set is_initiated to true if there is more that 0 key in the data. We assume that if data contains any key
|
||||||
|
// A payment has been initiated to the payment provider
|
||||||
|
await queryRunner.query(`
|
||||||
|
UPDATE payment_session SET is_initiated = true WHERE (
|
||||||
|
SELECT coalesce(json_array_length(json_agg(keys)), 0)
|
||||||
|
FROM jsonb_object_keys(data) AS keys (keys)
|
||||||
|
) > 0
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE payment_session DROP COLUMN is_initiated`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { BeforeInsert, Column, Entity, Index, JoinColumn, ManyToOne, Unique, } from "typeorm"
|
||||||
BeforeInsert,
|
|
||||||
Column,
|
|
||||||
Entity,
|
|
||||||
Index,
|
|
||||||
JoinColumn,
|
|
||||||
ManyToOne,
|
|
||||||
Unique,
|
|
||||||
} from "typeorm"
|
|
||||||
|
|
||||||
import { BaseEntity } from "../interfaces"
|
import { BaseEntity } from "../interfaces"
|
||||||
import { Cart } from "./cart"
|
import { Cart } from "./cart"
|
||||||
@@ -43,6 +35,9 @@ export class PaymentSession extends BaseEntity {
|
|||||||
@Column({ type: "boolean", nullable: true })
|
@Column({ type: "boolean", nullable: true })
|
||||||
is_selected: boolean | null
|
is_selected: boolean | null
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
is_initiated: boolean
|
||||||
|
|
||||||
@DbAwareColumn({ type: "enum", enum: PaymentSessionStatus })
|
@DbAwareColumn({ type: "enum", enum: PaymentSessionStatus })
|
||||||
status: string
|
status: string
|
||||||
|
|
||||||
@@ -97,6 +92,11 @@ export class PaymentSession extends BaseEntity {
|
|||||||
* description: "A flag to indicate if the Payment Session has been selected as the method that will be used to complete the purchase."
|
* description: "A flag to indicate if the Payment Session has been selected as the method that will be used to complete the purchase."
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* example: true
|
* example: true
|
||||||
|
* is_initiated:
|
||||||
|
* description: "A flag to indicate if a communication with the third party provider has been initiated."
|
||||||
|
* type: boolean
|
||||||
|
* example: true
|
||||||
|
* default: false
|
||||||
* status:
|
* status:
|
||||||
* description: "Indicates the status of the Payment Session. Will default to `pending`, and will eventually become `authorized`. Payment Sessions may have the status of `requires_more` to indicate that further actions are to be completed by the Customer."
|
* description: "Indicates the status of the Payment Session. Will default to `pending`, and will eventually become `authorized`. Payment Sessions may have the status of `requires_more` to indicate that further actions are to be completed by the Customer."
|
||||||
* type: string
|
* type: string
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant
|
|||||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||||
|
import { PaymentSessionStatus } from "../../models"
|
||||||
|
|
||||||
const eventBusService = {
|
const eventBusService = {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
@@ -1359,32 +1360,71 @@ describe("CartService", () => {
|
|||||||
|
|
||||||
describe("setPaymentSession", () => {
|
describe("setPaymentSession", () => {
|
||||||
const cartRepository = MockRepository({
|
const cartRepository = MockRepository({
|
||||||
findOneWithRelations: () => {
|
findOneWithRelations: (rels, q) => {
|
||||||
return Promise.resolve({
|
if (q.where.id === IdMap.getId("cartWithLine")) {
|
||||||
region: {
|
return Promise.resolve({
|
||||||
payment_providers: [
|
total: 100,
|
||||||
|
customer: {},
|
||||||
|
region: {
|
||||||
|
currency_code: "usd",
|
||||||
|
payment_providers: [
|
||||||
|
{
|
||||||
|
id: "test-provider",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
items: [],
|
||||||
|
shipping_methods: [],
|
||||||
|
payment_sessions: [
|
||||||
{
|
{
|
||||||
id: "test-provider",
|
id: IdMap.getId("test-session"),
|
||||||
|
provider_id: "test-provider",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
})
|
||||||
items: [],
|
} else if (q.where.id === IdMap.getId("cartWithLine2")) {
|
||||||
shipping_methods: [],
|
return Promise.resolve({
|
||||||
payment_sessions: [
|
total: 100,
|
||||||
{
|
customer: {},
|
||||||
id: IdMap.getId("test-session"),
|
region: {
|
||||||
provider_id: "test-provider",
|
currency_code: "usd",
|
||||||
|
payment_providers: [
|
||||||
|
{
|
||||||
|
id: "test-provider",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
items: [],
|
||||||
})
|
shipping_methods: [],
|
||||||
|
payment_sessions: [
|
||||||
|
{
|
||||||
|
id: IdMap.getId("test-session"),
|
||||||
|
provider_id: "test-provider",
|
||||||
|
is_selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const paymentSessionRepository = MockRepository({})
|
const paymentSessionRepository = MockRepository({})
|
||||||
|
|
||||||
|
const paymentProviderService = {
|
||||||
|
deleteSession: jest.fn(),
|
||||||
|
updateSession: jest.fn(),
|
||||||
|
createSession: jest.fn().mockImplementation(() => {
|
||||||
|
return { id: IdMap.getId("test-session") }
|
||||||
|
}),
|
||||||
|
withTransaction: function () {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const cartService = new CartService({
|
const cartService = new CartService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
paymentSessionRepository,
|
paymentSessionRepository,
|
||||||
|
paymentProviderService,
|
||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
@@ -1397,22 +1437,63 @@ describe("CartService", () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully sets a payment method", async () => {
|
it("successfully sets a payment method and create it remotely", async () => {
|
||||||
|
const providerId = "test-provider"
|
||||||
|
|
||||||
await cartService.setPaymentSession(
|
await cartService.setPaymentSession(
|
||||||
IdMap.getId("cartWithLine"),
|
IdMap.getId("cartWithLine"),
|
||||||
"test-provider"
|
providerId
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||||
"cart.updated",
|
CartService.Events.UPDATED,
|
||||||
expect.any(Object)
|
expect.any(Object)
|
||||||
)
|
)
|
||||||
expect(paymentSessionRepository.save).toHaveBeenCalledWith({
|
|
||||||
id: IdMap.getId("test-session"),
|
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||||
provider_id: "test-provider",
|
cart: expect.any(Object),
|
||||||
is_selected: true,
|
customer: expect.any(Object),
|
||||||
|
amount: expect.any(Number),
|
||||||
|
currency_code: expect.any(String),
|
||||||
|
provider_id: providerId,
|
||||||
|
payment_session_id: IdMap.getId("test-session"),
|
||||||
})
|
})
|
||||||
|
expect(paymentSessionRepository.update).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("test-session"),
|
||||||
|
{
|
||||||
|
is_selected: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully sets a payment method and update it remotely", async () => {
|
||||||
|
const providerId = "test-provider"
|
||||||
|
|
||||||
|
await cartService.setPaymentSession(
|
||||||
|
IdMap.getId("cartWithLine2"),
|
||||||
|
providerId
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||||
|
CartService.Events.UPDATED,
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(paymentProviderService.updateSession).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: IdMap.getId("test-session"),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
cart: expect.any(Object),
|
||||||
|
customer: expect.any(Object),
|
||||||
|
amount: expect.any(Number),
|
||||||
|
currency_code: expect.any(String),
|
||||||
|
provider_id: providerId,
|
||||||
|
payment_session_id: IdMap.getId("test-session"),
|
||||||
|
}
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("fails if the region does not contain the provider_id", async () => {
|
it("fails if the region does not contain the provider_id", async () => {
|
||||||
@@ -1423,13 +1504,16 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("setPaymentSessions", () => {
|
describe("setPaymentSessions", () => {
|
||||||
|
const provider1Id = "provider_1"
|
||||||
|
const provider2Id = "provider_2"
|
||||||
|
|
||||||
const cart1 = {
|
const cart1 = {
|
||||||
total: 100,
|
total: 100,
|
||||||
items: [{ subtotal: 100 }],
|
items: [{ subtotal: 100 }],
|
||||||
shipping_methods: [],
|
shipping_methods: [],
|
||||||
payment_sessions: [],
|
payment_sessions: [],
|
||||||
region: {
|
region: {
|
||||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1437,9 +1521,9 @@ describe("CartService", () => {
|
|||||||
total: 100,
|
total: 100,
|
||||||
items: [],
|
items: [],
|
||||||
shipping_methods: [],
|
shipping_methods: [],
|
||||||
payment_sessions: [{ provider_id: "provider_1" }],
|
payment_sessions: [{ provider_id: provider1Id }],
|
||||||
region: {
|
region: {
|
||||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1448,11 +1532,11 @@ describe("CartService", () => {
|
|||||||
items: [{ subtotal: 100 }],
|
items: [{ subtotal: 100 }],
|
||||||
shipping_methods: [{ subtotal: 100 }],
|
shipping_methods: [{ subtotal: 100 }],
|
||||||
payment_sessions: [
|
payment_sessions: [
|
||||||
{ provider_id: "provider_1" },
|
{ provider_id: provider1Id },
|
||||||
{ provider_id: "not_in_region" },
|
{ provider_id: "not_in_region" },
|
||||||
],
|
],
|
||||||
region: {
|
region: {
|
||||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1461,22 +1545,24 @@ describe("CartService", () => {
|
|||||||
items: [{ total: 0 }],
|
items: [{ total: 0 }],
|
||||||
shipping_methods: [],
|
shipping_methods: [],
|
||||||
payment_sessions: [
|
payment_sessions: [
|
||||||
{ provider_id: "provider_1" },
|
{ provider_id: provider1Id },
|
||||||
{ provider_id: "provider_2" },
|
{ provider_id: provider2Id },
|
||||||
],
|
],
|
||||||
region: {
|
region: {
|
||||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const cart5 = {
|
const cart5 = {
|
||||||
total: -1,
|
total: 100,
|
||||||
|
items: [{ subtotal: 100 }],
|
||||||
|
shipping_methods: [],
|
||||||
payment_sessions: [
|
payment_sessions: [
|
||||||
{ provider_id: "provider_1" },
|
{ provider_id: provider1Id, is_initiated: true },
|
||||||
{ provider_id: "provider_2" },
|
{ provider_id: provider2Id, is_selected: true },
|
||||||
],
|
],
|
||||||
region: {
|
region: {
|
||||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1494,6 +1580,11 @@ describe("CartService", () => {
|
|||||||
if (q.where.id === IdMap.getId("cart-negative")) {
|
if (q.where.id === IdMap.getId("cart-negative")) {
|
||||||
return Promise.resolve(cart4)
|
return Promise.resolve(cart4)
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
q.where.id === IdMap.getId("cartWithMixedSelectedInitiatedSessions")
|
||||||
|
) {
|
||||||
|
return Promise.resolve(cart5)
|
||||||
|
}
|
||||||
return Promise.resolve(cart1)
|
return Promise.resolve(cart1)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1507,8 +1598,11 @@ describe("CartService", () => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paymentSessionRepositoryMock = MockRepository({})
|
||||||
|
|
||||||
const cartService = new CartService({
|
const cartService = new CartService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
|
paymentSessionRepository: paymentSessionRepositoryMock,
|
||||||
totalsService,
|
totalsService,
|
||||||
cartRepository,
|
cartRepository,
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
@@ -1525,30 +1619,58 @@ describe("CartService", () => {
|
|||||||
it("initializes payment sessions for each of the providers", async () => {
|
it("initializes payment sessions for each of the providers", async () => {
|
||||||
await cartService.setPaymentSessions(IdMap.getId("cartWithLine"))
|
await cartService.setPaymentSessions(IdMap.getId("cartWithLine"))
|
||||||
|
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(2)
|
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledTimes(2)
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
expect(paymentSessionRepositoryMock.save).toHaveBeenCalledTimes(2)
|
||||||
cart: cart1,
|
|
||||||
customer: cart1.customer,
|
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledWith({
|
||||||
|
cart_id: IdMap.getId("cartWithLine"),
|
||||||
|
status: PaymentSessionStatus.PENDING,
|
||||||
amount: cart1.total,
|
amount: cart1.total,
|
||||||
currency_code: cart1.region.currency_code,
|
provider_id: provider1Id,
|
||||||
provider_id: "provider_1",
|
data: {},
|
||||||
})
|
})
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
|
||||||
cart: cart1,
|
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledWith({
|
||||||
customer: cart1.customer,
|
cart_id: IdMap.getId("cartWithLine"),
|
||||||
|
status: PaymentSessionStatus.PENDING,
|
||||||
amount: cart1.total,
|
amount: cart1.total,
|
||||||
currency_code: cart1.region.currency_code,
|
provider_id: provider2Id,
|
||||||
provider_id: "provider_2",
|
data: {},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("delete or update payment sessions remotely depending if they are selected and/or initiated", async () => {
|
||||||
|
await cartService.setPaymentSessions(
|
||||||
|
IdMap.getId("cartWithMixedSelectedInitiatedSessions")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selected, update
|
||||||
|
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(1)
|
||||||
|
expect(paymentProviderService.updateSession).toHaveBeenCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
expect.objectContaining({
|
||||||
|
provider_id: provider2Id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Not selected, but initiated, delete
|
||||||
|
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(1)
|
||||||
|
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
provider_id: provider1Id,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(paymentSessionRepositoryMock.save).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it("filters sessions not available in the region", async () => {
|
it("filters sessions not available in the region", async () => {
|
||||||
await cartService.setPaymentSessions(IdMap.getId("cart-to-filter"))
|
await cartService.setPaymentSessions(IdMap.getId("cart-to-filter"))
|
||||||
|
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(1)
|
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(1)
|
expect(paymentSessionRepositoryMock.save).toHaveBeenCalledTimes(2) // create and update
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(1)
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(1)
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||||
provider_id: "not_in_region",
|
provider_id: "not_in_region",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -1556,28 +1678,26 @@ describe("CartService", () => {
|
|||||||
it("removes if cart total === 0", async () => {
|
it("removes if cart total === 0", async () => {
|
||||||
await cartService.setPaymentSessions(IdMap.getId("cart-remove"))
|
await cartService.setPaymentSessions(IdMap.getId("cart-remove"))
|
||||||
|
|
||||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0)
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(2)
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0)
|
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2)
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
provider_id: provider1Id,
|
||||||
provider_id: "provider_1",
|
|
||||||
})
|
})
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||||
provider_id: "provider_2",
|
provider_id: provider2Id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it("removes if cart total < 0", async () => {
|
it("removes if cart total < 0", async () => {
|
||||||
await cartService.setPaymentSessions(IdMap.getId("cart-negative"))
|
await cartService.setPaymentSessions(IdMap.getId("cart-negative"))
|
||||||
|
|
||||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0)
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(2)
|
||||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0)
|
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2)
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
provider_id: provider1Id,
|
||||||
provider_id: "provider_1",
|
|
||||||
})
|
})
|
||||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||||
provider_id: "provider_2",
|
provider_id: provider2Id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ describe("PaymentProviderService", () => {
|
|||||||
withTransaction: function () {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
updatePayment: jest.fn().mockReturnValue(Promise.resolve()),
|
updatePayment: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
DiscountRuleType,
|
DiscountRuleType,
|
||||||
LineItem,
|
LineItem,
|
||||||
PaymentSession,
|
PaymentSession,
|
||||||
|
PaymentSessionStatus,
|
||||||
SalesChannel,
|
SalesChannel,
|
||||||
ShippingMethod,
|
ShippingMethod,
|
||||||
} from "../models"
|
} from "../models"
|
||||||
@@ -39,24 +40,24 @@ import { FlagRouter } from "../utils/flag-router"
|
|||||||
import { validateEmail } from "../utils/is-email"
|
import { validateEmail } from "../utils/is-email"
|
||||||
import { PaymentSessionInput } from "../types/payment"
|
import { PaymentSessionInput } from "../types/payment"
|
||||||
import {
|
import {
|
||||||
CustomShippingOptionService,
|
|
||||||
CustomerService,
|
CustomerService,
|
||||||
|
CustomShippingOptionService,
|
||||||
DiscountService,
|
DiscountService,
|
||||||
EventBusService,
|
EventBusService,
|
||||||
GiftCardService,
|
GiftCardService,
|
||||||
LineItemService,
|
|
||||||
LineItemAdjustmentService,
|
LineItemAdjustmentService,
|
||||||
|
LineItemService,
|
||||||
NewTotalsService,
|
NewTotalsService,
|
||||||
PaymentProviderService,
|
PaymentProviderService,
|
||||||
ProductService,
|
ProductService,
|
||||||
ProductVariantService,
|
|
||||||
ProductVariantInventoryService,
|
ProductVariantInventoryService,
|
||||||
|
ProductVariantService,
|
||||||
RegionService,
|
RegionService,
|
||||||
|
SalesChannelService,
|
||||||
ShippingOptionService,
|
ShippingOptionService,
|
||||||
StoreService,
|
StoreService,
|
||||||
TaxProviderService,
|
TaxProviderService,
|
||||||
TotalsService,
|
TotalsService,
|
||||||
SalesChannelService,
|
|
||||||
} from "."
|
} from "."
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
@@ -1631,12 +1632,11 @@ class CartService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a payment method for a cart.
|
* Selects a payment session for a cart and creates a payment object in the external provider system
|
||||||
* @param cartId - the id of the cart to add payment method to
|
* @param cartId - the id of the cart to add payment method to
|
||||||
* @param providerId - the id of the provider to be set to the cart
|
* @param providerId - the id of the provider to be set to the cart
|
||||||
* @return result of update operation
|
|
||||||
*/
|
*/
|
||||||
async setPaymentSession(cartId: string, providerId: string): Promise<Cart> {
|
async setPaymentSession(cartId: string, providerId: string): Promise<void> {
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const psRepo = transactionManager.getCustomRepository(
|
const psRepo = transactionManager.getCustomRepository(
|
||||||
@@ -1644,30 +1644,34 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const cart = await this.retrieveWithTotals(cartId, {
|
const cart = await this.retrieveWithTotals(cartId, {
|
||||||
relations: ["region", "region.payment_providers", "payment_sessions"],
|
relations: [
|
||||||
|
"customer",
|
||||||
|
"region",
|
||||||
|
"region.payment_providers",
|
||||||
|
"payment_sessions",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
// The region must have the provider id in its providers array
|
const isProviderPresent = cart.region.payment_providers.find(
|
||||||
if (
|
({ id }) => providerId === id
|
||||||
providerId !== "system" &&
|
)
|
||||||
!(
|
|
||||||
cart.region.payment_providers.length &&
|
if (providerId !== "system" && !isProviderPresent) {
|
||||||
cart.region.payment_providers.find(({ id }) => providerId === id)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
`The payment method is not available in this region`
|
`The payment method is not available in this region`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(
|
const cartPaymentSessionIds = cart.payment_sessions.map((p) => p.id)
|
||||||
cart.payment_sessions.map(async (paymentSession) => {
|
await psRepo.update(
|
||||||
return psRepo.save({ ...paymentSession, is_selected: null })
|
{ id: In(cartPaymentSessionIds) },
|
||||||
})
|
{
|
||||||
|
is_selected: null,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const paymentSession = cart.payment_sessions.find(
|
let paymentSession = cart.payment_sessions.find(
|
||||||
(ps) => ps.provider_id === providerId
|
(ps) => ps.provider_id === providerId
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1678,16 +1682,34 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
paymentSession.is_selected = true
|
const sessionInput: PaymentSessionInput = {
|
||||||
|
cart,
|
||||||
|
customer: cart.customer,
|
||||||
|
amount: cart.total!,
|
||||||
|
currency_code: cart.region.currency_code,
|
||||||
|
provider_id: providerId,
|
||||||
|
payment_session_id: paymentSession.id,
|
||||||
|
}
|
||||||
|
|
||||||
await psRepo.save(paymentSession)
|
if (paymentSession.is_selected) {
|
||||||
|
// update the session remotely
|
||||||
|
await this.paymentProviderService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.updateSession(paymentSession, sessionInput)
|
||||||
|
}
|
||||||
|
|
||||||
const updatedCart = await this.retrieve(cartId)
|
if (!paymentSession.is_initiated) {
|
||||||
|
// Create the session remotely
|
||||||
|
paymentSession = await this.paymentProviderService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.createSession(sessionInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
await psRepo.update(paymentSession.id, { is_selected: true })
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.emit(CartService.Events.UPDATED, updatedCart)
|
.emit(CartService.Events.UPDATED, { id: cartId })
|
||||||
return updatedCart
|
|
||||||
},
|
},
|
||||||
"SERIALIZABLE"
|
"SERIALIZABLE"
|
||||||
)
|
)
|
||||||
@@ -1709,6 +1731,9 @@ class CartService extends TransactionBaseService {
|
|||||||
this.paymentSessionRepository_
|
this.paymentSessionRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const paymentProviderServiceTx =
|
||||||
|
this.paymentProviderService_.withTransaction(transactionManager)
|
||||||
|
|
||||||
const cartId =
|
const cartId =
|
||||||
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
|
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
|
||||||
|
|
||||||
@@ -1735,77 +1760,135 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { total, region } = cart
|
const { total, region } = cart
|
||||||
|
|
||||||
|
// Helpers that either delete a session locally or remotely. Will be used in multiple places below.
|
||||||
|
const deleteSessionAppropriately = async (session) => {
|
||||||
|
if (session.is_selected || session.is_initiated) {
|
||||||
|
return paymentProviderServiceTx.deleteSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
return psRepo.delete(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the case of a cart that has a total <= 0 we can return prematurely.
|
||||||
|
// we are deleting the sessions, and we don't need to create or update anything from now on.
|
||||||
|
if (total <= 0) {
|
||||||
|
await Promise.all(
|
||||||
|
cart.payment_sessions.map(async (session) => {
|
||||||
|
return deleteSessionAppropriately(session)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerSet = new Set(region.payment_providers.map((p) => p.id))
|
||||||
|
const alreadyConsumedProviderIds: Set<string> = new Set()
|
||||||
|
|
||||||
const partialSessionInput: Omit<PaymentSessionInput, "provider_id"> = {
|
const partialSessionInput: Omit<PaymentSessionInput, "provider_id"> = {
|
||||||
cart: cart as Cart,
|
cart: cart as Cart,
|
||||||
customer: cart.customer,
|
customer: cart.customer,
|
||||||
amount: cart.total,
|
amount: total,
|
||||||
currency_code: cart.region.currency_code,
|
currency_code: cart.region.currency_code,
|
||||||
}
|
}
|
||||||
|
const partialPaymentSessionData = {
|
||||||
// If there are existing payment sessions ensure that these are up to date
|
cart_id: cartId,
|
||||||
const seen: string[] = []
|
data: {},
|
||||||
if (cart.payment_sessions?.length) {
|
status: PaymentSessionStatus.PENDING,
|
||||||
await Promise.all(
|
amount: total,
|
||||||
cart.payment_sessions.map(async (paymentSession) => {
|
|
||||||
if (
|
|
||||||
total <= 0 ||
|
|
||||||
!region.payment_providers.find(
|
|
||||||
({ id }) => id === paymentSession.provider_id
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return this.paymentProviderService_
|
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.deleteSession(paymentSession)
|
|
||||||
} else {
|
|
||||||
seen.push(paymentSession.provider_id)
|
|
||||||
|
|
||||||
const paymentSessionInput = {
|
|
||||||
...partialSessionInput,
|
|
||||||
provider_id: paymentSession.provider_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.paymentProviderService_
|
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.updateSession(paymentSession, paymentSessionInput)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total > 0) {
|
await Promise.all(
|
||||||
// If only one payment session exists, we preselect it
|
cart.payment_sessions.map(async (session) => {
|
||||||
if (region.payment_providers.length === 1 && !cart.payment_session) {
|
if (!providerSet.has(session.provider_id)) {
|
||||||
const paymentProvider = region.payment_providers[0]
|
/**
|
||||||
const paymentSessionInput = {
|
* if the provider does not belong to the region then delete the session.
|
||||||
...partialSessionInput,
|
* The deletion occurs locally if there is no external data or if it is not selected
|
||||||
provider_id: paymentProvider.id,
|
* otherwise the deletion will also occur remotely through the external provider.
|
||||||
|
*/
|
||||||
|
|
||||||
|
return await deleteSessionAppropriately(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentSession = await this.paymentProviderService_
|
/**
|
||||||
.withTransaction(transactionManager)
|
* if the provider belongs to the region then update or delete the session.
|
||||||
.createSession(paymentSessionInput)
|
* The update occurs locally if it is not selected
|
||||||
|
* otherwise the update will also occur remotely through the external provider.
|
||||||
|
* In case the session is not selected but contains an external provider data, we delete the external provider
|
||||||
|
* session to be in a clean state.
|
||||||
|
*/
|
||||||
|
|
||||||
paymentSession.is_selected = true
|
// We are saving the provider id on which the work below will be done. That way,
|
||||||
|
// when handling the providers from the cart region at a later point below, we do not double the work on the sessions that already
|
||||||
|
// exists for the same provider.
|
||||||
|
alreadyConsumedProviderIds.add(session.provider_id)
|
||||||
|
|
||||||
await psRepo.save(paymentSession)
|
// Update remotely
|
||||||
} else {
|
if (session.is_selected) {
|
||||||
await Promise.all(
|
const paymentSessionInput = {
|
||||||
region.payment_providers.map(async (paymentProvider) => {
|
...partialSessionInput,
|
||||||
if (!seen.includes(paymentProvider.id)) {
|
provider_id: session.provider_id,
|
||||||
const paymentSessionInput = {
|
}
|
||||||
...partialSessionInput,
|
|
||||||
provider_id: paymentProvider.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.paymentProviderService_
|
return paymentProviderServiceTx.updateSession(
|
||||||
.withTransaction(transactionManager)
|
session,
|
||||||
.createSession(paymentSessionInput)
|
paymentSessionInput
|
||||||
}
|
)
|
||||||
return
|
}
|
||||||
|
|
||||||
|
let updatedSession: PaymentSession
|
||||||
|
|
||||||
|
// At this stage the session is not selected. Delete it remotely if there is some
|
||||||
|
// external provider data and create the session locally only. Otherwise, update the existing local session.
|
||||||
|
if (session.is_initiated) {
|
||||||
|
await paymentProviderServiceTx.deleteSession(session)
|
||||||
|
updatedSession = psRepo.create({
|
||||||
|
...partialPaymentSessionData,
|
||||||
|
provider_id: session.provider_id,
|
||||||
})
|
})
|
||||||
)
|
} else {
|
||||||
|
updatedSession = { ...session, amount: total } as PaymentSession
|
||||||
|
}
|
||||||
|
|
||||||
|
return psRepo.save(updatedSession)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From now on, the sessions have been cleanup. We can now
|
||||||
|
* - Set the provider session as selected if it is the only one existing and there is no payment session on the cart
|
||||||
|
* - Create a session per provider locally if it does not already exists on the cart as per the previous step
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If only one provider exists and there is no session on the cart, create the session and select it.
|
||||||
|
if (region.payment_providers.length === 1 && !cart.payment_session) {
|
||||||
|
const paymentProvider = region.payment_providers[0]
|
||||||
|
|
||||||
|
const paymentSessionInput = {
|
||||||
|
...partialSessionInput,
|
||||||
|
provider_id: paymentProvider.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const paymentSession = await this.paymentProviderService_
|
||||||
|
.withTransaction(transactionManager)
|
||||||
|
.createSession(paymentSessionInput)
|
||||||
|
|
||||||
|
await psRepo.update(paymentSession.id, { is_selected: true })
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
region.payment_providers.map(async (paymentProvider) => {
|
||||||
|
if (alreadyConsumedProviderIds.has(paymentProvider.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const paymentSession = psRepo.create({
|
||||||
|
...partialPaymentSessionData,
|
||||||
|
provider_id: paymentProvider.id,
|
||||||
|
})
|
||||||
|
return psRepo.save(paymentSession)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1820,7 +1903,7 @@ class CartService extends TransactionBaseService {
|
|||||||
async deletePaymentSession(
|
async deletePaymentSession(
|
||||||
cartId: string,
|
cartId: string,
|
||||||
providerId: string
|
providerId: string
|
||||||
): Promise<Cart> {
|
): Promise<void> {
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const cart = await this.retrieve(cartId, {
|
const cart = await this.retrieve(cartId, {
|
||||||
@@ -1840,11 +1923,18 @@ class CartService extends TransactionBaseService {
|
|||||||
({ provider_id }) => provider_id !== providerId
|
({ provider_id }) => provider_id !== providerId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const psRepo = transactionManager.getCustomRepository(
|
||||||
|
this.paymentSessionRepository_
|
||||||
|
)
|
||||||
|
|
||||||
if (paymentSession) {
|
if (paymentSession) {
|
||||||
// Delete the session with the provider
|
if (paymentSession.is_selected || paymentSession.is_initiated) {
|
||||||
await this.paymentProviderService_
|
await this.paymentProviderService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.deleteSession(paymentSession)
|
.deleteSession(paymentSession)
|
||||||
|
} else {
|
||||||
|
await psRepo.delete(paymentSession)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1852,8 +1942,7 @@ class CartService extends TransactionBaseService {
|
|||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.emit(CartService.Events.UPDATED, cart)
|
.emit(CartService.Events.UPDATED, { id: cart.id })
|
||||||
return cart
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1863,12 +1952,12 @@ class CartService extends TransactionBaseService {
|
|||||||
* @param cartId - the id of the cart to remove from
|
* @param cartId - the id of the cart to remove from
|
||||||
* @param providerId - the id of the provider whoose payment session
|
* @param providerId - the id of the provider whoose payment session
|
||||||
* should be removed.
|
* should be removed.
|
||||||
* @return {Promise<Cart>} the resulting cart.
|
* @return {Promise<void>} the resulting cart.
|
||||||
*/
|
*/
|
||||||
async refreshPaymentSession(
|
async refreshPaymentSession(
|
||||||
cartId: string,
|
cartId: string,
|
||||||
providerId: string
|
providerId: string
|
||||||
): Promise<Cart> {
|
): Promise<void> {
|
||||||
return await this.atomicPhase_(
|
return await this.atomicPhase_(
|
||||||
async (transactionManager: EntityManager) => {
|
async (transactionManager: EntityManager) => {
|
||||||
const cart = await this.retrieveWithTotals(cartId, {
|
const cart = await this.retrieveWithTotals(cartId, {
|
||||||
@@ -1881,25 +1970,30 @@ class CartService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (paymentSession) {
|
if (paymentSession) {
|
||||||
// Delete the session with the provider
|
if (paymentSession.is_selected) {
|
||||||
await this.paymentProviderService_
|
await this.paymentProviderService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.refreshSession(paymentSession, {
|
.refreshSession(paymentSession, {
|
||||||
cart: cart as Cart,
|
cart: cart as Cart,
|
||||||
customer: cart.customer,
|
customer: cart.customer,
|
||||||
|
amount: cart.total,
|
||||||
|
currency_code: cart.region.currency_code,
|
||||||
|
provider_id: providerId,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const psRepo = transactionManager.getCustomRepository(
|
||||||
|
this.paymentSessionRepository_
|
||||||
|
)
|
||||||
|
await psRepo.update(paymentSession.id, {
|
||||||
amount: cart.total,
|
amount: cart.total,
|
||||||
currency_code: cart.region.currency_code,
|
|
||||||
provider_id: providerId,
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedCart = await this.retrieve(cartId)
|
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.emit(CartService.Events.UPDATED, updatedCart)
|
.emit(CartService.Events.UPDATED, { id: cartId })
|
||||||
return updatedCart
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,17 +215,15 @@ export default class PaymentProviderService extends TransactionBaseService {
|
|||||||
paymentResponse
|
paymentResponse
|
||||||
)
|
)
|
||||||
|
|
||||||
const amount = this.featureFlagRouter_.isFeatureEnabled(
|
|
||||||
OrderEditingFeatureFlag.key
|
|
||||||
)
|
|
||||||
? context.amount
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return await this.saveSession(providerId, {
|
return await this.saveSession(providerId, {
|
||||||
|
payment_session_id: !isString(providerIdOrSessionInput)
|
||||||
|
? providerIdOrSessionInput.payment_session_id
|
||||||
|
: undefined,
|
||||||
cartId: context.id,
|
cartId: context.id,
|
||||||
sessionData,
|
sessionData,
|
||||||
status: PaymentSessionStatus.PENDING,
|
status: PaymentSessionStatus.PENDING,
|
||||||
amount,
|
isInitiated: true,
|
||||||
|
amount: context.amount,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -281,20 +279,24 @@ export default class PaymentProviderService extends TransactionBaseService {
|
|||||||
|
|
||||||
const context = this.buildPaymentProcessorContext(sessionInput)
|
const context = this.buildPaymentProcessorContext(sessionInput)
|
||||||
|
|
||||||
const sessionData = await provider
|
const paymentResponse = await provider
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.updatePayment(paymentSession.data, context)
|
.updatePayment(paymentSession.data, context)
|
||||||
|
|
||||||
const amount = this.featureFlagRouter_.isFeatureEnabled(
|
const sessionData = paymentResponse.session_data ?? paymentResponse
|
||||||
OrderEditingFeatureFlag.key
|
|
||||||
|
await this.processUpdateRequestsData(
|
||||||
|
{
|
||||||
|
customer: { id: context.customer?.id },
|
||||||
|
},
|
||||||
|
paymentResponse
|
||||||
)
|
)
|
||||||
? context.amount
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
return await this.saveSession(paymentSession.provider_id, {
|
return await this.saveSession(paymentSession.provider_id, {
|
||||||
payment_session_id: paymentSession.id,
|
payment_session_id: paymentSession.id,
|
||||||
sessionData,
|
sessionData,
|
||||||
amount,
|
isInitiated: true,
|
||||||
|
amount: context.amount,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -687,44 +689,40 @@ export default class PaymentProviderService extends TransactionBaseService {
|
|||||||
amount?: number
|
amount?: number
|
||||||
sessionData: Record<string, unknown>
|
sessionData: Record<string, unknown>
|
||||||
isSelected?: boolean
|
isSelected?: boolean
|
||||||
|
isInitiated?: boolean
|
||||||
status?: PaymentSessionStatus
|
status?: PaymentSessionStatus
|
||||||
}
|
}
|
||||||
): Promise<PaymentSession> {
|
): Promise<PaymentSession> {
|
||||||
const manager = this.transactionManager_ ?? this.manager_
|
const manager = this.transactionManager_ ?? this.manager_
|
||||||
|
|
||||||
if (
|
|
||||||
data.amount != null &&
|
|
||||||
!this.featureFlagRouter_.isFeatureEnabled(OrderEditingFeatureFlag.key)
|
|
||||||
) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
|
||||||
"Amount on payment sessions is only available with the OrderEditing API currently guarded by feature flag `MEDUSA_FF_ORDER_EDITING`. Read more about feature flags here: https://docs.medusajs.com/advanced/backend/feature-flags/toggle/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionRepo = manager.getCustomRepository(
|
const sessionRepo = manager.getCustomRepository(
|
||||||
this.paymentSessionRepository_
|
this.paymentSessionRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Update an existing session
|
||||||
if (data.payment_session_id) {
|
if (data.payment_session_id) {
|
||||||
const session = await this.retrieveSession(data.payment_session_id)
|
const session = await this.retrieveSession(data.payment_session_id)
|
||||||
session.data = data.sessionData ?? session.data
|
session.data = data.sessionData ?? session.data
|
||||||
session.status = data.status ?? session.status
|
session.status = data.status ?? session.status
|
||||||
session.amount = data.amount ?? session.amount
|
session.amount = data.amount ?? session.amount
|
||||||
|
session.is_initiated = data.isInitiated ?? session.is_initiated
|
||||||
|
session.is_selected = data.isSelected ?? session.is_selected
|
||||||
return await sessionRepo.save(session)
|
return await sessionRepo.save(session)
|
||||||
} else {
|
|
||||||
const toCreate: Partial<PaymentSession> = {
|
|
||||||
cart_id: data.cartId || null,
|
|
||||||
provider_id: providerId,
|
|
||||||
data: data.sessionData,
|
|
||||||
is_selected: data.isSelected,
|
|
||||||
status: data.status,
|
|
||||||
amount: data.amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = sessionRepo.create(toCreate)
|
|
||||||
return await sessionRepo.save(created)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new session
|
||||||
|
const toCreate: Partial<PaymentSession> = {
|
||||||
|
cart_id: data.cartId || null,
|
||||||
|
provider_id: providerId,
|
||||||
|
data: data.sessionData,
|
||||||
|
is_selected: data.isSelected,
|
||||||
|
is_initiated: data.isInitiated,
|
||||||
|
status: data.status,
|
||||||
|
amount: data.amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = sessionRepo.create(toCreate)
|
||||||
|
return await sessionRepo.save(created)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import EventBusService from "../services/event-bus"
|
import EventBusService from "../services/event-bus"
|
||||||
import { CartService, PaymentProviderService } from "../services"
|
import { CartService } from "../services"
|
||||||
import { EntityManager } from "typeorm"
|
import { EntityManager } from "typeorm"
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
eventBusService: EventBusService
|
eventBusService: EventBusService
|
||||||
cartService: CartService
|
cartService: CartService
|
||||||
paymentProviderService: PaymentProviderService
|
|
||||||
manager: EntityManager
|
manager: EntityManager
|
||||||
}
|
}
|
||||||
|
|
||||||
class CartSubscriber {
|
class CartSubscriber {
|
||||||
protected readonly manager_: EntityManager
|
protected readonly manager_: EntityManager
|
||||||
protected readonly cartService_: CartService
|
protected readonly cartService_: CartService
|
||||||
protected readonly paymentProviderService_: PaymentProviderService
|
|
||||||
protected readonly eventBus_: EventBusService
|
protected readonly eventBus_: EventBusService
|
||||||
|
|
||||||
constructor({
|
constructor({ manager, cartService, eventBusService }: InjectedDependencies) {
|
||||||
manager,
|
|
||||||
cartService,
|
|
||||||
paymentProviderService,
|
|
||||||
eventBusService,
|
|
||||||
}: InjectedDependencies) {
|
|
||||||
this.cartService_ = cartService
|
this.cartService_ = cartService
|
||||||
this.paymentProviderService_ = paymentProviderService
|
|
||||||
this.eventBus_ = eventBusService
|
this.eventBus_ = eventBusService
|
||||||
this.manager_ = manager
|
this.manager_ = manager
|
||||||
|
|
||||||
@@ -38,30 +30,18 @@ class CartSubscriber {
|
|||||||
await this.manager_.transaction(
|
await this.manager_.transaction(
|
||||||
"SERIALIZABLE",
|
"SERIALIZABLE",
|
||||||
async (transactionManager) => {
|
async (transactionManager) => {
|
||||||
const cart = await this.cartService_
|
const cartServiceTx =
|
||||||
.withTransaction(transactionManager)
|
this.cartService_.withTransaction(transactionManager)
|
||||||
.retrieveWithTotals(cartId, {
|
|
||||||
relations: [
|
const cart = await cartServiceTx.retrieve(cartId, {
|
||||||
"billing_address",
|
relations: ["payment_sessions"],
|
||||||
"region",
|
})
|
||||||
"region.payment_providers",
|
|
||||||
"payment_sessions",
|
|
||||||
"customer",
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!cart.payment_sessions?.length) {
|
if (!cart.payment_sessions?.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentProviderServiceTx =
|
return await cartServiceTx.setPaymentSessions(cart.id)
|
||||||
this.paymentProviderService_.withTransaction(transactionManager)
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
cart.payment_sessions.map(async (paymentSession) => {
|
|
||||||
return paymentProviderServiceTx.updateSession(paymentSession, cart)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from "../models"
|
} from "../models"
|
||||||
|
|
||||||
export type PaymentSessionInput = {
|
export type PaymentSessionInput = {
|
||||||
|
payment_session_id?: string
|
||||||
provider_id: string
|
provider_id: string
|
||||||
// TODO: Support legacy payment provider API> Once we are ready to break the api then we can remove the Cart type
|
// TODO: Support legacy payment provider API> Once we are ready to break the api then we can remove the Cart type
|
||||||
cart:
|
cart:
|
||||||
|
|||||||
Reference in New Issue
Block a user