feat(medusa-payment-stripe): Stripe PaymentProcessor implementation (#3257)
This commit is contained in:
committed by
GitHub
parent
d61d6c7b7f
commit
589d1c09b0
7
.changeset/eleven-cycles-mate.md
Normal file
7
.changeset/eleven-cycles-mate.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"medusa-payment-paypal": minor
|
||||
"medusa-payment-stripe": minor
|
||||
"@medusajs/medusa": minor
|
||||
---
|
||||
|
||||
feat(medusa-payment-stripe): Implement payment processor API on stripe plugin and fix web hook issues
|
||||
@@ -7,6 +7,7 @@ jest*
|
||||
packages/*
|
||||
# List of packages to Lint
|
||||
!packages/medusa
|
||||
!packages/medusa-payment-stripe
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,10 @@ module.exports = {
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
project: "./packages/medusa/tsconfig.json",
|
||||
project: [
|
||||
"./packages/medusa/tsconfig.json",
|
||||
"./packages/medusa-payment-stripe/tsconfig.spec.json",
|
||||
]
|
||||
},
|
||||
rules: {
|
||||
"valid-jsdoc": "off",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "medusa-payment-paypal",
|
||||
"version": "1.2.10",
|
||||
"description": "Paypal Payment provider for Meduas Commerce",
|
||||
"description": "Paypal Payment provider for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
14
packages/medusa-payment-stripe/.gitignore
vendored
14
packages/medusa-payment-stripe/.gitignore
vendored
@@ -1,16 +1,4 @@
|
||||
/lib
|
||||
dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/helpers
|
||||
/__mocks__
|
||||
@@ -1,8 +0,0 @@
|
||||
.DS_store
|
||||
src
|
||||
dist
|
||||
yarn.lock
|
||||
.babelrc
|
||||
|
||||
.turbo
|
||||
.yarn
|
||||
@@ -10,10 +10,18 @@ Learn more about how you can use this plugin in the [documentaion](https://docs.
|
||||
{
|
||||
api_key: "STRIPE_API_KEY",
|
||||
webhook_secret: "STRIPE_WEBHOOK_SECRET",
|
||||
automatic_payment_methods: true
|
||||
|
||||
// automatic_payment_methods: true,
|
||||
|
||||
// This description will be used if the cart context does not provide one.
|
||||
// payment_description: "custom description to apply",
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic Payment Methods
|
||||
|
||||
If you wish to use [Stripe's automatic payment methods](https://stripe.com/docs/connect/automatic-payment-methods) set the `automatic_payment_methods` flag to true.
|
||||
|
||||
## Deprecation
|
||||
|
||||
The stripe plugin version `>=1.2.x` requires medusa `>=1.8.x`
|
||||
@@ -1 +0,0 @@
|
||||
// noop
|
||||
13
packages/medusa-payment-stripe/jest.config.js
Normal file
13
packages/medusa-payment-stripe/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
}
|
||||
@@ -2,46 +2,37 @@
|
||||
"name": "medusa-payment-stripe",
|
||||
"version": "1.1.53",
|
||||
"description": "Stripe Payment provider for Meduas Commerce",
|
||||
"main": "index.js",
|
||||
"main": "dist/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-payment-stripe"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "Medusa",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"medusa-interfaces": "^1.3.6",
|
||||
"medusa-test-utils": "^1.1.37"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||
"test": "jest --passWithNoTests src",
|
||||
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
|
||||
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@medusajs/medusa": "^1.7.7",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.3.6"
|
||||
"@medusajs/medusa": "^1.7.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^1.1.39",
|
||||
"stripe": "^8.50.0"
|
||||
"medusa-core-utils": "^1.1.38",
|
||||
"stripe": "^11.10.0"
|
||||
},
|
||||
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
|
||||
"keywords": [
|
||||
|
||||
30
packages/medusa-payment-stripe/src/__fixtures__/data.ts
Normal file
30
packages/medusa-payment-stripe/src/__fixtures__/data.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const PaymentIntentDataByStatus = {
|
||||
REQUIRES_PAYMENT_METHOD: {
|
||||
id: "requires_payment_method",
|
||||
status: "requires_payment_method",
|
||||
},
|
||||
REQUIRES_CONFIRMATION: {
|
||||
id: "requires_confirmation",
|
||||
status: "requires_confirmation",
|
||||
},
|
||||
PROCESSING: {
|
||||
id: "processing",
|
||||
status: "processing",
|
||||
},
|
||||
REQUIRES_ACTION: {
|
||||
id: "requires_action",
|
||||
status: "requires_action",
|
||||
},
|
||||
CANCELED: {
|
||||
id: "canceled",
|
||||
status: "canceled",
|
||||
},
|
||||
REQUIRES_CAPTURE: {
|
||||
id: "requires_capture",
|
||||
status: "requires_capture",
|
||||
},
|
||||
SUCCEEDED: {
|
||||
id: "succeeded",
|
||||
status: "succeeded",
|
||||
},
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const carts = {
|
||||
emptyCart: {
|
||||
id: IdMap.getId("emptyCart"),
|
||||
items: [],
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
customer_id: "test-customer",
|
||||
payment_sessions: [],
|
||||
shipping_options: [
|
||||
{
|
||||
id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
data: {
|
||||
some_data: "yes",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
frCart: {
|
||||
id: IdMap.getId("fr-cart"),
|
||||
email: "lebron@james.com",
|
||||
title: "test",
|
||||
region_id: IdMap.getId("region-france"),
|
||||
items: [
|
||||
{
|
||||
id: IdMap.getId("line"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
|
||||
unit_price: 8,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
id: IdMap.getId("product"),
|
||||
},
|
||||
// {
|
||||
// unit_price: 10,
|
||||
// variant: {
|
||||
// id: IdMap.getId("eur-10-us-12"),
|
||||
// },
|
||||
// product: {
|
||||
// id: IdMap.getId("product"),
|
||||
// },
|
||||
// quantity: 1,
|
||||
// },
|
||||
quantity: 10,
|
||||
},
|
||||
{
|
||||
id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
shipping_options: [
|
||||
{
|
||||
id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
payment_sessions: [
|
||||
{
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_123456789",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
],
|
||||
payment_method: {
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_123456789",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
region: { currency_code: "usd" },
|
||||
total: 100,
|
||||
shipping_address: {},
|
||||
billing_address: {},
|
||||
discounts: [],
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
context: {}
|
||||
},
|
||||
frCartNoStripeCustomer: {
|
||||
id: IdMap.getId("fr-cart-no-customer"),
|
||||
title: "test",
|
||||
region_id: IdMap.getId("region-france"),
|
||||
items: [
|
||||
{
|
||||
id: IdMap.getId("line"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: [
|
||||
{
|
||||
unit_price: 8,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
quantity: 10,
|
||||
},
|
||||
{
|
||||
id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
shipping_options: [
|
||||
{
|
||||
id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
payment_sessions: [
|
||||
{
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_no",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
],
|
||||
payment_method: {
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_no",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
shipping_address: {},
|
||||
billing_address: {},
|
||||
discounts: [],
|
||||
customer_id: IdMap.getId("vvd"),
|
||||
},
|
||||
}
|
||||
|
||||
export const CartServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockImplementation((cartId) => {
|
||||
if (cartId === IdMap.getId("fr-cart")) {
|
||||
return Promise.resolve(carts.frCart)
|
||||
}
|
||||
if (cartId === IdMap.getId("fr-cart-no-customer")) {
|
||||
return Promise.resolve(carts.frCartNoStripeCustomer)
|
||||
}
|
||||
if (cartId === IdMap.getId("emptyCart")) {
|
||||
return Promise.resolve(carts.emptyCart)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
updatePaymentSession: jest
|
||||
.fn()
|
||||
.mockImplementation((cartId, stripe, paymentIntent) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return CartServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,39 +0,0 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const CustomerServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockImplementation((id) => {
|
||||
if (id === IdMap.getId("lebron")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("lebron"),
|
||||
first_name: "LeBron",
|
||||
last_name: "James",
|
||||
email: "lebron@james.com",
|
||||
password_hash: "1234",
|
||||
metadata: {
|
||||
stripe_id: "cus_123456789_new",
|
||||
},
|
||||
})
|
||||
}
|
||||
if (id === IdMap.getId("vvd")) {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("vvd"),
|
||||
first_name: "Virgil",
|
||||
last_name: "Van Dijk",
|
||||
email: "virg@vvd.com",
|
||||
password_hash: "1234",
|
||||
metadata: {},
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
setMetadata: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return CustomerServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,10 +0,0 @@
|
||||
export const EventBusServiceMock = {
|
||||
emit: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return EventBusServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,87 +0,0 @@
|
||||
export const StripeMock = {
|
||||
customers: {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
if (data.email === "virg@vvd.com") {
|
||||
return Promise.resolve({
|
||||
id: "cus_vvd",
|
||||
email: "virg@vvd.com",
|
||||
})
|
||||
}
|
||||
if (data.email === "lebron@james.com") {
|
||||
return Promise.resolve({
|
||||
id: "cus_lebron",
|
||||
email: "lebron@james.com",
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
paymentIntents: {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
if (data.customer === "cus_123456789_new") {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
amount: data.amount,
|
||||
customer: "cus_123456789_new",
|
||||
description: data?.description,
|
||||
})
|
||||
}
|
||||
if (data.customer === "cus_lebron") {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
amount: data.amount,
|
||||
customer: "cus_lebron",
|
||||
description: data?.description,
|
||||
})
|
||||
}
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
})
|
||||
}),
|
||||
update: jest.fn().mockImplementation((pi, data) => {
|
||||
if (data.customer === "cus_lebron_2") {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron_2",
|
||||
amount: 1000,
|
||||
})
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
amount: 1000,
|
||||
})
|
||||
}),
|
||||
capture: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
amount: 1000,
|
||||
status: "succeeded",
|
||||
})
|
||||
}),
|
||||
cancel: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
status: "cancelled",
|
||||
})
|
||||
}),
|
||||
},
|
||||
refunds: {
|
||||
create: jest.fn().mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
id: "re_123",
|
||||
payment_intent: "pi_lebron",
|
||||
amount: 1000,
|
||||
status: "succeeded",
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
const stripe = jest.fn(() => StripeMock)
|
||||
|
||||
export default stripe
|
||||
99
packages/medusa-payment-stripe/src/__mocks__/stripe.ts
Normal file
99
packages/medusa-payment-stripe/src/__mocks__/stripe.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { PaymentIntentDataByStatus } from "../__fixtures__/data"
|
||||
import Stripe from "stripe";
|
||||
import { ErrorCodes, ErrorIntentStatus } from "../types";
|
||||
|
||||
export const WRONG_CUSTOMER_EMAIL = "wrong@test.fr"
|
||||
export const EXISTING_CUSTOMER_EMAIL = "right@test.fr"
|
||||
export const STRIPE_ID = "test"
|
||||
export const PARTIALLY_FAIL_INTENT_ID = "partially_unknown"
|
||||
export const FAIL_INTENT_ID = "unknown"
|
||||
|
||||
export const StripeMock = {
|
||||
paymentIntents: {
|
||||
retrieve: jest.fn().mockImplementation(async (paymentId) => {
|
||||
if (paymentId === FAIL_INTENT_ID) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
return Object.values(PaymentIntentDataByStatus).find(value => {
|
||||
return value.id === paymentId
|
||||
}) ?? {}
|
||||
}),
|
||||
update: jest.fn().mockImplementation(async (paymentId, updateData) => {
|
||||
if (paymentId === FAIL_INTENT_ID) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
const data = Object.values(PaymentIntentDataByStatus).find(value => {
|
||||
return value.id === paymentId
|
||||
}) ?? {}
|
||||
|
||||
return { ...data, ...updateData }
|
||||
}),
|
||||
create: jest.fn().mockImplementation(async (data) => {
|
||||
if (data.description === "fail") {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
return data
|
||||
}),
|
||||
cancel: jest.fn().mockImplementation(async (paymentId) => {
|
||||
if (paymentId === FAIL_INTENT_ID) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
if (paymentId === PARTIALLY_FAIL_INTENT_ID) {
|
||||
throw new Stripe.errors.StripeError({
|
||||
code: ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE,
|
||||
payment_intent: {
|
||||
id: paymentId,
|
||||
status: ErrorIntentStatus.CANCELED
|
||||
} as unknown as Stripe.PaymentIntent,
|
||||
type: "invalid_request_error"
|
||||
})
|
||||
}
|
||||
|
||||
return { id: paymentId }
|
||||
}),
|
||||
capture: jest.fn().mockImplementation(async (paymentId) => {
|
||||
if (paymentId === FAIL_INTENT_ID) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
if (paymentId === PARTIALLY_FAIL_INTENT_ID) {
|
||||
throw new Stripe.errors.StripeError({
|
||||
code: ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE,
|
||||
payment_intent: {
|
||||
id: paymentId,
|
||||
status: ErrorIntentStatus.SUCCEEDED
|
||||
} as unknown as Stripe.PaymentIntent,
|
||||
type: "invalid_request_error"
|
||||
})
|
||||
}
|
||||
|
||||
return { id: paymentId }
|
||||
})
|
||||
},
|
||||
refunds: {
|
||||
create: jest.fn().mockImplementation(async ({ payment_intent: paymentId }) => {
|
||||
if (paymentId === FAIL_INTENT_ID) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
return { id: paymentId }
|
||||
})
|
||||
},
|
||||
customers: {
|
||||
create: jest.fn().mockImplementation(async (data) => {
|
||||
if (data.email === EXISTING_CUSTOMER_EMAIL) {
|
||||
return { id: STRIPE_ID, ...data }
|
||||
}
|
||||
|
||||
throw new Error("Error")
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const stripe = jest.fn(() => StripeMock)
|
||||
|
||||
export default stripe
|
||||
@@ -1,12 +0,0 @@
|
||||
export const TotalsServiceMock = {
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
getTotal: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return TotalsServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -1,6 +1,7 @@
|
||||
import stripeHooks from "./stripe"
|
||||
import { Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
import middlewares from "../../middlewares"
|
||||
import { wrapHandler } from "@medusajs/medusa"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -11,7 +12,7 @@ export default (app) => {
|
||||
"/hooks",
|
||||
// stripe constructEvent fails without body-parser
|
||||
bodyParser.raw({ type: "application/json" }),
|
||||
middlewares.wrap(require("./stripe").default)
|
||||
wrapHandler(stripeHooks)
|
||||
)
|
||||
return app
|
||||
}
|
||||
25
packages/medusa-payment-stripe/src/api/hooks/stripe.ts
Normal file
25
packages/medusa-payment-stripe/src/api/hooks/stripe.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Request, Response } from "express"
|
||||
import { constructWebhook, handlePaymentHook } from "../utils/utils"
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
let event
|
||||
try {
|
||||
event = constructWebhook({
|
||||
signature: req.headers["stripe-signature"],
|
||||
body: req.body,
|
||||
container: req.scope,
|
||||
})
|
||||
} catch (err) {
|
||||
res.status(400).send(`Webhook Error: ${err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
const paymentIntent = event.data.object
|
||||
|
||||
const { statusCode } = await handlePaymentHook({
|
||||
event,
|
||||
container: req.scope,
|
||||
paymentIntent,
|
||||
})
|
||||
res.sendStatus(statusCode)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
import hooks from "./hooks"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
@@ -1,3 +0,0 @@
|
||||
export default (fn) =>
|
||||
(...args) =>
|
||||
fn(...args).catch(args[2])
|
||||
@@ -1,5 +0,0 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import { PostgresError } from "@medusajs/medusa/dist/utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const signature = req.headers["stripe-signature"]
|
||||
|
||||
let event
|
||||
try {
|
||||
const stripeProviderService = req.scope.resolve("pp_stripe")
|
||||
event = stripeProviderService.constructWebhookEvent(req.body, signature)
|
||||
} catch (err) {
|
||||
res.status(400).send(`Webhook Error: ${err.message}`)
|
||||
return
|
||||
}
|
||||
|
||||
function isPaymentCollection(id) {
|
||||
return id && id.startsWith("paycol")
|
||||
}
|
||||
|
||||
async function handleCartPayments(event, req, res, cartId) {
|
||||
const manager = req.scope.resolve("manager")
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
const order = await orderService
|
||||
.retrieveByCartId(cartId)
|
||||
.catch(() => undefined)
|
||||
|
||||
// handle payment intent events
|
||||
switch (event.type) {
|
||||
case "payment_intent.succeeded":
|
||||
if (order) {
|
||||
// If order is created but not captured, we attempt to do so
|
||||
if (order.payment_status !== "captured") {
|
||||
await manager.transaction(async (manager) => {
|
||||
await orderService
|
||||
.withTransaction(manager)
|
||||
.capturePayment(order.id)
|
||||
})
|
||||
} else {
|
||||
// Otherwise, respond with 200 preventing Stripe from retrying
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
} else {
|
||||
// If order is not created, we respond with 404 to trigger Stripe retry mechanism
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
break
|
||||
case "payment_intent.amount_capturable_updated":
|
||||
try {
|
||||
await manager.transaction(async (manager) => {
|
||||
await paymentIntentAmountCapturableEventHandler({
|
||||
order,
|
||||
cartId,
|
||||
container: req.scope,
|
||||
transactionManager: manager,
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
let message = `Stripe webhook ${event} handling failed\n${
|
||||
err?.detail ?? err?.message
|
||||
}`
|
||||
if (err?.code === PostgresError.SERIALIZATION_FAILURE) {
|
||||
message = `Stripe webhook ${event} handle failed. This can happen when this webhook is triggered during a cart completion and can be ignored. This event should be retried automatically.\n${
|
||||
err?.detail ?? err?.message
|
||||
}`
|
||||
}
|
||||
this.logger_.warn(message)
|
||||
return res.sendStatus(409)
|
||||
}
|
||||
break
|
||||
default:
|
||||
res.sendStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
async function handlePaymentCollection(event, req, res, id, paymentIntentId) {
|
||||
const manager = req.scope.resolve("manager")
|
||||
const paymentCollectionService = req.scope.resolve(
|
||||
"paymentCollectionService"
|
||||
)
|
||||
|
||||
const paycol = await paymentCollectionService
|
||||
.retrieve(id, { relations: ["payments"] })
|
||||
.catch(() => undefined)
|
||||
|
||||
if (paycol?.payments?.length) {
|
||||
if (event.type === "payment_intent.succeeded") {
|
||||
const payment = paycol.payments.find(
|
||||
(pay) => pay.data.id === paymentIntentId
|
||||
)
|
||||
if (payment && !payment.captured_at) {
|
||||
await manager.transaction(async (manager) => {
|
||||
await paymentCollectionService
|
||||
.withTransaction(manager)
|
||||
.capture(payment.id)
|
||||
})
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
return
|
||||
}
|
||||
}
|
||||
res.sendStatus(204)
|
||||
}
|
||||
|
||||
const paymentIntent = event.data.object
|
||||
const cartId = paymentIntent.metadata.cart_id // Backward compatibility
|
||||
const resourceId = paymentIntent.metadata.resource_id
|
||||
|
||||
if (isPaymentCollection(resourceId)) {
|
||||
await handlePaymentCollection(event, req, res, resourceId, paymentIntent.id)
|
||||
} else {
|
||||
await handleCartPayments(event, req, res, cartId ?? resourceId)
|
||||
}
|
||||
}
|
||||
|
||||
async function paymentIntentAmountCapturableEventHandler({
|
||||
order,
|
||||
cartId,
|
||||
container,
|
||||
transactionManager,
|
||||
}) {
|
||||
if (!order) {
|
||||
const cartService = container.resolve("cartService")
|
||||
const orderService = container.resolve("orderService")
|
||||
|
||||
const cartServiceTx = cartService.withTransaction(transactionManager)
|
||||
await cartServiceTx.setPaymentSession(cartId, "stripe")
|
||||
await cartServiceTx.authorizePayment(cartId)
|
||||
await orderService
|
||||
.withTransaction(transactionManager)
|
||||
.createFromCart(cartId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { asValue, createContainer } from "awilix"
|
||||
import {
|
||||
existingCartId,
|
||||
existingCartIdWithCapturedStatus,
|
||||
existingResourceId,
|
||||
existingResourceNotCapturedId,
|
||||
nonExistingCartId,
|
||||
orderIdForExistingCartId,
|
||||
paymentId,
|
||||
paymentIntentId,
|
||||
throwingCartId,
|
||||
} from "./data"
|
||||
|
||||
export const container = createContainer()
|
||||
container.register(
|
||||
"logger",
|
||||
asValue({
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"manager",
|
||||
asValue({
|
||||
transaction: function (cb) {
|
||||
return cb(this)
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"idempotencyKeyService",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockReturnValue(undefined),
|
||||
create: jest.fn().mockReturnValue({}),
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"cartCompletionStrategy",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
complete: jest.fn(),
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"cartService",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockReturnValue({ context: {} }),
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"orderService",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieveByCartId: jest.fn().mockImplementation(async (cartId) => {
|
||||
if (cartId === existingCartId) {
|
||||
return {
|
||||
id: orderIdForExistingCartId,
|
||||
payment_status: "pending",
|
||||
}
|
||||
}
|
||||
|
||||
if (cartId === existingCartIdWithCapturedStatus) {
|
||||
return {
|
||||
id: "order-1",
|
||||
payment_status: "captured",
|
||||
}
|
||||
}
|
||||
|
||||
if (cartId === throwingCartId) {
|
||||
throw new Error("Error")
|
||||
}
|
||||
|
||||
if (cartId === nonExistingCartId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {}
|
||||
}),
|
||||
capturePayment: jest.fn(),
|
||||
})
|
||||
)
|
||||
|
||||
container.register(
|
||||
"paymentCollectionService",
|
||||
asValue({
|
||||
withTransaction: function () {
|
||||
return this
|
||||
},
|
||||
retrieve: jest.fn().mockImplementation(async (resourceId) => {
|
||||
if (resourceId === existingResourceId) {
|
||||
return {
|
||||
id: existingResourceId,
|
||||
payments: [
|
||||
{
|
||||
id: paymentId,
|
||||
data: {
|
||||
id: paymentIntentId,
|
||||
},
|
||||
captured_at: "date",
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceId === existingResourceNotCapturedId) {
|
||||
return {
|
||||
id: existingResourceNotCapturedId,
|
||||
payments: [
|
||||
{
|
||||
id: paymentId,
|
||||
data: {
|
||||
id: paymentIntentId,
|
||||
},
|
||||
captured_at: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
}),
|
||||
capture: jest.fn(),
|
||||
})
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
export const existingCartId = "existingCartId"
|
||||
export const existingCartIdWithCapturedStatus =
|
||||
"existingCartIdWithCapturedStatus"
|
||||
export const nonExistingCartId = "nonExistingCartId"
|
||||
export const throwingCartId = "throwingCartId"
|
||||
|
||||
export const existingResourceId = "paycol_existing"
|
||||
export const existingResourceNotCapturedId = "paycol_existing_not_aptured"
|
||||
|
||||
export const orderIdForExistingCartId = "order-1"
|
||||
|
||||
export const paymentIntentId = "paymentIntentId"
|
||||
|
||||
export const paymentId = "paymentId"
|
||||
@@ -0,0 +1,351 @@
|
||||
import { PostgresError } from "@medusajs/medusa"
|
||||
import Stripe from "stripe"
|
||||
import { EOL } from "os"
|
||||
|
||||
import { buildError, handlePaymentHook, isPaymentCollection } from "../utils"
|
||||
import { container } from "../__fixtures__/container"
|
||||
import {
|
||||
existingCartId,
|
||||
existingCartIdWithCapturedStatus,
|
||||
existingResourceId,
|
||||
existingResourceNotCapturedId,
|
||||
nonExistingCartId,
|
||||
orderIdForExistingCartId,
|
||||
paymentId,
|
||||
paymentIntentId,
|
||||
} from "../__fixtures__/data"
|
||||
|
||||
describe("Utils", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("isPaymentCollection", () => {
|
||||
it("should return return true if starts with paycol otherwise return false", () => {
|
||||
let result = isPaymentCollection("paycol_test")
|
||||
expect(result).toBeTruthy()
|
||||
|
||||
result = isPaymentCollection("nopaycol_test")
|
||||
expect(result).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildError", () => {
|
||||
it("should return the appropriate error message", () => {
|
||||
let event = "test_event"
|
||||
let error = {
|
||||
code: PostgresError.SERIALIZATION_FAILURE,
|
||||
detail: "some details",
|
||||
} as Stripe.StripeRawError
|
||||
|
||||
let message = buildError(event, error)
|
||||
expect(message).toBe(
|
||||
`Stripe webhook ${event} handle failed. This can happen when this webhook is triggered during a cart completion and can be ignored. This event should be retried automatically.${EOL}${error.detail}`
|
||||
)
|
||||
|
||||
event = "test_event"
|
||||
error = {
|
||||
code: "409",
|
||||
detail: "some details",
|
||||
} as Stripe.StripeRawError
|
||||
|
||||
message = buildError(event, error)
|
||||
expect(message).toBe(
|
||||
`Stripe webhook ${event} handle failed.${EOL}${error.detail}`
|
||||
)
|
||||
|
||||
event = "test_event"
|
||||
error = {
|
||||
code: "",
|
||||
detail: "some details",
|
||||
} as Stripe.StripeRawError
|
||||
|
||||
message = buildError(event, error)
|
||||
expect(message).toBe(
|
||||
`Stripe webhook ${event} handling failed${EOL}${error.detail}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("handlePaymentHook", () => {
|
||||
describe("on event type payment_intent.succeeded", () => {
|
||||
describe("in a payment context", () => {
|
||||
it("should complete the cart on non existing order", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: nonExistingCartId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
const cartCompletionStrategy = container.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
const idempotencyKeyService = container.resolve(
|
||||
"idempotencyKeyService"
|
||||
)
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(idempotencyKeyService.retrieve).toHaveBeenCalled()
|
||||
expect(idempotencyKeyService.retrieve).toHaveBeenCalledWith({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: event.id,
|
||||
})
|
||||
|
||||
expect(idempotencyKeyService.create).toHaveBeenCalled()
|
||||
expect(idempotencyKeyService.create).toHaveBeenCalledWith({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: event.id,
|
||||
})
|
||||
|
||||
expect(cartService.retrieve).toHaveBeenCalled()
|
||||
expect(cartService.retrieve).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id,
|
||||
{ select: ["context"] }
|
||||
)
|
||||
|
||||
expect(cartCompletionStrategy.complete).toHaveBeenCalled()
|
||||
expect(cartCompletionStrategy.complete).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id,
|
||||
{},
|
||||
{ id: undefined }
|
||||
)
|
||||
})
|
||||
|
||||
it("should not try to complete the cart on existing order", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: existingCartId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
const cartCompletionStrategy = container.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
const idempotencyKeyService = container.resolve(
|
||||
"idempotencyKeyService"
|
||||
)
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(idempotencyKeyService.retrieve).not.toHaveBeenCalled()
|
||||
|
||||
expect(idempotencyKeyService.create).not.toHaveBeenCalled()
|
||||
|
||||
expect(cartService.retrieve).not.toHaveBeenCalled()
|
||||
|
||||
expect(cartCompletionStrategy.complete).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should capture the payment if not already captured", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: existingCartId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(orderService.capturePayment).toHaveBeenCalled()
|
||||
expect(orderService.capturePayment).toHaveBeenCalledWith(
|
||||
orderIdForExistingCartId
|
||||
)
|
||||
})
|
||||
|
||||
it("should not capture the payment if already captured", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: existingCartIdWithCapturedStatus },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(orderService.capturePayment).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("in a payment collection context", () => {
|
||||
it("should capture the payment collection if not already captured", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { resource_id: existingResourceNotCapturedId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const paymentCollectionService = container.resolve(
|
||||
"paymentCollectionService"
|
||||
)
|
||||
|
||||
expect(paymentCollectionService.retrieve).toHaveBeenCalled()
|
||||
expect(paymentCollectionService.retrieve).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.resource_id,
|
||||
{ relations: ["payments"] }
|
||||
)
|
||||
|
||||
expect(paymentCollectionService.capture).toHaveBeenCalled()
|
||||
expect(paymentCollectionService.capture).toHaveBeenCalledWith(
|
||||
paymentId
|
||||
)
|
||||
})
|
||||
|
||||
it("should not capture the payment collection if already captured", async () => {
|
||||
const event = { id: "event", type: "payment_intent.succeeded" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { resource_id: existingResourceId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const paymentCollectionService = container.resolve(
|
||||
"paymentCollectionService"
|
||||
)
|
||||
|
||||
expect(paymentCollectionService.retrieve).toHaveBeenCalled()
|
||||
expect(paymentCollectionService.retrieve).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.resource_id,
|
||||
{ relations: ["payments"] }
|
||||
)
|
||||
|
||||
expect(paymentCollectionService.capture).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("on event type payment_intent.amount_capturable_updated", () => {
|
||||
it("should complete the cart on non existing order", async () => {
|
||||
const event = {
|
||||
id: "event",
|
||||
type: "payment_intent.amount_capturable_updated",
|
||||
}
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: nonExistingCartId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
const cartCompletionStrategy = container.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
const idempotencyKeyService = container.resolve("idempotencyKeyService")
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(idempotencyKeyService.retrieve).toHaveBeenCalled()
|
||||
expect(idempotencyKeyService.retrieve).toHaveBeenCalledWith({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: event.id,
|
||||
})
|
||||
|
||||
expect(idempotencyKeyService.create).toHaveBeenCalled()
|
||||
expect(idempotencyKeyService.create).toHaveBeenCalledWith({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: event.id,
|
||||
})
|
||||
|
||||
expect(cartService.retrieve).toHaveBeenCalled()
|
||||
expect(cartService.retrieve).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id,
|
||||
{ select: ["context"] }
|
||||
)
|
||||
|
||||
expect(cartCompletionStrategy.complete).toHaveBeenCalled()
|
||||
expect(cartCompletionStrategy.complete).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id,
|
||||
{},
|
||||
{ id: undefined }
|
||||
)
|
||||
})
|
||||
|
||||
it("should not try to complete the cart on existing order", async () => {
|
||||
const event = {
|
||||
id: "event",
|
||||
type: "payment_intent.amount_capturable_updated",
|
||||
}
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: existingCartId },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const orderService = container.resolve("orderService")
|
||||
const cartCompletionStrategy = container.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
const idempotencyKeyService = container.resolve("idempotencyKeyService")
|
||||
const cartService = container.resolve("cartService")
|
||||
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalled()
|
||||
expect(orderService.retrieveByCartId).toHaveBeenCalledWith(
|
||||
paymentIntent.metadata.cart_id
|
||||
)
|
||||
|
||||
expect(idempotencyKeyService.retrieve).not.toHaveBeenCalled()
|
||||
|
||||
expect(idempotencyKeyService.create).not.toHaveBeenCalled()
|
||||
|
||||
expect(cartService.retrieve).not.toHaveBeenCalled()
|
||||
|
||||
expect(cartCompletionStrategy.complete).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("on event type payment_intent.payment_failed", () => {
|
||||
it("should log the error", async () => {
|
||||
const event = { id: "event", type: "payment_intent.payment_failed" }
|
||||
const paymentIntent = {
|
||||
id: paymentIntentId,
|
||||
metadata: { cart_id: nonExistingCartId },
|
||||
last_payment_error: { message: "error message" },
|
||||
}
|
||||
|
||||
await handlePaymentHook({ event, container, paymentIntent })
|
||||
|
||||
const logger = container.resolve("logger")
|
||||
|
||||
expect(logger.error).toHaveBeenCalled()
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
`The payment of the payment intent ${paymentIntent.id} has failed${EOL}${paymentIntent.last_payment_error.message}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
259
packages/medusa-payment-stripe/src/api/utils/utils.ts
Normal file
259
packages/medusa-payment-stripe/src/api/utils/utils.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import { AwilixContainer } from "awilix"
|
||||
import Stripe from "stripe"
|
||||
import {
|
||||
AbstractCartCompletionStrategy,
|
||||
CartService,
|
||||
IdempotencyKeyService,
|
||||
PostgresError,
|
||||
} from "@medusajs/medusa"
|
||||
import { EOL } from "os"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
const PAYMENT_PROVIDER_KEY = "pp_stripe"
|
||||
|
||||
export function constructWebhook({
|
||||
signature,
|
||||
body,
|
||||
container,
|
||||
}: {
|
||||
signature: string | string[] | undefined
|
||||
body: any
|
||||
container: AwilixContainer
|
||||
}): Stripe.Event {
|
||||
const stripeProviderService = container.resolve(PAYMENT_PROVIDER_KEY)
|
||||
return stripeProviderService.constructWebhookEvent(body, signature)
|
||||
}
|
||||
|
||||
export function isPaymentCollection(id) {
|
||||
return id && id.startsWith("paycol")
|
||||
}
|
||||
|
||||
export function buildError(event: string, err: Stripe.StripeRawError): string {
|
||||
let message = `Stripe webhook ${event} handling failed${EOL}${
|
||||
err?.detail ?? err?.message
|
||||
}`
|
||||
if (err?.code === PostgresError.SERIALIZATION_FAILURE) {
|
||||
message = `Stripe webhook ${event} handle failed. This can happen when this webhook is triggered during a cart completion and can be ignored. This event should be retried automatically.${EOL}${
|
||||
err?.detail ?? err?.message
|
||||
}`
|
||||
}
|
||||
if (err?.code === "409") {
|
||||
message = `Stripe webhook ${event} handle failed.${EOL}${
|
||||
err?.detail ?? err?.message
|
||||
}`
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
export async function handlePaymentHook({
|
||||
event,
|
||||
container,
|
||||
paymentIntent,
|
||||
}: {
|
||||
event: { type: string; id: string }
|
||||
container: AwilixContainer
|
||||
paymentIntent: {
|
||||
id: string
|
||||
metadata: { cart_id?: string; resource_id?: string }
|
||||
last_payment_error?: { message: string }
|
||||
}
|
||||
}): Promise<{ statusCode: number }> {
|
||||
const logger = container.resolve("logger")
|
||||
|
||||
const cartId =
|
||||
paymentIntent.metadata.cart_id ?? paymentIntent.metadata.resource_id // Backward compatibility
|
||||
const resourceId = paymentIntent.metadata.resource_id
|
||||
|
||||
switch (event.type) {
|
||||
case "payment_intent.succeeded":
|
||||
try {
|
||||
await onPaymentIntentSucceeded({
|
||||
eventId: event.id,
|
||||
paymentIntent,
|
||||
cartId,
|
||||
resourceId,
|
||||
isPaymentCollection: isPaymentCollection(resourceId),
|
||||
container,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = buildError(event.type, err)
|
||||
logger.warn(message)
|
||||
return { statusCode: 409 }
|
||||
}
|
||||
|
||||
break
|
||||
case "payment_intent.amount_capturable_updated":
|
||||
try {
|
||||
await onPaymentAmountCapturableUpdate({
|
||||
eventId: event.id,
|
||||
cartId,
|
||||
container,
|
||||
})
|
||||
} catch (err) {
|
||||
const message = buildError(event.type, err)
|
||||
logger.warn(message)
|
||||
return { statusCode: 409 }
|
||||
}
|
||||
|
||||
break
|
||||
case "payment_intent.payment_failed": {
|
||||
const message =
|
||||
paymentIntent.last_payment_error &&
|
||||
paymentIntent.last_payment_error.message
|
||||
logger.error(
|
||||
`The payment of the payment intent ${paymentIntent.id} has failed${EOL}${message}`
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
return { statusCode: 204 }
|
||||
}
|
||||
|
||||
return { statusCode: 200 }
|
||||
}
|
||||
|
||||
async function onPaymentIntentSucceeded({
|
||||
eventId,
|
||||
paymentIntent,
|
||||
cartId,
|
||||
resourceId,
|
||||
isPaymentCollection,
|
||||
container,
|
||||
}) {
|
||||
const manager = container.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
if (isPaymentCollection) {
|
||||
await capturePaymenCollectiontIfNecessary({
|
||||
paymentIntent,
|
||||
resourceId,
|
||||
container,
|
||||
})
|
||||
} else {
|
||||
await completeCartIfNecessary({
|
||||
eventId,
|
||||
cartId,
|
||||
container,
|
||||
transactionManager,
|
||||
})
|
||||
|
||||
await capturePaymentIfNecessary({
|
||||
cartId,
|
||||
transactionManager,
|
||||
container,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onPaymentAmountCapturableUpdate({ eventId, cartId, container }) {
|
||||
const manager = container.resolve("manager")
|
||||
|
||||
await manager.transaction(async (transactionManager) => {
|
||||
await completeCartIfNecessary({
|
||||
eventId,
|
||||
cartId,
|
||||
container,
|
||||
transactionManager,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function capturePaymenCollectiontIfNecessary({
|
||||
paymentIntent,
|
||||
resourceId,
|
||||
container,
|
||||
}) {
|
||||
const manager = container.resolve("manager")
|
||||
const paymentCollectionService = container.resolve("paymentCollectionService")
|
||||
|
||||
const paycol = await paymentCollectionService
|
||||
.retrieve(resourceId, { relations: ["payments"] })
|
||||
.catch(() => undefined)
|
||||
|
||||
if (paycol?.payments?.length) {
|
||||
const payment = paycol.payments.find(
|
||||
(pay) => pay.data.id === paymentIntent.id
|
||||
)
|
||||
|
||||
if (payment && !payment.captured_at) {
|
||||
await manager.transaction(async (manager) => {
|
||||
await paymentCollectionService
|
||||
.withTransaction(manager)
|
||||
.capture(payment.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function capturePaymentIfNecessary({
|
||||
cartId,
|
||||
transactionManager,
|
||||
container,
|
||||
}) {
|
||||
const orderService = container.resolve("orderService")
|
||||
const order = await orderService
|
||||
.retrieveByCartId(cartId)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (order?.payment_status !== "captured") {
|
||||
await orderService
|
||||
.withTransaction(transactionManager)
|
||||
.capturePayment(order.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function completeCartIfNecessary({
|
||||
eventId,
|
||||
cartId,
|
||||
container,
|
||||
transactionManager,
|
||||
}) {
|
||||
const orderService = container.resolve("orderService")
|
||||
const order = await orderService
|
||||
.retrieveByCartId(cartId)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!order) {
|
||||
const completionStrat: AbstractCartCompletionStrategy = container.resolve(
|
||||
"cartCompletionStrategy"
|
||||
)
|
||||
const cartService: CartService = container.resolve("cartService")
|
||||
const idempotencyKeyService: IdempotencyKeyService = container.resolve(
|
||||
"idempotencyKeyService"
|
||||
)
|
||||
|
||||
const idempotencyKeyServiceTx =
|
||||
idempotencyKeyService.withTransaction(transactionManager)
|
||||
let idempotencyKey = await idempotencyKeyServiceTx.retrieve({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: eventId,
|
||||
})
|
||||
|
||||
if (!idempotencyKey) {
|
||||
idempotencyKey = await idempotencyKeyService
|
||||
.withTransaction(transactionManager)
|
||||
.create({
|
||||
request_path: "/stripe/hooks",
|
||||
idempotency_key: eventId,
|
||||
})
|
||||
}
|
||||
|
||||
const cart = await cartService
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(cartId, { select: ["context"] })
|
||||
|
||||
const { response_code, response_body } = await completionStrat
|
||||
.withTransaction(transactionManager)
|
||||
.complete(cartId, idempotencyKey, { ip: cart.context?.ip as string })
|
||||
|
||||
if (response_code !== 200) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.UNEXPECTED_STATE,
|
||||
response_body["message"],
|
||||
response_body["code"].toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
214
packages/medusa-payment-stripe/src/core/__fixtures__/data.ts
Normal file
214
packages/medusa-payment-stripe/src/core/__fixtures__/data.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
EXISTING_CUSTOMER_EMAIL,
|
||||
FAIL_INTENT_ID,
|
||||
PARTIALLY_FAIL_INTENT_ID,
|
||||
WRONG_CUSTOMER_EMAIL,
|
||||
} from "../../__mocks__/stripe"
|
||||
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
|
||||
|
||||
// INITIATE PAYMENT DATA
|
||||
|
||||
export const initiatePaymentContextWithExistingCustomer = {
|
||||
email: EXISTING_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {},
|
||||
context: {},
|
||||
paymentSessionData: {},
|
||||
}
|
||||
|
||||
export const initiatePaymentContextWithExistingCustomerStripeId = {
|
||||
email: EXISTING_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {
|
||||
metadata: {
|
||||
stripe_id: "test",
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
paymentSessionData: {},
|
||||
}
|
||||
|
||||
export const initiatePaymentContextWithWrongEmail = {
|
||||
email: WRONG_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {},
|
||||
context: {},
|
||||
paymentSessionData: {},
|
||||
}
|
||||
|
||||
export const initiatePaymentContextWithFailIntentCreation = {
|
||||
email: EXISTING_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {},
|
||||
context: {
|
||||
payment_description: "fail",
|
||||
},
|
||||
paymentSessionData: {},
|
||||
}
|
||||
|
||||
// AUTHORIZE PAYMENT DATA
|
||||
|
||||
export const authorizePaymentSuccessData = {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
}
|
||||
|
||||
// CANCEL PAYMENT DATA
|
||||
|
||||
export const cancelPaymentSuccessData = {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
}
|
||||
|
||||
export const cancelPaymentFailData = {
|
||||
id: FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
export const cancelPaymentPartiallyFailData = {
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
// CAPTURE PAYMENT DATA
|
||||
|
||||
export const capturePaymentContextSuccessData = {
|
||||
paymentSessionData: {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
},
|
||||
}
|
||||
|
||||
export const capturePaymentContextFailData = {
|
||||
paymentSessionData: {
|
||||
id: FAIL_INTENT_ID,
|
||||
},
|
||||
}
|
||||
|
||||
export const capturePaymentContextPartiallyFailData = {
|
||||
paymentSessionData: {
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
},
|
||||
}
|
||||
|
||||
// DELETE PAYMENT DATA
|
||||
|
||||
export const deletePaymentSuccessData = {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
}
|
||||
|
||||
export const deletePaymentFailData = {
|
||||
id: FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
export const deletePaymentPartiallyFailData = {
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
// REFUND PAYMENT DATA
|
||||
|
||||
export const refundPaymentSuccessData = {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
}
|
||||
|
||||
export const refundPaymentFailData = {
|
||||
id: FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
// RETRIEVE PAYMENT DATA
|
||||
|
||||
export const retrievePaymentSuccessData = {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
}
|
||||
|
||||
export const retrievePaymentFailData = {
|
||||
id: FAIL_INTENT_ID,
|
||||
}
|
||||
|
||||
// UPDATE PAYMENT DATA
|
||||
|
||||
export const updatePaymentContextWithExistingCustomer = {
|
||||
email: EXISTING_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {},
|
||||
context: {},
|
||||
paymentSessionData: {
|
||||
customer: "test",
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
export const updatePaymentContextWithExistingCustomerStripeId = {
|
||||
email: EXISTING_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {
|
||||
metadata: {
|
||||
stripe_id: "test",
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
paymentSessionData: {
|
||||
customer: "test",
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
export const updatePaymentContextWithWrongEmail = {
|
||||
email: WRONG_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
resource_id: "test",
|
||||
customer: {},
|
||||
context: {},
|
||||
paymentSessionData: {
|
||||
customer: "test",
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
export const updatePaymentContextWithDifferentAmount = {
|
||||
email: WRONG_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 2000,
|
||||
resource_id: "test",
|
||||
customer: {
|
||||
metadata: {
|
||||
stripe_id: "test",
|
||||
},
|
||||
},
|
||||
context: {},
|
||||
paymentSessionData: {
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
customer: "test",
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
export const updatePaymentContextFailWithDifferentAmount = {
|
||||
email: WRONG_CUSTOMER_EMAIL,
|
||||
currency_code: "usd",
|
||||
amount: 2000,
|
||||
resource_id: "test",
|
||||
customer: {
|
||||
metadata: {
|
||||
stripe_id: "test",
|
||||
},
|
||||
},
|
||||
context: {
|
||||
metadata: {
|
||||
stripe_id: "test",
|
||||
},
|
||||
},
|
||||
paymentSessionData: {
|
||||
id: FAIL_INTENT_ID,
|
||||
customer: "test",
|
||||
amount: 1000,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import StripeBase from "../stripe-base"
|
||||
import { PaymentIntentOptions } from "../../types"
|
||||
|
||||
export class StripeTest extends StripeBase {
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
import { EOL } from "os"
|
||||
import { StripeTest } from "../__fixtures__/stripe-test"
|
||||
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
|
||||
import { PaymentSessionStatus } from "@medusajs/medusa"
|
||||
import {
|
||||
authorizePaymentSuccessData,
|
||||
cancelPaymentFailData,
|
||||
cancelPaymentPartiallyFailData,
|
||||
cancelPaymentSuccessData,
|
||||
capturePaymentContextFailData,
|
||||
capturePaymentContextPartiallyFailData,
|
||||
capturePaymentContextSuccessData,
|
||||
deletePaymentFailData,
|
||||
deletePaymentPartiallyFailData,
|
||||
deletePaymentSuccessData,
|
||||
initiatePaymentContextWithExistingCustomer,
|
||||
initiatePaymentContextWithExistingCustomerStripeId,
|
||||
initiatePaymentContextWithFailIntentCreation,
|
||||
initiatePaymentContextWithWrongEmail,
|
||||
refundPaymentFailData,
|
||||
refundPaymentSuccessData,
|
||||
retrievePaymentFailData,
|
||||
retrievePaymentSuccessData,
|
||||
updatePaymentContextFailWithDifferentAmount,
|
||||
updatePaymentContextWithDifferentAmount,
|
||||
updatePaymentContextWithExistingCustomer,
|
||||
updatePaymentContextWithExistingCustomerStripeId,
|
||||
updatePaymentContextWithWrongEmail,
|
||||
} from "../__fixtures__/data"
|
||||
import {
|
||||
PARTIALLY_FAIL_INTENT_ID,
|
||||
STRIPE_ID,
|
||||
StripeMock,
|
||||
} from "../../__mocks__/stripe"
|
||||
import { ErrorIntentStatus } from "../../types"
|
||||
|
||||
const container = {}
|
||||
|
||||
describe("StripeTest", () => {
|
||||
describe("getPaymentStatus", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should return the correct status", async () => {
|
||||
let status: PaymentSessionStatus
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.REQUIRES_PAYMENT_METHOD.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.REQUIRES_CONFIRMATION.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.PROCESSING.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.REQUIRES_ACTION.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.CANCELED.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.CANCELED)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.REQUIRES_CAPTURE.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.AUTHORIZED)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.AUTHORIZED)
|
||||
|
||||
status = await stripeTest.getPaymentStatus({
|
||||
id: "unknown-id",
|
||||
})
|
||||
expect(status).toBe(PaymentSessionStatus.PENDING)
|
||||
})
|
||||
})
|
||||
|
||||
describe("initiatePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed with an existing customer but no stripe id", async () => {
|
||||
const result = await stripeTest.initiatePayment(
|
||||
initiatePaymentContextWithExistingCustomer
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).toHaveBeenCalled()
|
||||
expect(StripeMock.customers.create).toHaveBeenCalledWith({
|
||||
email: initiatePaymentContextWithExistingCustomer.email,
|
||||
})
|
||||
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: undefined,
|
||||
amount: initiatePaymentContextWithExistingCustomer.amount,
|
||||
currency: initiatePaymentContextWithExistingCustomer.currency_code,
|
||||
metadata: {
|
||||
resource_id: initiatePaymentContextWithExistingCustomer.resource_id,
|
||||
},
|
||||
capture_method: "manual",
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
session_data: expect.any(Object),
|
||||
update_requests: {
|
||||
customer_metadata: {
|
||||
stripe_id: STRIPE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should succeed with an existing customer with an existing stripe id", async () => {
|
||||
const result = await stripeTest.initiatePayment(
|
||||
initiatePaymentContextWithExistingCustomerStripeId
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).not.toHaveBeenCalled()
|
||||
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: undefined,
|
||||
amount: initiatePaymentContextWithExistingCustomer.amount,
|
||||
currency: initiatePaymentContextWithExistingCustomer.currency_code,
|
||||
metadata: {
|
||||
resource_id: initiatePaymentContextWithExistingCustomer.resource_id,
|
||||
},
|
||||
capture_method: "manual",
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
session_data: expect.any(Object),
|
||||
update_requests: undefined,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail on customer creation", async () => {
|
||||
const result = await stripeTest.initiatePayment(
|
||||
initiatePaymentContextWithWrongEmail
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).toHaveBeenCalled()
|
||||
expect(StripeMock.customers.create).toHaveBeenCalledWith({
|
||||
email: initiatePaymentContextWithWrongEmail.email,
|
||||
})
|
||||
|
||||
expect(StripeMock.paymentIntents.create).not.toHaveBeenCalled()
|
||||
|
||||
expect(result).toEqual({
|
||||
error:
|
||||
"An error occurred in initiatePayment when creating a Stripe customer",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on payment intents creation", async () => {
|
||||
const result = await stripeTest.initiatePayment(
|
||||
initiatePaymentContextWithFailIntentCreation
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).toHaveBeenCalled()
|
||||
expect(StripeMock.customers.create).toHaveBeenCalledWith({
|
||||
email: initiatePaymentContextWithFailIntentCreation.email,
|
||||
})
|
||||
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description:
|
||||
initiatePaymentContextWithFailIntentCreation.context
|
||||
.payment_description,
|
||||
amount: initiatePaymentContextWithFailIntentCreation.amount,
|
||||
currency: initiatePaymentContextWithFailIntentCreation.currency_code,
|
||||
metadata: {
|
||||
resource_id:
|
||||
initiatePaymentContextWithFailIntentCreation.resource_id,
|
||||
},
|
||||
capture_method: "manual",
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
error:
|
||||
"An error occurred in InitiatePayment during the creation of the stripe payment intent",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("authorizePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.authorizePayment(
|
||||
authorizePaymentSuccessData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
data: authorizePaymentSuccessData,
|
||||
status: PaymentSessionStatus.AUTHORIZED,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelPayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.cancelPayment(cancelPaymentSuccessData)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent cancellation but still return the intent", async () => {
|
||||
const result = await stripeTest.cancelPayment(
|
||||
cancelPaymentPartiallyFailData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
status: ErrorIntentStatus.CANCELED,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent cancellation", async () => {
|
||||
const result = await stripeTest.cancelPayment(cancelPaymentFailData)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in cancelPayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("capturePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.capturePayment(
|
||||
capturePaymentContextSuccessData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent capture but still return the intent", async () => {
|
||||
const result = await stripeTest.capturePayment(
|
||||
capturePaymentContextPartiallyFailData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
status: ErrorIntentStatus.SUCCEEDED,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent capture", async () => {
|
||||
const result = await stripeTest.capturePayment(
|
||||
capturePaymentContextFailData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in deletePayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("deletePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.cancelPayment(deletePaymentSuccessData)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent cancellation but still return the intent", async () => {
|
||||
const result = await stripeTest.cancelPayment(
|
||||
deletePaymentPartiallyFailData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PARTIALLY_FAIL_INTENT_ID,
|
||||
status: ErrorIntentStatus.CANCELED,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on intent cancellation", async () => {
|
||||
const result = await stripeTest.cancelPayment(deletePaymentFailData)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in cancelPayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("refundPayment", function () {
|
||||
let stripeTest
|
||||
const refundAmount = 500
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.refundPayment(
|
||||
refundPaymentSuccessData,
|
||||
refundAmount
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on refund creation", async () => {
|
||||
const result = await stripeTest.refundPayment(
|
||||
refundPaymentFailData,
|
||||
refundAmount
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in refundPayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrievePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed", async () => {
|
||||
const result = await stripeTest.retrievePayment(
|
||||
retrievePaymentSuccessData
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
id: PaymentIntentDataByStatus.SUCCEEDED.id,
|
||||
status: PaymentIntentDataByStatus.SUCCEEDED.status,
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail on refund creation", async () => {
|
||||
const result = await stripeTest.retrievePayment(retrievePaymentFailData)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in retrievePayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updatePayment", function () {
|
||||
let stripeTest
|
||||
|
||||
beforeAll(async () => {
|
||||
const scopedContainer = { ...container }
|
||||
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
|
||||
await stripeTest.init()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should succeed to initiate a payment with an existing customer but no stripe id", async () => {
|
||||
const result = await stripeTest.updatePayment(
|
||||
updatePaymentContextWithExistingCustomer
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).toHaveBeenCalled()
|
||||
expect(StripeMock.customers.create).toHaveBeenCalledWith({
|
||||
email: updatePaymentContextWithExistingCustomer.email,
|
||||
})
|
||||
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
description: undefined,
|
||||
amount: updatePaymentContextWithExistingCustomer.amount,
|
||||
currency: updatePaymentContextWithExistingCustomer.currency_code,
|
||||
metadata: {
|
||||
resource_id: updatePaymentContextWithExistingCustomer.resource_id,
|
||||
},
|
||||
capture_method: "manual",
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
session_data: expect.any(Object),
|
||||
update_requests: {
|
||||
customer_metadata: {
|
||||
stripe_id: STRIPE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to initiate a payment with an existing customer but no stripe id", async () => {
|
||||
const result = await stripeTest.updatePayment(
|
||||
updatePaymentContextWithWrongEmail
|
||||
)
|
||||
|
||||
expect(StripeMock.customers.create).toHaveBeenCalled()
|
||||
expect(StripeMock.customers.create).toHaveBeenCalledWith({
|
||||
email: updatePaymentContextWithWrongEmail.email,
|
||||
})
|
||||
|
||||
expect(StripeMock.paymentIntents.create).not.toHaveBeenCalled()
|
||||
|
||||
expect(result).toEqual({
|
||||
error:
|
||||
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
|
||||
code: "",
|
||||
detail:
|
||||
"An error occurred in initiatePayment when creating a Stripe customer" +
|
||||
EOL +
|
||||
"Error",
|
||||
})
|
||||
})
|
||||
|
||||
it("should succeed but no update occurs when the amount did not changed", async () => {
|
||||
const result = await stripeTest.updatePayment(
|
||||
updatePaymentContextWithExistingCustomerStripeId
|
||||
)
|
||||
|
||||
expect(StripeMock.paymentIntents.update).not.toHaveBeenCalled()
|
||||
|
||||
expect(result).not.toBeDefined()
|
||||
})
|
||||
|
||||
it("should succeed to update the intent with the new amount", async () => {
|
||||
const result = await stripeTest.updatePayment(
|
||||
updatePaymentContextWithDifferentAmount
|
||||
)
|
||||
|
||||
expect(StripeMock.paymentIntents.update).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.update).toHaveBeenCalledWith(
|
||||
updatePaymentContextWithDifferentAmount.paymentSessionData.id,
|
||||
{
|
||||
amount: updatePaymentContextWithDifferentAmount.amount,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
session_data: expect.objectContaining({
|
||||
amount: updatePaymentContextWithDifferentAmount.amount,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
it("should fail to update the intent with the new amount", async () => {
|
||||
const result = await stripeTest.updatePayment(
|
||||
updatePaymentContextFailWithDifferentAmount
|
||||
)
|
||||
|
||||
expect(StripeMock.paymentIntents.update).toHaveBeenCalled()
|
||||
expect(StripeMock.paymentIntents.update).toHaveBeenCalledWith(
|
||||
updatePaymentContextFailWithDifferentAmount.paymentSessionData.id,
|
||||
{
|
||||
amount: updatePaymentContextFailWithDifferentAmount.amount,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "An error occurred in updatePayment",
|
||||
code: "",
|
||||
detail: "Error",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
315
packages/medusa-payment-stripe/src/core/stripe-base.ts
Normal file
315
packages/medusa-payment-stripe/src/core/stripe-base.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import Stripe from "stripe"
|
||||
import { EOL } from "os"
|
||||
import {
|
||||
AbstractPaymentProcessor,
|
||||
isPaymentProcessorError,
|
||||
PaymentProcessorContext,
|
||||
PaymentProcessorError,
|
||||
PaymentProcessorSessionResponse,
|
||||
PaymentSessionStatus,
|
||||
} from "@medusajs/medusa"
|
||||
import {
|
||||
ErrorCodes,
|
||||
ErrorIntentStatus,
|
||||
PaymentIntentOptions,
|
||||
StripeOptions,
|
||||
} from "../types"
|
||||
|
||||
abstract class StripeBase extends AbstractPaymentProcessor {
|
||||
static identifier = ""
|
||||
|
||||
protected readonly options_: StripeOptions
|
||||
protected stripe_: Stripe
|
||||
|
||||
protected constructor(_, options) {
|
||||
super(_, options)
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.init()
|
||||
}
|
||||
|
||||
protected init(): void {
|
||||
this.stripe_ =
|
||||
this.stripe_ ||
|
||||
new Stripe(this.options_.api_key, {
|
||||
apiVersion: "2022-11-15",
|
||||
})
|
||||
}
|
||||
|
||||
abstract get paymentIntentOptions(): PaymentIntentOptions
|
||||
|
||||
getPaymentIntentOptions(): PaymentIntentOptions {
|
||||
const options: PaymentIntentOptions = {}
|
||||
|
||||
if (this?.paymentIntentOptions?.capture_method) {
|
||||
options.capture_method = this.paymentIntentOptions.capture_method
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.setup_future_usage) {
|
||||
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.payment_method_types) {
|
||||
options.payment_method_types =
|
||||
this.paymentIntentOptions.payment_method_types
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
async getPaymentStatus(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<PaymentSessionStatus> {
|
||||
const id = paymentSessionData.id as string
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async initiatePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
|
||||
const intentRequestData = this.getPaymentIntentOptions()
|
||||
const {
|
||||
email,
|
||||
context: cart_context,
|
||||
currency_code,
|
||||
amount,
|
||||
resource_id,
|
||||
customer,
|
||||
} = context
|
||||
|
||||
const description = (cart_context.payment_description ??
|
||||
this.options_?.payment_description) as string
|
||||
|
||||
const intentRequest: Stripe.PaymentIntentCreateParams = {
|
||||
description,
|
||||
amount: Math.round(amount),
|
||||
currency: currency_code,
|
||||
metadata: { resource_id },
|
||||
capture_method: this.options_.capture ? "automatic" : "manual",
|
||||
...intentRequestData,
|
||||
}
|
||||
|
||||
if (this.options_?.automatic_payment_methods) {
|
||||
intentRequest.automatic_payment_methods = { enabled: true }
|
||||
}
|
||||
|
||||
if (customer?.metadata?.stripe_id) {
|
||||
intentRequest.customer = customer.metadata.stripe_id as string
|
||||
} else {
|
||||
let stripeCustomer
|
||||
try {
|
||||
stripeCustomer = await this.stripe_.customers.create({
|
||||
email,
|
||||
})
|
||||
} catch (e) {
|
||||
return this.buildError(
|
||||
"An error occurred in initiatePayment when creating a Stripe customer",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
intentRequest.customer = stripeCustomer.id
|
||||
}
|
||||
|
||||
let session_data
|
||||
try {
|
||||
session_data = (await this.stripe_.paymentIntents.create(
|
||||
intentRequest
|
||||
)) as unknown as Record<string, unknown>
|
||||
} catch (e) {
|
||||
return this.buildError(
|
||||
"An error occurred in InitiatePayment during the creation of the stripe payment intent",
|
||||
e
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
session_data,
|
||||
update_requests: customer?.metadata?.stripe_id
|
||||
? undefined
|
||||
: {
|
||||
customer_metadata: {
|
||||
stripe_id: intentRequest.customer,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async authorizePayment(
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
context: Record<string, unknown>
|
||||
): Promise<
|
||||
| PaymentProcessorError
|
||||
| {
|
||||
status: PaymentSessionStatus
|
||||
data: PaymentProcessorSessionResponse["session_data"]
|
||||
}
|
||||
> {
|
||||
const status = await this.getPaymentStatus(paymentSessionData)
|
||||
return { data: paymentSessionData, status }
|
||||
}
|
||||
|
||||
async cancelPayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
> {
|
||||
try {
|
||||
const id = paymentSessionData.id as string
|
||||
return (await this.stripe_.paymentIntents.cancel(
|
||||
id
|
||||
)) as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||
} catch (error) {
|
||||
if (error.payment_intent?.status === ErrorIntentStatus.CANCELED) {
|
||||
return error.payment_intent
|
||||
}
|
||||
|
||||
return this.buildError("An error occurred in cancelPayment", error)
|
||||
}
|
||||
}
|
||||
|
||||
async capturePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
> {
|
||||
const id = context.paymentSessionData.id as string
|
||||
try {
|
||||
const intent = await this.stripe_.paymentIntents.capture(id)
|
||||
return intent as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||
} catch (error) {
|
||||
if (error.code === ErrorCodes.PAYMENT_INTENT_UNEXPECTED_STATE) {
|
||||
if (error.payment_intent?.status === ErrorIntentStatus.SUCCEEDED) {
|
||||
return error.payment_intent
|
||||
}
|
||||
}
|
||||
|
||||
return this.buildError("An error occurred in deletePayment", error)
|
||||
}
|
||||
}
|
||||
|
||||
async deletePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
> {
|
||||
return await this.cancelPayment(paymentSessionData)
|
||||
}
|
||||
|
||||
async refundPayment(
|
||||
paymentSessionData: Record<string, unknown>,
|
||||
refundAmount: number
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
> {
|
||||
const id = paymentSessionData.id as string
|
||||
|
||||
try {
|
||||
await this.stripe_.refunds.create({
|
||||
amount: Math.round(refundAmount),
|
||||
payment_intent: id as string,
|
||||
})
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in refundPayment", e)
|
||||
}
|
||||
|
||||
return paymentSessionData
|
||||
}
|
||||
|
||||
async retrievePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
|
||||
> {
|
||||
try {
|
||||
const id = paymentSessionData.id as string
|
||||
const intent = await this.stripe_.paymentIntents.retrieve(id)
|
||||
return intent as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in retrievePayment", e)
|
||||
}
|
||||
}
|
||||
|
||||
async updatePayment(
|
||||
context: PaymentProcessorContext
|
||||
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse | void> {
|
||||
const { amount, customer, paymentSessionData } = context
|
||||
const stripeId = customer?.metadata?.stripe_id
|
||||
|
||||
if (stripeId !== paymentSessionData.customer) {
|
||||
const result = await this.initiatePayment(context)
|
||||
if (isPaymentProcessorError(result)) {
|
||||
return this.buildError(
|
||||
"An error occurred in updatePayment during the initiate of the new payment for the new customer",
|
||||
result
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
} else {
|
||||
if (amount && paymentSessionData.amount === Math.round(amount)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const id = paymentSessionData.id as string
|
||||
const sessionData = (await this.stripe_.paymentIntents.update(id, {
|
||||
amount: Math.round(amount),
|
||||
})) as unknown as PaymentProcessorSessionResponse["session_data"]
|
||||
|
||||
return { session_data: sessionData }
|
||||
} catch (e) {
|
||||
return this.buildError("An error occurred in updatePayment", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
}
|
||||
|
||||
protected buildError(
|
||||
message: string,
|
||||
e: Stripe.StripeRawError | PaymentProcessorError | Error
|
||||
): PaymentProcessorError {
|
||||
return {
|
||||
error: message,
|
||||
code: "code" in e ? e.code : "",
|
||||
detail: isPaymentProcessorError(e)
|
||||
? `${e.error}${EOL}${e.detail ?? ""}`
|
||||
: "detail" in e
|
||||
? e.detail
|
||||
: e.message ?? "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeBase
|
||||
@@ -1,284 +0,0 @@
|
||||
import { carts } from "../../__mocks__/cart"
|
||||
import StripeBase from "../stripe-base";
|
||||
|
||||
const fakeContainer = {}
|
||||
|
||||
describe("StripeBase", () => {
|
||||
describe("createPayment", () => {
|
||||
let result
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test"
|
||||
}
|
||||
)
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("returns created stripe payment intent for cart with existing customer", async () => {
|
||||
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({
|
||||
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 () => {
|
||||
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({
|
||||
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 StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
payment_description: "test options description"
|
||||
})
|
||||
|
||||
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({
|
||||
session_data: {
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
description: "test options description",
|
||||
amount: 100,
|
||||
},
|
||||
update_requests: {
|
||||
customer_metadata: {
|
||||
stripe_id: "cus_lebron"
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrievePayment", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.retrievePayment({
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "pi_lebron",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updatePayment", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.updatePayment(
|
||||
{
|
||||
id: "pi_lebron",
|
||||
amount: 800,
|
||||
},
|
||||
{
|
||||
total: 1000,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns updated stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
amount: 1000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updatePaymentIntentCustomer", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.updatePaymentIntentCustomer(
|
||||
"pi_lebron",
|
||||
"cus_lebron_2"
|
||||
)
|
||||
})
|
||||
|
||||
it("returns update stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron_2",
|
||||
amount: 1000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("capturePayment", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.capturePayment({
|
||||
data: {
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
amount: 1000,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns captured stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
amount: 1000,
|
||||
status: "succeeded",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("refundPayment", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.refundPayment(
|
||||
{
|
||||
data: {
|
||||
id: "re_123",
|
||||
payment_intent: "pi_lebron",
|
||||
amount: 1000,
|
||||
status: "succeeded",
|
||||
},
|
||||
},
|
||||
1000
|
||||
)
|
||||
})
|
||||
|
||||
it("returns refunded stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "re_123",
|
||||
payment_intent: "pi_lebron",
|
||||
amount: 1000,
|
||||
status: "succeeded",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelPayment", () => {
|
||||
let result
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
const stripeBase = new StripeBase(
|
||||
fakeContainer,
|
||||
{
|
||||
api_key: "test",
|
||||
}
|
||||
)
|
||||
|
||||
result = await stripeBase.cancelPayment({
|
||||
data: {
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
status: "cancelled",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns cancelled stripe payment intent", () => {
|
||||
expect(result).toEqual({
|
||||
id: "pi_lebron",
|
||||
customer: "cus_lebron",
|
||||
status: "cancelled",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,305 +0,0 @@
|
||||
import { AbstractPaymentService } from "@medusajs/medusa"
|
||||
import Stripe from "stripe"
|
||||
import { PaymentSessionStatus } from "@medusajs/medusa/dist";
|
||||
|
||||
class StripeBase extends AbstractPaymentService {
|
||||
static identifier = null
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, 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)
|
||||
}
|
||||
|
||||
getPaymentIntentOptions() {
|
||||
const options = {}
|
||||
|
||||
if (this?.paymentIntentOptions?.capture_method) {
|
||||
options.capture_method = this.paymentIntentOptions.capture_method
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.setup_future_usage) {
|
||||
options.setup_future_usage = this.paymentIntentOptions.setup_future_usage
|
||||
}
|
||||
|
||||
if (this?.paymentIntentOptions?.payment_method_types) {
|
||||
options.payment_method_types =
|
||||
this.paymentIntentOptions.payment_method_types
|
||||
}
|
||||
|
||||
return 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
|
||||
}
|
||||
return await this.stripe_.customers.retrieve(customerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Stripe payment intent.
|
||||
* If customer is not registered in Stripe, we do so.
|
||||
* @param {Cart & PaymentContext} context - context to use to create a payment for
|
||||
* @return {Promise<PaymentSessionResponse>} Stripe payment intent
|
||||
*/
|
||||
async createPayment(context) {
|
||||
const intentRequestData = this.getPaymentIntentOptions()
|
||||
const { id: cart_id, email, context: cart_context, currency_code, amount, resource_id, customer } = context
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
if (this.options_?.automatic_payment_methods) {
|
||||
intentRequest.automatic_payment_methods = { enabled: true }
|
||||
}
|
||||
|
||||
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 ? undefined : {
|
||||
customer_metadata: {
|
||||
stripe_id: intentRequest.customer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Stripe payment intent.
|
||||
* @param {PaymentData} data - the data of the payment to retrieve
|
||||
* @return {Promise<Data>} Stripe payment intent
|
||||
*/
|
||||
async retrievePayment(data) {
|
||||
return await this.stripe_.paymentIntents.retrieve(data.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 {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)
|
||||
return { data: paymentSession.data, status: stat }
|
||||
}
|
||||
|
||||
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 & PaymentContext} context
|
||||
* @return {Promise<PaymentSessionData>} Stripe payment intent
|
||||
*/
|
||||
async updatePayment(paymentSessionData, context) {
|
||||
const { amount, customer } = context
|
||||
const stripeId = customer?.metadata?.stripe_id || undefined
|
||||
|
||||
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 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 \ Stripe customer
|
||||
* @return {object} Stripe payment intent
|
||||
*/
|
||||
async updatePaymentIntentCustomer(paymentIntentId, customerId) {
|
||||
return await this.stripe_.paymentIntents.update(paymentIntentId, {
|
||||
customer: customerId,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
await this.stripe_.refunds.create({
|
||||
amount: Math.round(amountToRefund),
|
||||
payment_intent: id,
|
||||
})
|
||||
|
||||
return payment.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeBase
|
||||
@@ -1,13 +1,14 @@
|
||||
import StripeBase from "../helpers/stripe-base"
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class BancontactProviderService extends StripeBase {
|
||||
static identifier = "stripe-bancontact"
|
||||
static identifier = PaymentProviderKeys.BAN_CONTACT
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["bancontact"],
|
||||
capture_method: "automatic",
|
||||
@@ -1,13 +1,14 @@
|
||||
import StripeBase from "../helpers/stripe-base"
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class BlikProviderService extends StripeBase {
|
||||
static identifier = "stripe-blik"
|
||||
static identifier = PaymentProviderKeys.BLIK
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["blik"],
|
||||
capture_method: "automatic",
|
||||
@@ -1,13 +1,14 @@
|
||||
import StripeBase from "../helpers/stripe-base"
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class GiropayProviderService extends StripeBase {
|
||||
static identifier = "stripe-giropay"
|
||||
static identifier = PaymentProviderKeys.GIROPAY
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["giropay"],
|
||||
capture_method: "automatic",
|
||||
@@ -1,13 +1,14 @@
|
||||
import StripeBase from "../helpers/stripe-base"
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class IdealProviderService extends StripeBase {
|
||||
static identifier = "stripe-ideal"
|
||||
static identifier = PaymentProviderKeys.IDEAL
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["ideal"],
|
||||
capture_method: "automatic",
|
||||
@@ -1,15 +0,0 @@
|
||||
import StripeBase from "../helpers/stripe-base";
|
||||
|
||||
class StripeProviderService extends StripeBase {
|
||||
static identifier = "stripe"
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeProviderService
|
||||
@@ -0,0 +1,16 @@
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class StripeProviderService extends StripeBase {
|
||||
static identifier = PaymentProviderKeys.STRIPE
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeProviderService
|
||||
@@ -1,13 +1,14 @@
|
||||
import StripeBase from "../helpers/stripe-base"
|
||||
import StripeBase from "../core/stripe-base"
|
||||
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
|
||||
|
||||
class Przelewy24ProviderService extends StripeBase {
|
||||
static identifier = "stripe-przelewy24"
|
||||
static identifier = PaymentProviderKeys.PRZELEWY_24
|
||||
|
||||
constructor(_, options) {
|
||||
super(_, options)
|
||||
}
|
||||
|
||||
get paymentIntentOptions() {
|
||||
get paymentIntentOptions(): PaymentIntentOptions {
|
||||
return {
|
||||
payment_method_types: ["p24"],
|
||||
capture_method: "automatic",
|
||||
40
packages/medusa-payment-stripe/src/types.ts
Normal file
40
packages/medusa-payment-stripe/src/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface StripeOptions {
|
||||
api_key: string
|
||||
webhook_secret: string
|
||||
/**
|
||||
* Use this flag to capture payment immediately (default is false)
|
||||
*/
|
||||
capture?: boolean
|
||||
/**
|
||||
* set `automatic_payment_methods` to `{ enabled: true }`
|
||||
*/
|
||||
automatic_payment_methods?: boolean
|
||||
/**
|
||||
* Set a default description on the intent if the context does not provide one
|
||||
*/
|
||||
payment_description?: string
|
||||
}
|
||||
|
||||
export interface PaymentIntentOptions {
|
||||
capture_method?: "automatic" | "manual"
|
||||
setup_future_usage?: "on_session" | "off_session"
|
||||
payment_method_types?: string[]
|
||||
}
|
||||
|
||||
export const ErrorCodes = {
|
||||
PAYMENT_INTENT_UNEXPECTED_STATE: "payment_intent_unexpected_state",
|
||||
}
|
||||
|
||||
export const ErrorIntentStatus = {
|
||||
SUCCEEDED: "succeeded",
|
||||
CANCELED: "canceled",
|
||||
}
|
||||
|
||||
export const PaymentProviderKeys = {
|
||||
STRIPE: "stripe",
|
||||
BAN_CONTACT: "stripe-bancontact",
|
||||
BLIK: "stripe-blik",
|
||||
GIROPAY: "stripe-giropay",
|
||||
IDEAL: "stripe-ideal",
|
||||
PRZELEWY_24: "stripe-przelewy24",
|
||||
}
|
||||
33
packages/medusa-payment-stripe/tsconfig.json
Normal file
33
packages/medusa-payment-stripe/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"es5",
|
||||
"es6",
|
||||
"es2019"
|
||||
],
|
||||
"target": "es5",
|
||||
"outDir": "./dist",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": true,
|
||||
"noImplicitReturns": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitThis": true,
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"downlevelIteration": true // to use ES5 specific tooling
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"src/**/__tests__",
|
||||
"src/**/__mocks__",
|
||||
"src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
5
packages/medusa-payment-stripe/tsconfig.spec.json
Normal file
5
packages/medusa-payment-stripe/tsconfig.spec.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -9,12 +9,24 @@ export { getRequestedBatchJob } from "./batch-job/get-requested-batch-job"
|
||||
export { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount"
|
||||
export { transformIncludesOptions } from "./transform-includes-options"
|
||||
export { transformBody } from "./transform-body"
|
||||
export { default as authenticate } from "./authenticate"
|
||||
export { default as authenticateCustomer } from "./authenticate-customer"
|
||||
export { default as wrapHandler } from "./await-middleware"
|
||||
export { default as normalizeQuery } from "./normalized-query"
|
||||
export { default as requireCustomerAuthentication } from "./require-customer-authentication"
|
||||
export { transformQuery, transformStoreQuery } from "./transform-query"
|
||||
|
||||
/**
|
||||
* @deprecated you can now import the middlewares directly without passing by the default export
|
||||
* e.g `import { authenticate } from "@medusajs/medusa"
|
||||
*/
|
||||
export default {
|
||||
authenticate,
|
||||
authenticateCustomer,
|
||||
requireCustomerAuthentication,
|
||||
normalizeQuery,
|
||||
/**
|
||||
* @deprecated use `import { wrapHandler } from "@medusajs/medusa"`
|
||||
*/
|
||||
wrap,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./api"
|
||||
export * from "./api/middlewares"
|
||||
export * from "./interfaces"
|
||||
export * from "./models"
|
||||
export * from "./services"
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface ICartCompletionStrategy {
|
||||
}
|
||||
|
||||
export abstract class AbstractCartCompletionStrategy
|
||||
extends TransactionBaseService
|
||||
implements ICartCompletionStrategy
|
||||
{
|
||||
abstract complete(
|
||||
|
||||
@@ -33,11 +33,6 @@ export interface PaymentProcessor {
|
||||
*/
|
||||
getIdentifier(): string
|
||||
|
||||
/**
|
||||
* Used to initialise anything like an SDK or similar
|
||||
*/
|
||||
init(): Promise<void>
|
||||
|
||||
/**
|
||||
* Initiate a payment session with the external provider
|
||||
*/
|
||||
@@ -147,8 +142,6 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
|
||||
return ctr.identifier
|
||||
}
|
||||
|
||||
abstract init(): Promise<void>
|
||||
|
||||
abstract capturePayment(
|
||||
paymentSessionData: Record<string, unknown>
|
||||
): Promise<
|
||||
|
||||
@@ -205,12 +205,9 @@ async function registerPaymentProcessor({
|
||||
).filter((provider) => provider instanceof AbstractPaymentProcessor)
|
||||
|
||||
const payIds: string[] = []
|
||||
await Promise.all(
|
||||
payProviders.map((paymentProvider) => {
|
||||
payIds.push(paymentProvider.getIdentifier())
|
||||
return paymentProvider.init()
|
||||
})
|
||||
)
|
||||
payProviders.map((paymentProvider) => {
|
||||
payIds.push(paymentProvider.getIdentifier())
|
||||
})
|
||||
|
||||
const pProviderService = container.resolve<PaymentProviderService>(
|
||||
"paymentProviderService"
|
||||
|
||||
@@ -6,7 +6,6 @@ import glob from "glob"
|
||||
import _ from "lodash"
|
||||
import { createRequireFromPath } from "medusa-core-utils"
|
||||
import {
|
||||
BaseService as LegacyBaseService,
|
||||
FileService,
|
||||
FulfillmentService,
|
||||
OauthService,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
isPriceSelectionStrategy,
|
||||
isSearchService,
|
||||
isTaxCalculationStrategy,
|
||||
TransactionBaseService as BaseService,
|
||||
} from "../interfaces"
|
||||
import { MiddlewareService } from "../services"
|
||||
import {
|
||||
@@ -364,16 +362,6 @@ export async function registerServices(
|
||||
const loaded = require(fn).default
|
||||
const name = formatRegistrationName(fn)
|
||||
|
||||
if (
|
||||
!(loaded.prototype instanceof LegacyBaseService) &&
|
||||
!(loaded.prototype instanceof BaseService)
|
||||
) {
|
||||
const logger = container.resolve<Logger>("logger")
|
||||
const message = `The class must be a valid service implementation, please check ${fn}`
|
||||
logger.error(message)
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
const context = { container, pluginDetails, registrationName: name }
|
||||
|
||||
registerPaymentServiceFromClass(loaded, context)
|
||||
@@ -627,8 +615,18 @@ function resolvePlugin(pluginName: string): {
|
||||
)
|
||||
// warnOnIncompatiblePeerDependency(packageJSON.name, packageJSON)
|
||||
|
||||
const computedResolvedPath =
|
||||
resolvedPath + (process.env.DEV_MODE ? "/src" : "")
|
||||
|
||||
// Add support for a plugin to output the build into a dist directory
|
||||
const resolvedPathToDist = resolvedPath + "/dist"
|
||||
const isDistExist =
|
||||
resolvedPathToDist &&
|
||||
!process.env.DEV_MODE &&
|
||||
existsSync(resolvedPath + "/dist")
|
||||
|
||||
return {
|
||||
resolve: resolvedPath + (process.env.DEV_MODE ? "/src" : ""),
|
||||
resolve: isDistExist ? resolvedPathToDist : computedResolvedPath,
|
||||
id: createPluginId(packageJSON.name),
|
||||
name: packageJSON.name,
|
||||
options: {},
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
CreateIdempotencyKeyInput,
|
||||
IdempotencyCallbackResult,
|
||||
} from "../types/idempotency-key"
|
||||
import { Selector } from "../types/common"
|
||||
import { buildQuery, isString } from "../utils"
|
||||
|
||||
const KEY_LOCKED_TIMEOUT = 1000
|
||||
|
||||
@@ -75,14 +77,16 @@ class IdempotencyKeyService extends TransactionBaseService {
|
||||
|
||||
/**
|
||||
* Retrieves an idempotency key
|
||||
* @param idempotencyKey - key to retrieve
|
||||
* @param idempotencyKeyOrSelector - key or selector to retrieve
|
||||
* @return idempotency key
|
||||
*/
|
||||
async retrieve(idempotencyKey: string): Promise<IdempotencyKey | never> {
|
||||
if (!isDefined(idempotencyKey)) {
|
||||
async retrieve(
|
||||
idempotencyKeyOrSelector: string | Selector<IdempotencyKey>
|
||||
): Promise<IdempotencyKey | never> {
|
||||
if (!isDefined(idempotencyKeyOrSelector)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`"idempotencyKey" must be defined`
|
||||
`"idempotencyKeyOrSelector" must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -90,15 +94,34 @@ class IdempotencyKeyService extends TransactionBaseService {
|
||||
this.idempotencyKeyRepository_
|
||||
)
|
||||
|
||||
const iKey = await idempotencyKeyRepo.findOne({
|
||||
where: { idempotency_key: idempotencyKey },
|
||||
})
|
||||
const selector = isString(idempotencyKeyOrSelector)
|
||||
? { idempotency_key: idempotencyKeyOrSelector }
|
||||
: idempotencyKeyOrSelector
|
||||
const query = buildQuery(selector)
|
||||
|
||||
const iKeys = await idempotencyKeyRepo.find(query)
|
||||
|
||||
if (iKeys.length > 1) {
|
||||
throw new Error(
|
||||
`Multiple keys were found for constraints: ${JSON.stringify(
|
||||
idempotencyKeyOrSelector
|
||||
)}. There should only be one.`
|
||||
)
|
||||
}
|
||||
|
||||
const iKey = iKeys[0]
|
||||
|
||||
if (!iKey) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Idempotency key ${idempotencyKey} was not found`
|
||||
)
|
||||
let message
|
||||
if (isString(idempotencyKeyOrSelector)) {
|
||||
message = `Idempotency key ${idempotencyKeyOrSelector} was not found`
|
||||
} else {
|
||||
message = `Idempotency key with constraints ${JSON.stringify(
|
||||
idempotencyKeyOrSelector
|
||||
)} was not found`
|
||||
}
|
||||
|
||||
throw new MedusaError(MedusaError.Types.NOT_FOUND, message)
|
||||
}
|
||||
|
||||
return iKey
|
||||
|
||||
@@ -356,17 +356,16 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
|
||||
let paymentResponse
|
||||
if (provider instanceof AbstractPaymentProcessor) {
|
||||
paymentResponse =
|
||||
(await provider.updatePayment({
|
||||
amount: context.amount,
|
||||
context: context.context,
|
||||
currency_code: context.currency_code,
|
||||
customer: context.customer,
|
||||
email: context.email,
|
||||
billing_address: context.billing_address,
|
||||
resource_id: context.resource_id,
|
||||
paymentSessionData: paymentSession.data,
|
||||
})) ?? {}
|
||||
paymentResponse = await provider.updatePayment({
|
||||
amount: context.amount,
|
||||
context: context.context,
|
||||
currency_code: context.currency_code,
|
||||
customer: context.customer,
|
||||
email: context.email,
|
||||
billing_address: context.billing_address,
|
||||
resource_id: context.resource_id,
|
||||
paymentSessionData: paymentSession.data,
|
||||
})
|
||||
|
||||
if (paymentResponse && "error" in paymentResponse) {
|
||||
this.throwFromPaymentProcessorError(paymentResponse)
|
||||
@@ -377,7 +376,7 @@ export default class PaymentProviderService extends TransactionBaseService {
|
||||
.updatePayment(paymentSession.data, context)
|
||||
}
|
||||
|
||||
const sessionData = paymentResponse.session_data ?? paymentResponse
|
||||
const sessionData = paymentResponse?.session_data ?? paymentResponse
|
||||
|
||||
// If no update occurs, return the original session
|
||||
if (!sessionData) {
|
||||
|
||||
@@ -30,8 +30,6 @@ type InjectedDependencies = {
|
||||
}
|
||||
|
||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
protected manager_: EntityManager
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
@@ -47,9 +45,9 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
cartService,
|
||||
orderService,
|
||||
swapService,
|
||||
manager,
|
||||
}: InjectedDependencies) {
|
||||
super()
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
@@ -57,7 +55,6 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
this.cartService_ = cartService
|
||||
this.orderService_ = orderService
|
||||
this.swapService_ = swapService
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async complete(
|
||||
@@ -73,7 +70,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
while (inProgress) {
|
||||
switch (idempotencyKey.recovery_point) {
|
||||
case "started": {
|
||||
await this.manager_
|
||||
await this.activeManager_
|
||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||
idempotencyKey = await this.idempotencyKeyService_
|
||||
.withTransaction(transactionManager)
|
||||
@@ -90,7 +87,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
break
|
||||
}
|
||||
case "tax_lines_created": {
|
||||
await this.manager_
|
||||
await this.activeManager_
|
||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||
idempotencyKey = await this.idempotencyKeyService_
|
||||
.withTransaction(transactionManager)
|
||||
@@ -111,7 +108,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
}
|
||||
|
||||
case "payment_authorized": {
|
||||
await this.manager_
|
||||
await this.activeManager_
|
||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||
idempotencyKey = await this.idempotencyKeyService_
|
||||
.withTransaction(transactionManager)
|
||||
@@ -134,7 +131,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
}
|
||||
|
||||
default:
|
||||
await this.manager_.transaction(async (transactionManager) => {
|
||||
await this.activeManager_.transaction(async (transactionManager) => {
|
||||
idempotencyKey = await this.idempotencyKeyService_
|
||||
.withTransaction(transactionManager)
|
||||
.update(idempotencyKey.idempotency_key, {
|
||||
@@ -149,7 +146,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
|
||||
if (err) {
|
||||
if (idempotencyKey.recovery_point !== "started") {
|
||||
await this.manager_.transaction(async (transactionManager) => {
|
||||
await this.activeManager_.transaction(async (transactionManager) => {
|
||||
try {
|
||||
await this.orderService_
|
||||
.withTransaction(transactionManager)
|
||||
@@ -264,16 +261,19 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
const cartServiceTx = this.cartService_.withTransaction(manager)
|
||||
|
||||
const cart = await cartServiceTx.retrieveWithTotals(id, {
|
||||
relations: ["region", "payment", "payment_sessions", "items.variant.product",],
|
||||
relations: [
|
||||
"region",
|
||||
"payment",
|
||||
"payment_sessions",
|
||||
"items.variant.product",
|
||||
],
|
||||
})
|
||||
|
||||
let allowBackorder = false
|
||||
let swapId: string
|
||||
|
||||
if (cart.type === "swap") {
|
||||
const swap = await swapServiceTx.retrieveByCartId(id)
|
||||
allowBackorder = swap.allow_backorder
|
||||
swapId = swap.id
|
||||
}
|
||||
|
||||
if (!allowBackorder) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export type CreateIdempotencyKeyInput = {
|
||||
request_method: string
|
||||
request_params: Record<string, unknown>
|
||||
request_path: string
|
||||
request_method?: string
|
||||
request_params?: Record<string, unknown>
|
||||
request_path?: string
|
||||
idempotency_key?: string
|
||||
}
|
||||
|
||||
|
||||
45
yarn.lock
45
yarn.lock
@@ -9215,6 +9215,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stripe@npm:^8.0.417":
|
||||
version: 8.0.417
|
||||
resolution: "@types/stripe@npm:8.0.417"
|
||||
dependencies:
|
||||
stripe: "*"
|
||||
checksum: 97e3a5ad6948a94366d6749328b721a3c9004ba68795e7675efaa74e4b4c51d57c1dfc785747f4221192dabd7a13fb41778e88e080bafd5d3938a50df7f4ef98
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/superagent@npm:*":
|
||||
version: 4.1.15
|
||||
resolution: "@types/superagent@npm:4.1.15"
|
||||
@@ -25402,7 +25411,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"medusa-core-utils@^1.1.31, medusa-core-utils@^1.1.39, medusa-core-utils@workspace:packages/medusa-core-utils":
|
||||
"medusa-core-utils@^1.1.31, medusa-core-utils@^1.1.38, medusa-core-utils@^1.1.39, medusa-core-utils@workspace:packages/medusa-core-utils":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "medusa-core-utils@workspace:packages/medusa-core-utils"
|
||||
dependencies:
|
||||
@@ -25719,28 +25728,16 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "medusa-payment-stripe@workspace:packages/medusa-payment-stripe"
|
||||
dependencies:
|
||||
"@babel/cli": ^7.7.5
|
||||
"@babel/core": ^7.7.5
|
||||
"@babel/node": ^7.7.4
|
||||
"@babel/plugin-proposal-class-properties": ^7.7.4
|
||||
"@babel/plugin-proposal-optional-chaining": ^7.12.7
|
||||
"@babel/plugin-transform-classes": ^7.9.5
|
||||
"@babel/plugin-transform-instanceof": ^7.8.3
|
||||
"@babel/plugin-transform-runtime": ^7.7.6
|
||||
"@babel/preset-env": ^7.7.5
|
||||
"@babel/register": ^7.7.4
|
||||
"@babel/runtime": ^7.9.6
|
||||
"@medusajs/medusa": ^1.7.7
|
||||
"@types/stripe": ^8.0.417
|
||||
body-parser: ^1.19.0
|
||||
client-sessions: ^0.8.0
|
||||
cross-env: ^5.2.1
|
||||
express: ^4.17.1
|
||||
jest: ^25.5.4
|
||||
medusa-core-utils: ^1.1.39
|
||||
medusa-interfaces: ^1.3.6
|
||||
medusa-test-utils: ^1.1.37
|
||||
stripe: ^8.50.0
|
||||
medusa-core-utils: ^1.1.38
|
||||
stripe: ^11.10.0
|
||||
peerDependencies:
|
||||
medusa-interfaces: 1.3.6
|
||||
"@medusajs/medusa": ^1.7.7
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -30339,7 +30336,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.5.1, qs@npm:^6.6.0, qs@npm:^6.9.4":
|
||||
"qs@npm:^6.10.0, qs@npm:^6.10.3, qs@npm:^6.11.0, qs@npm:^6.5.1, qs@npm:^6.6.0, qs@npm:^6.9.4":
|
||||
version: 6.11.0
|
||||
resolution: "qs@npm:6.11.0"
|
||||
dependencies:
|
||||
@@ -33904,6 +33901,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:*, stripe@npm:^11.10.0":
|
||||
version: 11.10.0
|
||||
resolution: "stripe@npm:11.10.0"
|
||||
dependencies:
|
||||
"@types/node": ">=8.1.0"
|
||||
qs: ^6.11.0
|
||||
checksum: e50b7f671f608557a6dec74ae817c13df73ded75f586e2c5069e6acf86d5e459753695c59454226e8ede3ab7df00915f6a5becda8e7153226ea2eacbd7fb87a2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:^8.50.0":
|
||||
version: 8.222.0
|
||||
resolution: "stripe@npm:8.222.0"
|
||||
|
||||
Reference in New Issue
Block a user