feat(medusa,medusa-payment-stripe): Move database mutation from plugin to core (#2743)

**what**
The goal of that PR is to first refactor the payment provider and payment plugin to support the new API that removes the data mutation from within the plugin to be done by the core instead. In any case, this pr does not include the steps of the deeper refactoring. The last part will come in later pr.

**How**
- The payment plugin is now capable to handle both the deprecated and new API and the plugin works the same as it use to works.
- The mutation made by the plugin have been moved into the core as well as the subscriber
- The tests have been updated to reflect the changed
- Remove all new methods introduced by the payment collections
  - Mutualise types
  - Update provider and payment collection services
  - cleanup around all those refactoring including cleanup of the payment collection
  - refactor stripe payment plugin

FIXES CORE-887
This commit is contained in:
Adrien de Peretti authored and GitHub committed 2022-12-19 14:37:35 +00:00
1 parent 8dcc805ccf
commit c8724da503
87 files changed
+1138 -1429

No files matched your search

+2 -2
View File
@@ -29,9 +29,9 @@
"medusa-test-utils": "^1.1.37"
},
"scripts": {
"build": "babel src -d . --ignore **/__tests__",
"build": "babel src -d . --ignore **/__tests__ --ignore **/__mocks__",
"prepare": "cross-env NODE_ENV=production yarn run build",
"watch": "babel -w src --out-dir . --ignore **/__tests__",
"watch": "babel -w src --out-dir . --ignore **/__tests__ --ignore **/__mocks__",
"test": "jest"
},
"peerDependencies": {
@@ -91,6 +91,8 @@ export const carts = {
customer: IdMap.getId("not-lebron"),
},
},
region: { currency_code: "usd" },
total: 100,
shipping_address: {},
billing_address: {},
discounts: [],
@@ -20,7 +20,7 @@ export const StripeMock = {
if (data.customer === "cus_123456789_new") {
return Promise.resolve({
id: "pi_lebron",
amount: 100,
amount: data.amount,
customer: "cus_123456789_new",
description: data?.description,
})
@@ -28,7 +28,7 @@ export const StripeMock = {
if (data.customer === "cus_lebron") {
return Promise.resolve({
id: "pi_lebron",
amount: 100,
amount: data.amount,
customer: "cus_lebron",
description: data?.description,
})
@@ -1,58 +1,13 @@
import { IdMap } from "medusa-test-utils"
import StripeProviderService from "../stripe-provider"
import { CustomerServiceMock } from "../../__mocks__/customer"
import { carts } from "../../__mocks__/cart"
import { TotalsServiceMock } from "../../__mocks__/totals"
import StripeBase from "../stripe-base";
const RegionServiceMock = {
withTransaction: function () {
return this
},
retrieve: jest.fn().mockReturnValue(Promise.resolve({})),
}
describe("StripeProviderService", () => {
describe("createCustomer", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
result = await stripeProviderService.createCustomer({
_id: IdMap.getId("vvd"),
first_name: "Virgil",
last_name: "Van Dijk",
email: "virg@vvd.com",
password_hash: "1234",
metadata: {},
})
})
it("returns created stripe customer", () => {
expect(result).toEqual({
id: "cus_vvd",
email: "virg@vvd.com",
})
})
})
const fakeContainer = {}
describe("StripeBase", () => {
describe("createPayment", () => {
let result
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test"
}
@@ -63,45 +18,88 @@ describe("StripeProviderService", () => {
})
it("returns created stripe payment intent for cart with existing customer", async () => {
result = await stripeProviderService.createPayment(carts.frCart)
const cart = carts.frCart
const context = {
cart,
amount: cart.total,
currency_code: cart.region?.currency_code,
}
Object.assign(context, cart)
result = await stripeBase.createPayment(context)
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_123456789_new",
amount: 100,
session_data: {
id: "pi_lebron",
customer: "cus_lebron",
description: undefined,
amount: 100,
},
update_requests: {
customer_metadata: {
stripe_id: "cus_lebron"
}
}
})
})
it("returns created stripe payment intent for cart with no customer", async () => {
carts.frCart.customer_id = ""
carts.frCart.context.payment_description = 'some description'
result = await stripeProviderService.createPayment(carts.frCart)
const cart = carts.frCart
const context = {
cart,
amount: cart.total,
currency_code: cart.region?.currency_code,
}
Object.assign(context, cart)
context.cart.context.payment_description = 'some description'
result = await stripeBase.createPayment(context)
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
amount: 100,
description: 'some description',
session_data: {
id: "pi_lebron",
customer: "cus_lebron",
description: 'some description',
amount: 100,
},
update_requests: {
customer_metadata: {
stripe_id: "cus_lebron"
}
}
})
})
it("returns created stripe payment intent for cart with no customer and the options default description", async () => {
const localStripeProviderService = new StripeProviderService({
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
const localStripeProviderService = new StripeBase(
fakeContainer,
{
api_key: "test",
payment_description: "test options description"
})
carts.frCart.customer_id = ""
carts.frCart.context.payment_description = null
result = await localStripeProviderService.createPayment(carts.frCart)
const cart = carts.frCart
const context = {
cart,
amount: cart.total,
currency_code: cart.region?.currency_code,
}
Object.assign(context, cart)
context.cart.context.payment_description = null
result = await localStripeProviderService.createPayment(context)
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
amount: 100,
description: "test options description",
session_data: {
id: "pi_lebron",
customer: "cus_lebron",
description: "test options description",
amount: 100,
},
update_requests: {
customer_metadata: {
stripe_id: "cus_lebron"
}
}
})
})
})
@@ -110,18 +108,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.retrievePayment({
result = await stripeBase.retrievePayment({
payment_method: {
data: {
id: "pi_lebron",
@@ -142,18 +136,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.updatePayment(
result = await stripeBase.updatePayment(
{
id: "pi_lebron",
amount: 800,
@@ -177,18 +167,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
regionService: RegionServiceMock,
totalsService: TotalsServiceMock,
},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.updatePaymentIntentCustomer(
result = await stripeBase.updatePaymentIntentCustomer(
"pi_lebron",
"cus_lebron_2"
)
@@ -207,14 +193,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.capturePayment({
result = await stripeBase.capturePayment({
data: {
id: "pi_lebron",
customer: "cus_lebron",
@@ -237,14 +223,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.refundPayment(
result = await stripeBase.refundPayment(
{
data: {
id: "re_123",
@@ -271,14 +257,14 @@ describe("StripeProviderService", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
const stripeBase = new StripeBase(
fakeContainer,
{
api_key: "test",
}
)
result = await stripeProviderService.cancelPayment({
result = await stripeBase.cancelPayment({
data: {
id: "pi_lebron",
customer: "cus_lebron",
@@ -1,29 +1,12 @@
import { AbstractPaymentService, PaymentSessionData } from "@medusajs/medusa"
import { AbstractPaymentService } from "@medusajs/medusa"
import Stripe from "stripe"
import { PaymentSessionStatus } from "@medusajs/medusa/dist";
class StripeBase extends AbstractPaymentService {
static identifier = null
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
/**
* Required Stripe options:
@@ -38,21 +21,6 @@ class StripeBase extends AbstractPaymentService {
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.stripeProviderService_ = stripeProviderService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
/** @private @const {EntityManager} */
this.manager_ = manager
}
getPaymentIntentOptions() {
@@ -74,22 +42,48 @@ class StripeBase extends AbstractPaymentService {
return options
}
/**
* Fetches Stripe payment intent. Check its status and returns the
* corresponding Medusa status.
* @param {PaymentSessionData} paymentSessionData - payment method data from cart
* @return {Promise<PaymentSessionStatus>} the status of the payment intent
/**
* Get payment session status
* statuses.
* @param {PaymentSessionData} paymentData - the data stored with the payment session
* @return {Promise<PaymentSessionStatus>} the status of the order
*/
async getStatus(paymentSessionData) {
return await this.stripeProviderService_.getStatus(paymentSessionData)
async getStatus(paymentData) {
const { id } = paymentData
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
switch (paymentIntent.status) {
case "requires_payment_method":
case "requires_confirmation":
case "processing":
return PaymentSessionStatus.PENDING
case "requires_action":
return PaymentSessionStatus.REQUIRES_MORE
case "canceled":
return PaymentSessionStatus.CANCELED
case "requires_capture":
case "succeeded":
return PaymentSessionStatus.AUTHORIZED
default:
return PaymentSessionStatus.PENDING
}
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @param {Customer} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
if (customer.metadata && customer.metadata.stripe_id) {
const methods = await this.stripe_.paymentMethods.list({
customer: customer.metadata.stripe_id,
type: "card",
})
return methods.data
}
return []
}
@@ -99,49 +93,68 @@ class StripeBase extends AbstractPaymentService {
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
return await this.stripeProviderService_.retrieveCustomer(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createCustomer(customer)
if (!customerId) {
return
}
return await this.stripe_.customers.retrieve(customerId)
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @return {Promise<PaymentSessionData>} Stripe payment intent
* @param {Cart & PaymentContext} context - context to use to create a payment for
* @return {Promise<PaymentSessionResponse>} Stripe payment intent
*/
async createPayment(cart) {
async createPayment(context) {
const intentRequestData = this.getPaymentIntentOptions()
const { id: cart_id, email, context: cart_context, currency_code, amount, resource_id, customer } = context
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createPayment(cart, intentRequestData)
}
const intentRequest = {
description:
cart_context.payment_description ??
this.options_?.payment_description,
amount: Math.round(amount),
currency: currency_code,
metadata: { cart_id, resource_id },
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}
async createPaymentNew(paymentInput) {
const intentRequestData = this.getPaymentIntentOptions()
if (this.options_?.automatic_payment_methods) {
intentRequest.automatic_payment_methods = { enabled: true }
}
return await this.stripeProviderService_
.withTransaction(this.manager_)
.createPaymentNew(paymentInput, intentRequestData)
if (customer?.metadata?.stripe_id) {
intentRequest.customer = customer?.metadata?.stripe_id
} else {
const stripeCustomer = await this.stripe_.customers.create({
email,
})
intentRequest.customer = stripeCustomer.id
}
const session_data = await this.stripe_.paymentIntents.create(
intentRequest
)
return {
session_data,
update_requests: {
customer_metadata: {
stripe_id: intentRequest.customer
}
}
}
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @param {PaymentData} data - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(paymentData) {
return await this.stripeProviderService_.retrievePayment(paymentData)
async retrievePayment(data) {
return await this.stripe_.paymentIntents.retrieve(data.id)
}
/**
@@ -150,67 +163,73 @@ class StripeBase extends AbstractPaymentService {
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
return await this.stripeProviderService_.getPaymentData(paymentSession)
return await this.stripe_.paymentIntents.retrieve(paymentSession.data.id)
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {object} context - properties relevant to current context
* @return {Promise<{data: PaymentSessionData; status: PaymentSessionStatus}>} result with data and status
* @param {Data} context - properties relevant to current context
* @return {Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
return await this.stripeProviderService_.authorizePayment(
paymentSession,
context
)
const stat = await this.getStatus(paymentSession.data)
return { data: paymentSession.data, status: stat }
}
async updatePaymentData(paymentSessionData, data) {
return await this.stripeProviderService_.updatePaymentData(
paymentSessionData,
data
)
async updatePaymentData(sessionData, update) {
return await this.stripe_.paymentIntents.update(sessionData.id, {
...update.data,
})
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @param {Cart & PaymentContext} context
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart) {
const intentRequestData = this.getPaymentIntentOptions()
async updatePayment(paymentSessionData, context) {
const { amount, customer } = context
const stripeId = customer?.metadata?.stripe_id || undefined
return await this.stripeProviderService_
.withTransaction(this.manager_)
.updatePayment(paymentSessionData, cart, intentRequestData)
if (stripeId !== paymentSessionData.customer) {
return await this.createPayment(context)
} else {
if (
amount &&
paymentSessionData.amount === Math.round(amount)
) {
return paymentSessionData
}
return await this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(amount),
})
}
}
async updatePaymentNew(paymentSessionData, paymentInput) {
const intentRequestData = this.getPaymentIntentOptions()
return await this.stripeProviderService_
.withTransaction(this.manager_)
.updatePaymentNew(paymentSessionData, paymentInput, intentRequestData)
}
async deletePayment(paymentSession) {
return await this.stripeProviderService_.deletePayment(paymentSession)
async deletePayment(payment) {
const { id } = payment.data
return this.stripe_.paymentIntents.cancel(id).catch((err) => {
if (err.statusCode === 400) {
return
}
throw err
})
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @param {string} customerId - id of \ Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
return await this.stripeProviderService_.updatePaymentIntentCustomer(
paymentIntentId,
customerId
)
return await this.stripe_.paymentIntents.update(paymentIntentId, {
customer: customerId,
})
}
/**
@@ -219,7 +238,18 @@ class StripeBase extends AbstractPaymentService {
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
return await this.stripeProviderService_.capturePayment(payment)
const { id } = payment.data
try {
const intent = await this.stripe_.paymentIntents.capture(id)
return intent
} catch (error) {
if (error.code === "payment_intent_unexpected_state") {
if (error.payment_intent.status === "succeeded") {
return error.payment_intent
}
}
throw error
}
}
/**
@@ -228,11 +258,14 @@ class StripeBase extends AbstractPaymentService {
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, refundAmount) {
return await this.stripeProviderService_.refundPayment(
payment,
refundAmount
)
async refundPayment(payment, amountToRefund) {
const { id } = payment.data
await this.stripe_.refunds.create({
amount: Math.round(amountToRefund),
payment_intent: id,
})
return payment.data
}
/**
@@ -241,7 +274,31 @@ class StripeBase extends AbstractPaymentService {
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
return await this.stripeProviderService_.cancelPayment(payment)
const { id } = payment.data
try {
return await this.stripe_.paymentIntents.cancel(id)
} catch (error) {
if (error.payment_intent.status === "canceled") {
return error.payment_intent
}
throw error
}
}
/**
* Constructs Stripe Webhook event
* @param {object} data - the data of the webhook request: req.body
* @param {object} signature - the Stripe signature on the event, that
* ensures integrity of the webhook event
* @return {object} Stripe Webhook event
*/
constructWebhookEvent(data, signature) {
return this.stripe_.webhooks.constructEvent(
data,
signature,
this.options_.webhook_secret
)
}
}
@@ -1,55 +0,0 @@
import { IdMap } from "medusa-test-utils"
export const StripeProviderServiceMock = {
withTransaction: function () {
return this
},
retrievePayment: jest.fn().mockImplementation((payData) => {
if (payData.id === "pi_123456789") {
return Promise.resolve({
id: "pi",
customer: "cus_123456789",
})
}
if (payData.id === "pi_no") {
return Promise.resolve({
id: "pi_no",
})
}
return Promise.resolve(undefined)
}),
cancelPayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
updatePaymentIntentCustomer: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
retrieveCustomer: jest.fn().mockImplementation((customerId) => {
if (customerId === "cus_123456789_new") {
return Promise.resolve({
id: "cus_123456789_new",
})
}
return Promise.resolve(undefined)
}),
createCustomer: jest.fn().mockImplementation((customer) => {
if (customer._id === IdMap.getId("vvd")) {
return Promise.resolve({
id: "cus_123456789_new_vvd",
})
}
return Promise.resolve(undefined)
}),
createPayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve({
id: "pi_new",
customer: "cus_123456789_new",
})
}),
}
const mock = jest.fn().mockImplementation(() => {
return StripeProviderServiceMock
})
export default mock
@@ -3,26 +3,8 @@ import StripeBase from "../helpers/stripe-base"
class BancontactProviderService extends StripeBase {
static identifier = "stripe-bancontact"
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions() {
@@ -3,26 +3,8 @@ import StripeBase from "../helpers/stripe-base"
class BlikProviderService extends StripeBase {
static identifier = "stripe-blik"
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions() {
@@ -3,26 +3,8 @@ import StripeBase from "../helpers/stripe-base"
class GiropayProviderService extends StripeBase {
static identifier = "stripe-giropay"
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions() {
@@ -3,26 +3,8 @@ import StripeBase from "../helpers/stripe-base"
class IdealProviderService extends StripeBase {
static identifier = "stripe-ideal"
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions() {
@@ -1,424 +1,14 @@
import {
AbstractPaymentService,
PaymentSessionData,
PaymentSessionStatus,
} from "@medusajs/medusa"
import Stripe from "stripe"
import StripeBase from "../helpers/stripe-base";
class StripeProviderService extends AbstractPaymentService {
class StripeProviderService extends StripeBase {
static identifier = "stripe"
constructor(
{ customerService, totalsService, regionService, manager },
options
) {
super({ customerService, totalsService, regionService, manager }, options)
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
/** @private @const {EntityManager} */
this.manager_ = manager
constructor(_, options) {
super(_, options)
}
/**
* Get payment session status
* statuses.
* @param {PaymentSessionData} paymentData - the data stored with the payment session
* @return {Promise<PaymentSessionStatus>} the status of the order
*/
async getStatus(paymentData) {
const { id } = paymentData
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
switch (paymentIntent.status) {
case "requires_payment_method":
case "requires_confirmation":
case "processing":
return PaymentSessionStatus.PENDING
case "requires_action":
return PaymentSessionStatus.REQUIRES_MORE
case "canceled":
return PaymentSessionStatus.CANCELED
case "requires_capture":
case "succeeded":
return PaymentSessionStatus.AUTHORIZED
default:
return PaymentSessionStatus.PENDING
}
}
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {Customer} customer - customer to fetch saved cards for
* @return {Promise<Data[]>} saved payments methods
*/
async retrieveSavedMethods(customer) {
if (customer.metadata && customer.metadata.stripe_id) {
const methods = await this.stripe_.paymentMethods.list({
customer: customer.metadata.stripe_id,
type: "card",
})
return methods.data
}
return []
}
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @return {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) {
if (!customerId) {
return Promise.resolve()
}
return this.stripe_.customers.retrieve(customerId)
}
/**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @return {Promise<object>} Stripe customer
*/
async createCustomer(customer) {
try {
const stripeCustomer = await this.stripe_.customers.create({
email: customer.email,
})
if (customer.id) {
await this.customerService_
.withTransaction(this.manager_)
.update(customer.id, {
metadata: { stripe_id: stripeCustomer.id },
})
}
return stripeCustomer
} catch (error) {
throw error
}
}
/**
* Creates a Stripe payment intent.
* If customer is not registered in Stripe, we do so.
* @param {Cart} cart - cart to create a payment for
* @param intentRequestData
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async createPayment(cart, intentRequestData = {}) {
const { customer_id, region_id, email } = cart
const { currency_code } = await this.regionService_
.withTransaction(this.manager_)
.retrieve(region_id)
const amount = cart.total
const intentRequest = {
description:
cart?.context?.payment_description ??
this.options_?.payment_description,
amount: Math.round(amount),
currency: currency_code,
metadata: { cart_id: `${cart.id}` },
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}
if (this.options_?.automatic_payment_methods) {
intentRequest.automatic_payment_methods = { enabled: true }
}
if (customer_id) {
const customer = await this.customerService_
.withTransaction(this.manager_)
.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
async createPaymentNew(paymentInput, intentRequestData = {}) {
const { customer, currency_code, amount, resource_id, cart } = paymentInput
const { id: customer_id, email } = customer ?? {}
const intentRequest = {
description:
cart?.context?.payment_description ??
this.options_?.payment_description,
amount: Math.round(amount),
currency: currency_code,
metadata: { resource_id },
capture_method: this.options_.capture ? "automatic" : "manual",
...intentRequestData,
}
if (customer_id) {
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else if (email) {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
return await this.stripe_.paymentIntents.create(intentRequest)
}
/**
* Retrieves Stripe payment intent.
* @param {PaymentData} paymentData - the data of the payment to retrieve
* @return {Promise<Data>} Stripe payment intent
*/
async retrievePayment(data) {
try {
return await this.stripe_.paymentIntents.retrieve(data.id)
} catch (error) {
throw error
}
}
/**
* Gets a Stripe payment intent and returns it.
* @param {PaymentSession} paymentSession - the data of the payment to retrieve
* @return {Promise<PaymentData>} Stripe payment intent
*/
async getPaymentData(paymentSession) {
try {
return await this.stripe_.paymentIntents.retrieve(paymentSession.data.id)
} catch (error) {
throw error
}
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent in use.
* @param {PaymentSession} paymentSession - payment session data
* @param {Data} context - properties relevant to current context
* @return {Promise<{ data: PaymentSessionData; status: PaymentSessionStatus }>} result with data and status
*/
async authorizePayment(paymentSession, context = {}) {
const stat = await this.getStatus(paymentSession.data)
try {
return { data: paymentSession.data, status: stat }
} catch (error) {
throw error
}
}
async updatePaymentData(sessionData, update) {
try {
return await this.stripe_.paymentIntents.update(sessionData.id, {
...update.data,
})
} catch (error) {
throw error
}
}
/**
* Updates Stripe payment intent.
* @param {PaymentSessionData} paymentSessionData - payment session data.
* @param {Cart} cart
* @param intentRequestData
* @return {Promise<PaymentSessionData>} Stripe payment intent
*/
async updatePayment(paymentSessionData, cart, intentRequestData) {
try {
const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== paymentSessionData.customer) {
return await this.createPayment(cart, intentRequestData)
} else {
if (
cart.total &&
paymentSessionData.amount === Math.round(cart.total)
) {
return paymentSessionData
}
return await this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(cart.total),
})
}
} catch (error) {
throw error
}
}
async updatePaymentNew(paymentSessionData, paymentInput, intentRequestData) {
try {
const stripeId = paymentInput.customer?.metadata?.stripe_id
if (stripeId !== paymentSessionData.customer) {
return await this.createPaymentNew(paymentInput, intentRequestData)
} else {
if (paymentSessionData.amount === Math.round(paymentInput.amount)) {
return paymentSessionData
}
return await this.stripe_.paymentIntents.update(paymentSessionData.id, {
amount: Math.round(paymentInput.amount),
})
}
} catch (error) {
throw error
}
}
async deletePayment(payment) {
try {
const { id } = payment.data
return this.stripe_.paymentIntents.cancel(id).catch((err) => {
if (err.statusCode === 400) {
return
}
throw err
})
} catch (error) {
throw error
}
}
/**
* Updates customer of Stripe payment intent.
* @param {string} paymentIntentId - id of payment intent to update
* @param {string} customerId - id of new Stripe customer
* @return {object} Stripe payment intent
*/
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
try {
return await this.stripe_.paymentIntents.update(paymentIntentId, {
customer: customerId,
})
} catch (error) {
throw error
}
}
/**
* Captures payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} Stripe payment intent
*/
async capturePayment(payment) {
const { id } = payment.data
try {
const intent = await this.stripe_.paymentIntents.capture(id)
return intent
} catch (error) {
if (error.code === "payment_intent_unexpected_state") {
if (error.payment_intent.status === "succeeded") {
return error.payment_intent
}
}
throw error
}
}
/**
* Refunds payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @param {number} refundAmount - amount to refund
* @return {Promise<PaymentData>} refunded payment intent
*/
async refundPayment(payment, amountToRefund) {
const { id } = payment.data
try {
await this.stripe_.refunds.create({
amount: Math.round(amountToRefund),
payment_intent: id,
})
return payment.data
} catch (error) {
throw error
}
}
/**
* Cancels payment for Stripe payment intent.
* @param {Payment} payment - payment method data from cart
* @return {Promise<PaymentData>} canceled payment intent
*/
async cancelPayment(payment) {
const { id } = payment.data
try {
return await this.stripe_.paymentIntents.cancel(id)
} catch (error) {
if (error.payment_intent.status === "canceled") {
return error.payment_intent
}
throw error
}
}
/**
* Constructs Stripe Webhook event
* @param {object} data - the data of the webhook request: req.body
* @param {object} signature - the Stripe signature on the event, that
* ensures integrity of the webhook event
* @return {object} Stripe Webhook event
*/
constructWebhookEvent(data, signature) {
return this.stripe_.webhooks.constructEvent(
data,
signature,
this.options_.webhook_secret
)
get paymentIntentOptions() {
return {}
}
}
@@ -3,26 +3,8 @@ import StripeBase from "../helpers/stripe-base"
class Przelewy24ProviderService extends StripeBase {
static identifier = "stripe-przelewy24"
constructor(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
) {
super(
{
stripeProviderService,
customerService,
totalsService,
regionService,
manager,
},
options
)
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions() {
@@ -1,60 +0,0 @@
class CartSubscriber {
constructor({
manager,
cartService,
paymentProviderService,
eventBusService,
}) {
this.cartService_ = cartService
this.paymentProviderService_ = paymentProviderService
this.eventBus_ = eventBusService
this.manager_ = manager
this.eventBus_.subscribe("cart.customer_updated", async (cart) => {
await this.onCustomerUpdated(cart)
})
}
async onCustomerUpdated(cartId) {
await this.manager_.transaction(async (transactionManager) => {
const cart = await this.cartService_
.withTransaction(transactionManager)
.retrieve(cartId, {
select: [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"gift_card_total",
"total",
],
relations: [
"billing_address",
"shipping_address",
"region",
"region.payment_providers",
"items",
"items.adjustments",
"payment_sessions",
"customer",
],
})
if (!cart.payment_sessions?.length) {
return Promise.resolve()
}
const session = cart.payment_sessions.find(
(ps) => ps.provider_id === "stripe"
)
if (session) {
return await this.paymentProviderService_
.withTransaction(transactionManager)
.updateSession(session, cart)
}
})
}
}
export default CartSubscriber