feat(medusa): Update payment session management (#2937)
This commit is contained in:
committed by
GitHub
parent
71fa60892c
commit
cac81749ea
5
.changeset/swift-kiwis-relate.md
Normal file
5
.changeset/swift-kiwis-relate.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
chore: Update cart payment session management
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EntityManager } from "typeorm"
|
||||
import { AbstractCartCompletionStrategy } from "../../../../interfaces"
|
||||
import { IdempotencyKey } from "../../../../models/idempotency-key"
|
||||
import { IdempotencyKey } from "../../../../models"
|
||||
import { IdempotencyKeyService } from "../../../../services"
|
||||
|
||||
/**
|
||||
|
||||
@@ -195,11 +195,12 @@ export abstract class AbstractPaymentService
|
||||
* @param paymentSessionData
|
||||
* @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
|
||||
* @return it return either a PaymentSessionResponse or PaymentSessionResponse["session_data"] to maintain backward compatibility
|
||||
*/
|
||||
public abstract updatePayment(
|
||||
paymentSessionData: PaymentSessionData,
|
||||
context: Cart & PaymentContext
|
||||
): Promise<PaymentSessionResponse>
|
||||
): Promise<PaymentSessionResponse | PaymentSessionResponse["session_data"]>
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
BeforeInsert,
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
Unique,
|
||||
} from "typeorm"
|
||||
import { BeforeInsert, Column, Entity, Index, JoinColumn, ManyToOne, Unique, } from "typeorm"
|
||||
|
||||
import { BaseEntity } from "../interfaces"
|
||||
import { Cart } from "./cart"
|
||||
@@ -43,6 +35,9 @@ export class PaymentSession extends BaseEntity {
|
||||
@Column({ type: "boolean", nullable: true })
|
||||
is_selected: boolean | null
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
is_initiated: boolean
|
||||
|
||||
@DbAwareColumn({ type: "enum", enum: PaymentSessionStatus })
|
||||
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."
|
||||
* type: boolean
|
||||
* 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:
|
||||
* 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
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant
|
||||
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { taxProviderServiceMock } from "../__mocks__/tax-provider"
|
||||
import { PaymentSessionStatus } from "../../models"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
@@ -1359,32 +1360,71 @@ describe("CartService", () => {
|
||||
|
||||
describe("setPaymentSession", () => {
|
||||
const cartRepository = MockRepository({
|
||||
findOneWithRelations: () => {
|
||||
return Promise.resolve({
|
||||
region: {
|
||||
payment_providers: [
|
||||
findOneWithRelations: (rels, q) => {
|
||||
if (q.where.id === IdMap.getId("cartWithLine")) {
|
||||
return Promise.resolve({
|
||||
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: [],
|
||||
shipping_methods: [],
|
||||
payment_sessions: [
|
||||
{
|
||||
id: IdMap.getId("test-session"),
|
||||
provider_id: "test-provider",
|
||||
})
|
||||
} else if (q.where.id === IdMap.getId("cartWithLine2")) {
|
||||
return Promise.resolve({
|
||||
total: 100,
|
||||
customer: {},
|
||||
region: {
|
||||
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 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({
|
||||
manager: MockManager,
|
||||
paymentSessionRepository,
|
||||
paymentProviderService,
|
||||
totalsService,
|
||||
cartRepository,
|
||||
eventBusService,
|
||||
@@ -1397,22 +1437,63 @@ describe("CartService", () => {
|
||||
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(
|
||||
IdMap.getId("cartWithLine"),
|
||||
"test-provider"
|
||||
providerId
|
||||
)
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
"cart.updated",
|
||||
CartService.Events.UPDATED,
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(paymentSessionRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("test-session"),
|
||||
provider_id: "test-provider",
|
||||
is_selected: true,
|
||||
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||
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"),
|
||||
})
|
||||
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 () => {
|
||||
@@ -1423,13 +1504,16 @@ describe("CartService", () => {
|
||||
})
|
||||
|
||||
describe("setPaymentSessions", () => {
|
||||
const provider1Id = "provider_1"
|
||||
const provider2Id = "provider_2"
|
||||
|
||||
const cart1 = {
|
||||
total: 100,
|
||||
items: [{ subtotal: 100 }],
|
||||
shipping_methods: [],
|
||||
payment_sessions: [],
|
||||
region: {
|
||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
||||
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1437,9 +1521,9 @@ describe("CartService", () => {
|
||||
total: 100,
|
||||
items: [],
|
||||
shipping_methods: [],
|
||||
payment_sessions: [{ provider_id: "provider_1" }],
|
||||
payment_sessions: [{ provider_id: provider1Id }],
|
||||
region: {
|
||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
||||
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1448,11 +1532,11 @@ describe("CartService", () => {
|
||||
items: [{ subtotal: 100 }],
|
||||
shipping_methods: [{ subtotal: 100 }],
|
||||
payment_sessions: [
|
||||
{ provider_id: "provider_1" },
|
||||
{ provider_id: provider1Id },
|
||||
{ provider_id: "not_in_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 }],
|
||||
shipping_methods: [],
|
||||
payment_sessions: [
|
||||
{ provider_id: "provider_1" },
|
||||
{ provider_id: "provider_2" },
|
||||
{ provider_id: provider1Id },
|
||||
{ provider_id: provider2Id },
|
||||
],
|
||||
region: {
|
||||
payment_providers: [{ id: "provider_1" }, { id: "provider_2" }],
|
||||
payment_providers: [{ id: provider1Id }, { id: provider2Id }],
|
||||
},
|
||||
}
|
||||
|
||||
const cart5 = {
|
||||
total: -1,
|
||||
total: 100,
|
||||
items: [{ subtotal: 100 }],
|
||||
shipping_methods: [],
|
||||
payment_sessions: [
|
||||
{ provider_id: "provider_1" },
|
||||
{ provider_id: "provider_2" },
|
||||
{ provider_id: provider1Id, is_initiated: true },
|
||||
{ provider_id: provider2Id, is_selected: true },
|
||||
],
|
||||
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")) {
|
||||
return Promise.resolve(cart4)
|
||||
}
|
||||
if (
|
||||
q.where.id === IdMap.getId("cartWithMixedSelectedInitiatedSessions")
|
||||
) {
|
||||
return Promise.resolve(cart5)
|
||||
}
|
||||
return Promise.resolve(cart1)
|
||||
},
|
||||
})
|
||||
@@ -1507,8 +1598,11 @@ describe("CartService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const paymentSessionRepositoryMock = MockRepository({})
|
||||
|
||||
const cartService = new CartService({
|
||||
manager: MockManager,
|
||||
paymentSessionRepository: paymentSessionRepositoryMock,
|
||||
totalsService,
|
||||
cartRepository,
|
||||
paymentProviderService,
|
||||
@@ -1525,30 +1619,58 @@ describe("CartService", () => {
|
||||
it("initializes payment sessions for each of the providers", async () => {
|
||||
await cartService.setPaymentSessions(IdMap.getId("cartWithLine"))
|
||||
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(2)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledTimes(2)
|
||||
expect(paymentSessionRepositoryMock.save).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledWith({
|
||||
cart_id: IdMap.getId("cartWithLine"),
|
||||
status: PaymentSessionStatus.PENDING,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_1",
|
||||
provider_id: provider1Id,
|
||||
data: {},
|
||||
})
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledWith({
|
||||
cart: cart1,
|
||||
customer: cart1.customer,
|
||||
|
||||
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledWith({
|
||||
cart_id: IdMap.getId("cartWithLine"),
|
||||
status: PaymentSessionStatus.PENDING,
|
||||
amount: cart1.total,
|
||||
currency_code: cart1.region.currency_code,
|
||||
provider_id: "provider_2",
|
||||
provider_id: provider2Id,
|
||||
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 () => {
|
||||
await cartService.setPaymentSessions(IdMap.getId("cart-to-filter"))
|
||||
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(1)
|
||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(1)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(1)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
||||
expect(paymentSessionRepositoryMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(paymentSessionRepositoryMock.save).toHaveBeenCalledTimes(2) // create and update
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||
provider_id: "not_in_region",
|
||||
})
|
||||
})
|
||||
@@ -1556,28 +1678,26 @@ describe("CartService", () => {
|
||||
it("removes if cart total === 0", async () => {
|
||||
await cartService.setPaymentSessions(IdMap.getId("cart-remove"))
|
||||
|
||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
||||
provider_id: "provider_1",
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||
provider_id: provider1Id,
|
||||
})
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
||||
provider_id: "provider_2",
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||
provider_id: provider2Id,
|
||||
})
|
||||
})
|
||||
|
||||
it("removes if cart total < 0", async () => {
|
||||
await cartService.setPaymentSessions(IdMap.getId("cart-negative"))
|
||||
|
||||
expect(paymentProviderService.updateSession).toHaveBeenCalledTimes(0)
|
||||
expect(paymentProviderService.createSession).toHaveBeenCalledTimes(0)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledTimes(2)
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
||||
provider_id: "provider_1",
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||
provider_id: provider1Id,
|
||||
})
|
||||
expect(paymentProviderService.deleteSession).toHaveBeenCalledWith({
|
||||
provider_id: "provider_2",
|
||||
expect(paymentSessionRepositoryMock.delete).toHaveBeenCalledWith({
|
||||
provider_id: provider2Id,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,7 +95,7 @@ describe("PaymentProviderService", () => {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
updatePayment: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
updatePayment: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DiscountRuleType,
|
||||
LineItem,
|
||||
PaymentSession,
|
||||
PaymentSessionStatus,
|
||||
SalesChannel,
|
||||
ShippingMethod,
|
||||
} from "../models"
|
||||
@@ -39,24 +40,24 @@ import { FlagRouter } from "../utils/flag-router"
|
||||
import { validateEmail } from "../utils/is-email"
|
||||
import { PaymentSessionInput } from "../types/payment"
|
||||
import {
|
||||
CustomShippingOptionService,
|
||||
CustomerService,
|
||||
CustomShippingOptionService,
|
||||
DiscountService,
|
||||
EventBusService,
|
||||
GiftCardService,
|
||||
LineItemService,
|
||||
LineItemAdjustmentService,
|
||||
LineItemService,
|
||||
NewTotalsService,
|
||||
PaymentProviderService,
|
||||
ProductService,
|
||||
ProductVariantService,
|
||||
ProductVariantInventoryService,
|
||||
ProductVariantService,
|
||||
RegionService,
|
||||
SalesChannelService,
|
||||
ShippingOptionService,
|
||||
StoreService,
|
||||
TaxProviderService,
|
||||
TotalsService,
|
||||
SalesChannelService,
|
||||
} from "."
|
||||
|
||||
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 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_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const psRepo = transactionManager.getCustomRepository(
|
||||
@@ -1644,30 +1644,34 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
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
|
||||
if (
|
||||
providerId !== "system" &&
|
||||
!(
|
||||
cart.region.payment_providers.length &&
|
||||
cart.region.payment_providers.find(({ id }) => providerId === id)
|
||||
)
|
||||
) {
|
||||
const isProviderPresent = cart.region.payment_providers.find(
|
||||
({ id }) => providerId === id
|
||||
)
|
||||
|
||||
if (providerId !== "system" && !isProviderPresent) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`The payment method is not available in this region`
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
cart.payment_sessions.map(async (paymentSession) => {
|
||||
return psRepo.save({ ...paymentSession, is_selected: null })
|
||||
})
|
||||
const cartPaymentSessionIds = cart.payment_sessions.map((p) => p.id)
|
||||
await psRepo.update(
|
||||
{ id: In(cartPaymentSessionIds) },
|
||||
{
|
||||
is_selected: null,
|
||||
}
|
||||
)
|
||||
|
||||
const paymentSession = cart.payment_sessions.find(
|
||||
let paymentSession = cart.payment_sessions.find(
|
||||
(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_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(CartService.Events.UPDATED, updatedCart)
|
||||
return updatedCart
|
||||
.emit(CartService.Events.UPDATED, { id: cartId })
|
||||
},
|
||||
"SERIALIZABLE"
|
||||
)
|
||||
@@ -1709,6 +1731,9 @@ class CartService extends TransactionBaseService {
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(transactionManager)
|
||||
|
||||
const cartId =
|
||||
typeof cartOrCartId === `string` ? cartOrCartId : cartOrCartId.id
|
||||
|
||||
@@ -1735,77 +1760,135 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
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"> = {
|
||||
cart: cart as Cart,
|
||||
customer: cart.customer,
|
||||
amount: cart.total,
|
||||
amount: total,
|
||||
currency_code: cart.region.currency_code,
|
||||
}
|
||||
|
||||
// If there are existing payment sessions ensure that these are up to date
|
||||
const seen: string[] = []
|
||||
if (cart.payment_sessions?.length) {
|
||||
await Promise.all(
|
||||
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)
|
||||
}
|
||||
})
|
||||
)
|
||||
const partialPaymentSessionData = {
|
||||
cart_id: cartId,
|
||||
data: {},
|
||||
status: PaymentSessionStatus.PENDING,
|
||||
amount: total,
|
||||
}
|
||||
|
||||
if (total > 0) {
|
||||
// If only one payment session exists, we preselect it
|
||||
if (region.payment_providers.length === 1 && !cart.payment_session) {
|
||||
const paymentProvider = region.payment_providers[0]
|
||||
const paymentSessionInput = {
|
||||
...partialSessionInput,
|
||||
provider_id: paymentProvider.id,
|
||||
await Promise.all(
|
||||
cart.payment_sessions.map(async (session) => {
|
||||
if (!providerSet.has(session.provider_id)) {
|
||||
/**
|
||||
* if the provider does not belong to the region then delete the session.
|
||||
* The deletion occurs locally if there is no external data or if it is not selected
|
||||
* otherwise the deletion will also occur remotely through the external provider.
|
||||
*/
|
||||
|
||||
return await deleteSessionAppropriately(session)
|
||||
}
|
||||
|
||||
const paymentSession = await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.createSession(paymentSessionInput)
|
||||
/**
|
||||
* if the provider belongs to the region then update or delete the session.
|
||||
* 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)
|
||||
} else {
|
||||
await Promise.all(
|
||||
region.payment_providers.map(async (paymentProvider) => {
|
||||
if (!seen.includes(paymentProvider.id)) {
|
||||
const paymentSessionInput = {
|
||||
...partialSessionInput,
|
||||
provider_id: paymentProvider.id,
|
||||
}
|
||||
// Update remotely
|
||||
if (session.is_selected) {
|
||||
const paymentSessionInput = {
|
||||
...partialSessionInput,
|
||||
provider_id: session.provider_id,
|
||||
}
|
||||
|
||||
return this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.createSession(paymentSessionInput)
|
||||
}
|
||||
return
|
||||
return paymentProviderServiceTx.updateSession(
|
||||
session,
|
||||
paymentSessionInput
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
cartId: string,
|
||||
providerId: string
|
||||
): Promise<Cart> {
|
||||
): Promise<void> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieve(cartId, {
|
||||
@@ -1840,11 +1923,18 @@ class CartService extends TransactionBaseService {
|
||||
({ provider_id }) => provider_id !== providerId
|
||||
)
|
||||
|
||||
const psRepo = transactionManager.getCustomRepository(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
if (paymentSession) {
|
||||
// Delete the session with the provider
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.deleteSession(paymentSession)
|
||||
if (paymentSession.is_selected || paymentSession.is_initiated) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.deleteSession(paymentSession)
|
||||
} else {
|
||||
await psRepo.delete(paymentSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1852,8 +1942,7 @@ class CartService extends TransactionBaseService {
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(CartService.Events.UPDATED, cart)
|
||||
return cart
|
||||
.emit(CartService.Events.UPDATED, { id: cart.id })
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1863,12 +1952,12 @@ class CartService extends TransactionBaseService {
|
||||
* @param cartId - the id of the cart to remove from
|
||||
* @param providerId - the id of the provider whoose payment session
|
||||
* should be removed.
|
||||
* @return {Promise<Cart>} the resulting cart.
|
||||
* @return {Promise<void>} the resulting cart.
|
||||
*/
|
||||
async refreshPaymentSession(
|
||||
cartId: string,
|
||||
providerId: string
|
||||
): Promise<Cart> {
|
||||
): Promise<void> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieveWithTotals(cartId, {
|
||||
@@ -1881,25 +1970,30 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
if (paymentSession) {
|
||||
// Delete the session with the provider
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.refreshSession(paymentSession, {
|
||||
cart: cart as Cart,
|
||||
customer: cart.customer,
|
||||
if (paymentSession.is_selected) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.refreshSession(paymentSession, {
|
||||
cart: cart as Cart,
|
||||
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,
|
||||
currency_code: cart.region.currency_code,
|
||||
provider_id: providerId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedCart = await this.retrieve(cartId)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(CartService.Events.UPDATED, updatedCart)
|
||||
return updatedCart
|
||||
.emit(CartService.Events.UPDATED, { id: cartId })
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -215,17 +215,15 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
paymentResponse
|
||||
)
|
||||
|
||||
const amount = this.featureFlagRouter_.isFeatureEnabled(
|
||||
OrderEditingFeatureFlag.key
|
||||
)
|
||||
? context.amount
|
||||
: undefined
|
||||
|
||||
return await this.saveSession(providerId, {
|
||||
payment_session_id: !isString(providerIdOrSessionInput)
|
||||
? providerIdOrSessionInput.payment_session_id
|
||||
: undefined,
|
||||
cartId: context.id,
|
||||
sessionData,
|
||||
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 sessionData = await provider
|
||||
const paymentResponse = await provider
|
||||
.withTransaction(transactionManager)
|
||||
.updatePayment(paymentSession.data, context)
|
||||
|
||||
const amount = this.featureFlagRouter_.isFeatureEnabled(
|
||||
OrderEditingFeatureFlag.key
|
||||
const sessionData = paymentResponse.session_data ?? paymentResponse
|
||||
|
||||
await this.processUpdateRequestsData(
|
||||
{
|
||||
customer: { id: context.customer?.id },
|
||||
},
|
||||
paymentResponse
|
||||
)
|
||||
? context.amount
|
||||
: undefined
|
||||
|
||||
return await this.saveSession(paymentSession.provider_id, {
|
||||
payment_session_id: paymentSession.id,
|
||||
sessionData,
|
||||
amount,
|
||||
isInitiated: true,
|
||||
amount: context.amount,
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -687,44 +689,40 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
amount?: number
|
||||
sessionData: Record<string, unknown>
|
||||
isSelected?: boolean
|
||||
isInitiated?: boolean
|
||||
status?: PaymentSessionStatus
|
||||
}
|
||||
): Promise<PaymentSession> {
|
||||
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(
|
||||
this.paymentSessionRepository_
|
||||
)
|
||||
|
||||
// Update an existing session
|
||||
if (data.payment_session_id) {
|
||||
const session = await this.retrieveSession(data.payment_session_id)
|
||||
session.data = data.sessionData ?? session.data
|
||||
session.status = data.status ?? session.status
|
||||
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)
|
||||
} 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 { CartService, PaymentProviderService } from "../services"
|
||||
import { CartService } from "../services"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
cartService: CartService
|
||||
paymentProviderService: PaymentProviderService
|
||||
manager: EntityManager
|
||||
}
|
||||
|
||||
class CartSubscriber {
|
||||
protected readonly manager_: EntityManager
|
||||
protected readonly cartService_: CartService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
protected readonly eventBus_: EventBusService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
}: InjectedDependencies) {
|
||||
constructor({ manager, cartService, eventBusService }: InjectedDependencies) {
|
||||
this.cartService_ = cartService
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.eventBus_ = eventBusService
|
||||
this.manager_ = manager
|
||||
|
||||
@@ -38,30 +30,18 @@ class CartSubscriber {
|
||||
await this.manager_.transaction(
|
||||
"SERIALIZABLE",
|
||||
async (transactionManager) => {
|
||||
const cart = await this.cartService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieveWithTotals(cartId, {
|
||||
relations: [
|
||||
"billing_address",
|
||||
"region",
|
||||
"region.payment_providers",
|
||||
"payment_sessions",
|
||||
"customer",
|
||||
],
|
||||
})
|
||||
const cartServiceTx =
|
||||
this.cartService_.withTransaction(transactionManager)
|
||||
|
||||
const cart = await cartServiceTx.retrieve(cartId, {
|
||||
relations: ["payment_sessions"],
|
||||
})
|
||||
|
||||
if (!cart.payment_sessions?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const paymentProviderServiceTx =
|
||||
this.paymentProviderService_.withTransaction(transactionManager)
|
||||
|
||||
return await Promise.all(
|
||||
cart.payment_sessions.map(async (paymentSession) => {
|
||||
return paymentProviderServiceTx.updateSession(paymentSession, cart)
|
||||
})
|
||||
)
|
||||
return await cartServiceTx.setPaymentSessions(cart.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../models"
|
||||
|
||||
export type PaymentSessionInput = {
|
||||
payment_session_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
|
||||
cart:
|
||||
|
||||
Reference in New Issue
Block a user