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/*
|
packages/*
|
||||||
# List of packages to Lint
|
# List of packages to Lint
|
||||||
!packages/medusa
|
!packages/medusa
|
||||||
|
!packages/medusa-payment-stripe
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ module.exports = {
|
|||||||
extends: ["plugin:@typescript-eslint/recommended"],
|
extends: ["plugin:@typescript-eslint/recommended"],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./packages/medusa/tsconfig.json",
|
project: [
|
||||||
|
"./packages/medusa/tsconfig.json",
|
||||||
|
"./packages/medusa-payment-stripe/tsconfig.spec.json",
|
||||||
|
]
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"valid-jsdoc": "off",
|
"valid-jsdoc": "off",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "medusa-payment-paypal",
|
"name": "medusa-payment-paypal",
|
||||||
"version": "1.2.10",
|
"version": "1.2.10",
|
||||||
"description": "Paypal Payment provider for Meduas Commerce",
|
"description": "Paypal Payment provider for Medusa Commerce",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"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
|
node_modules
|
||||||
.DS_store
|
.DS_store
|
||||||
.env*
|
|
||||||
/*.js
|
|
||||||
!index.js
|
|
||||||
yarn.lock
|
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",
|
api_key: "STRIPE_API_KEY",
|
||||||
webhook_secret: "STRIPE_WEBHOOK_SECRET",
|
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
|
## 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.
|
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",
|
"name": "medusa-payment-stripe",
|
||||||
"version": "1.1.53",
|
"version": "1.1.53",
|
||||||
"description": "Stripe Payment provider for Meduas Commerce",
|
"description": "Stripe Payment provider for Meduas Commerce",
|
||||||
"main": "index.js",
|
"main": "dist/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/medusajs/medusa",
|
"url": "https://github.com/medusajs/medusa",
|
||||||
"directory": "packages/medusa-payment-stripe"
|
"directory": "packages/medusa-payment-stripe"
|
||||||
},
|
},
|
||||||
"author": "Sebastian Rindom",
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"author": "Medusa",
|
||||||
"license": "MIT",
|
"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": {
|
"scripts": {
|
||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"test": "jest --passWithNoTests src",
|
"test": "jest --passWithNoTests src",
|
||||||
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
|
"build": "tsc",
|
||||||
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
|
"watch": "tsc --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@medusajs/medusa": "^1.7.7",
|
||||||
|
"@types/stripe": "^8.0.417",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"jest": "^25.5.4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"medusa-interfaces": "1.3.6"
|
"@medusajs/medusa": "^1.7.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"medusa-core-utils": "^1.1.39",
|
"medusa-core-utils": "^1.1.38",
|
||||||
"stripe": "^8.50.0"
|
"stripe": "^11.10.0"
|
||||||
},
|
},
|
||||||
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
|
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
|
||||||
"keywords": [
|
"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 { Router } from "express"
|
||||||
import bodyParser from "body-parser"
|
import bodyParser from "body-parser"
|
||||||
import middlewares from "../../middlewares"
|
import { wrapHandler } from "@medusajs/medusa"
|
||||||
|
|
||||||
const route = Router()
|
const route = Router()
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ export default (app) => {
|
|||||||
"/hooks",
|
"/hooks",
|
||||||
// stripe constructEvent fails without body-parser
|
// stripe constructEvent fails without body-parser
|
||||||
bodyParser.raw({ type: "application/json" }),
|
bodyParser.raw({ type: "application/json" }),
|
||||||
middlewares.wrap(require("./stripe").default)
|
wrapHandler(stripeHooks)
|
||||||
)
|
)
|
||||||
return app
|
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 { Router } from "express"
|
||||||
import hooks from "./routes/hooks"
|
import hooks from "./hooks"
|
||||||
|
|
||||||
export default (container) => {
|
export default (container) => {
|
||||||
const app = Router()
|
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 {
|
class BancontactProviderService extends StripeBase {
|
||||||
static identifier = "stripe-bancontact"
|
static identifier = PaymentProviderKeys.BAN_CONTACT
|
||||||
|
|
||||||
constructor(_, options) {
|
constructor(_, options) {
|
||||||
super(_, options)
|
super(_, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentIntentOptions() {
|
get paymentIntentOptions(): PaymentIntentOptions {
|
||||||
return {
|
return {
|
||||||
payment_method_types: ["bancontact"],
|
payment_method_types: ["bancontact"],
|
||||||
capture_method: "automatic",
|
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 {
|
class BlikProviderService extends StripeBase {
|
||||||
static identifier = "stripe-blik"
|
static identifier = PaymentProviderKeys.BLIK
|
||||||
|
|
||||||
constructor(_, options) {
|
constructor(_, options) {
|
||||||
super(_, options)
|
super(_, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentIntentOptions() {
|
get paymentIntentOptions(): PaymentIntentOptions {
|
||||||
return {
|
return {
|
||||||
payment_method_types: ["blik"],
|
payment_method_types: ["blik"],
|
||||||
capture_method: "automatic",
|
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 {
|
class GiropayProviderService extends StripeBase {
|
||||||
static identifier = "stripe-giropay"
|
static identifier = PaymentProviderKeys.GIROPAY
|
||||||
|
|
||||||
constructor(_, options) {
|
constructor(_, options) {
|
||||||
super(_, options)
|
super(_, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentIntentOptions() {
|
get paymentIntentOptions(): PaymentIntentOptions {
|
||||||
return {
|
return {
|
||||||
payment_method_types: ["giropay"],
|
payment_method_types: ["giropay"],
|
||||||
capture_method: "automatic",
|
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 {
|
class IdealProviderService extends StripeBase {
|
||||||
static identifier = "stripe-ideal"
|
static identifier = PaymentProviderKeys.IDEAL
|
||||||
|
|
||||||
constructor(_, options) {
|
constructor(_, options) {
|
||||||
super(_, options)
|
super(_, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentIntentOptions() {
|
get paymentIntentOptions(): PaymentIntentOptions {
|
||||||
return {
|
return {
|
||||||
payment_method_types: ["ideal"],
|
payment_method_types: ["ideal"],
|
||||||
capture_method: "automatic",
|
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 {
|
class Przelewy24ProviderService extends StripeBase {
|
||||||
static identifier = "stripe-przelewy24"
|
static identifier = PaymentProviderKeys.PRZELEWY_24
|
||||||
|
|
||||||
constructor(_, options) {
|
constructor(_, options) {
|
||||||
super(_, options)
|
super(_, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
get paymentIntentOptions() {
|
get paymentIntentOptions(): PaymentIntentOptions {
|
||||||
return {
|
return {
|
||||||
payment_method_types: ["p24"],
|
payment_method_types: ["p24"],
|
||||||
capture_method: "automatic",
|
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 { doesConditionBelongToDiscount } from "./discount/does-condition-belong-to-discount"
|
||||||
export { transformIncludesOptions } from "./transform-includes-options"
|
export { transformIncludesOptions } from "./transform-includes-options"
|
||||||
export { transformBody } from "./transform-body"
|
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"
|
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 {
|
export default {
|
||||||
authenticate,
|
authenticate,
|
||||||
authenticateCustomer,
|
authenticateCustomer,
|
||||||
requireCustomerAuthentication,
|
requireCustomerAuthentication,
|
||||||
normalizeQuery,
|
normalizeQuery,
|
||||||
|
/**
|
||||||
|
* @deprecated use `import { wrapHandler } from "@medusajs/medusa"`
|
||||||
|
*/
|
||||||
wrap,
|
wrap,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./api"
|
export * from "./api"
|
||||||
|
export * from "./api/middlewares"
|
||||||
export * from "./interfaces"
|
export * from "./interfaces"
|
||||||
export * from "./models"
|
export * from "./models"
|
||||||
export * from "./services"
|
export * from "./services"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ICartCompletionStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AbstractCartCompletionStrategy
|
export abstract class AbstractCartCompletionStrategy
|
||||||
|
extends TransactionBaseService
|
||||||
implements ICartCompletionStrategy
|
implements ICartCompletionStrategy
|
||||||
{
|
{
|
||||||
abstract complete(
|
abstract complete(
|
||||||
|
|||||||
@@ -33,11 +33,6 @@ export interface PaymentProcessor {
|
|||||||
*/
|
*/
|
||||||
getIdentifier(): string
|
getIdentifier(): string
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to initialise anything like an SDK or similar
|
|
||||||
*/
|
|
||||||
init(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a payment session with the external provider
|
* Initiate a payment session with the external provider
|
||||||
*/
|
*/
|
||||||
@@ -147,8 +142,6 @@ export abstract class AbstractPaymentProcessor implements PaymentProcessor {
|
|||||||
return ctr.identifier
|
return ctr.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract init(): Promise<void>
|
|
||||||
|
|
||||||
abstract capturePayment(
|
abstract capturePayment(
|
||||||
paymentSessionData: Record<string, unknown>
|
paymentSessionData: Record<string, unknown>
|
||||||
): Promise<
|
): Promise<
|
||||||
|
|||||||
@@ -205,12 +205,9 @@ async function registerPaymentProcessor({
|
|||||||
).filter((provider) => provider instanceof AbstractPaymentProcessor)
|
).filter((provider) => provider instanceof AbstractPaymentProcessor)
|
||||||
|
|
||||||
const payIds: string[] = []
|
const payIds: string[] = []
|
||||||
await Promise.all(
|
payProviders.map((paymentProvider) => {
|
||||||
payProviders.map((paymentProvider) => {
|
payIds.push(paymentProvider.getIdentifier())
|
||||||
payIds.push(paymentProvider.getIdentifier())
|
})
|
||||||
return paymentProvider.init()
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const pProviderService = container.resolve<PaymentProviderService>(
|
const pProviderService = container.resolve<PaymentProviderService>(
|
||||||
"paymentProviderService"
|
"paymentProviderService"
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import glob from "glob"
|
|||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
import { createRequireFromPath } from "medusa-core-utils"
|
import { createRequireFromPath } from "medusa-core-utils"
|
||||||
import {
|
import {
|
||||||
BaseService as LegacyBaseService,
|
|
||||||
FileService,
|
FileService,
|
||||||
FulfillmentService,
|
FulfillmentService,
|
||||||
OauthService,
|
OauthService,
|
||||||
@@ -23,7 +22,6 @@ import {
|
|||||||
isPriceSelectionStrategy,
|
isPriceSelectionStrategy,
|
||||||
isSearchService,
|
isSearchService,
|
||||||
isTaxCalculationStrategy,
|
isTaxCalculationStrategy,
|
||||||
TransactionBaseService as BaseService,
|
|
||||||
} from "../interfaces"
|
} from "../interfaces"
|
||||||
import { MiddlewareService } from "../services"
|
import { MiddlewareService } from "../services"
|
||||||
import {
|
import {
|
||||||
@@ -364,16 +362,6 @@ export async function registerServices(
|
|||||||
const loaded = require(fn).default
|
const loaded = require(fn).default
|
||||||
const name = formatRegistrationName(fn)
|
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 }
|
const context = { container, pluginDetails, registrationName: name }
|
||||||
|
|
||||||
registerPaymentServiceFromClass(loaded, context)
|
registerPaymentServiceFromClass(loaded, context)
|
||||||
@@ -627,8 +615,18 @@ function resolvePlugin(pluginName: string): {
|
|||||||
)
|
)
|
||||||
// warnOnIncompatiblePeerDependency(packageJSON.name, packageJSON)
|
// 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 {
|
return {
|
||||||
resolve: resolvedPath + (process.env.DEV_MODE ? "/src" : ""),
|
resolve: isDistExist ? resolvedPathToDist : computedResolvedPath,
|
||||||
id: createPluginId(packageJSON.name),
|
id: createPluginId(packageJSON.name),
|
||||||
name: packageJSON.name,
|
name: packageJSON.name,
|
||||||
options: {},
|
options: {},
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
CreateIdempotencyKeyInput,
|
CreateIdempotencyKeyInput,
|
||||||
IdempotencyCallbackResult,
|
IdempotencyCallbackResult,
|
||||||
} from "../types/idempotency-key"
|
} from "../types/idempotency-key"
|
||||||
|
import { Selector } from "../types/common"
|
||||||
|
import { buildQuery, isString } from "../utils"
|
||||||
|
|
||||||
const KEY_LOCKED_TIMEOUT = 1000
|
const KEY_LOCKED_TIMEOUT = 1000
|
||||||
|
|
||||||
@@ -75,14 +77,16 @@ class IdempotencyKeyService extends TransactionBaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves an idempotency key
|
* Retrieves an idempotency key
|
||||||
* @param idempotencyKey - key to retrieve
|
* @param idempotencyKeyOrSelector - key or selector to retrieve
|
||||||
* @return idempotency key
|
* @return idempotency key
|
||||||
*/
|
*/
|
||||||
async retrieve(idempotencyKey: string): Promise<IdempotencyKey | never> {
|
async retrieve(
|
||||||
if (!isDefined(idempotencyKey)) {
|
idempotencyKeyOrSelector: string | Selector<IdempotencyKey>
|
||||||
|
): Promise<IdempotencyKey | never> {
|
||||||
|
if (!isDefined(idempotencyKeyOrSelector)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_FOUND,
|
MedusaError.Types.NOT_FOUND,
|
||||||
`"idempotencyKey" must be defined`
|
`"idempotencyKeyOrSelector" must be defined`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,15 +94,34 @@ class IdempotencyKeyService extends TransactionBaseService {
|
|||||||
this.idempotencyKeyRepository_
|
this.idempotencyKeyRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const iKey = await idempotencyKeyRepo.findOne({
|
const selector = isString(idempotencyKeyOrSelector)
|
||||||
where: { idempotency_key: idempotencyKey },
|
? { 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) {
|
if (!iKey) {
|
||||||
throw new MedusaError(
|
let message
|
||||||
MedusaError.Types.NOT_FOUND,
|
if (isString(idempotencyKeyOrSelector)) {
|
||||||
`Idempotency key ${idempotencyKey} was not found`
|
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
|
return iKey
|
||||||
|
|||||||
@@ -356,17 +356,16 @@ export default class PaymentProviderService extends TransactionBaseService {
|
|||||||
|
|
||||||
let paymentResponse
|
let paymentResponse
|
||||||
if (provider instanceof AbstractPaymentProcessor) {
|
if (provider instanceof AbstractPaymentProcessor) {
|
||||||
paymentResponse =
|
paymentResponse = await provider.updatePayment({
|
||||||
(await provider.updatePayment({
|
amount: context.amount,
|
||||||
amount: context.amount,
|
context: context.context,
|
||||||
context: context.context,
|
currency_code: context.currency_code,
|
||||||
currency_code: context.currency_code,
|
customer: context.customer,
|
||||||
customer: context.customer,
|
email: context.email,
|
||||||
email: context.email,
|
billing_address: context.billing_address,
|
||||||
billing_address: context.billing_address,
|
resource_id: context.resource_id,
|
||||||
resource_id: context.resource_id,
|
paymentSessionData: paymentSession.data,
|
||||||
paymentSessionData: paymentSession.data,
|
})
|
||||||
})) ?? {}
|
|
||||||
|
|
||||||
if (paymentResponse && "error" in paymentResponse) {
|
if (paymentResponse && "error" in paymentResponse) {
|
||||||
this.throwFromPaymentProcessorError(paymentResponse)
|
this.throwFromPaymentProcessorError(paymentResponse)
|
||||||
@@ -377,7 +376,7 @@ export default class PaymentProviderService extends TransactionBaseService {
|
|||||||
.updatePayment(paymentSession.data, context)
|
.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 no update occurs, return the original session
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ type InjectedDependencies = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||||
protected manager_: EntityManager
|
|
||||||
|
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||||
protected readonly paymentProviderService_: PaymentProviderService
|
protected readonly paymentProviderService_: PaymentProviderService
|
||||||
@@ -47,9 +45,9 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
cartService,
|
cartService,
|
||||||
orderService,
|
orderService,
|
||||||
swapService,
|
swapService,
|
||||||
manager,
|
|
||||||
}: InjectedDependencies) {
|
}: InjectedDependencies) {
|
||||||
super()
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
super(arguments[0])
|
||||||
|
|
||||||
this.paymentProviderService_ = paymentProviderService
|
this.paymentProviderService_ = paymentProviderService
|
||||||
this.productVariantInventoryService_ = productVariantInventoryService
|
this.productVariantInventoryService_ = productVariantInventoryService
|
||||||
@@ -57,7 +55,6 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
this.cartService_ = cartService
|
this.cartService_ = cartService
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
this.swapService_ = swapService
|
this.swapService_ = swapService
|
||||||
this.manager_ = manager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async complete(
|
async complete(
|
||||||
@@ -73,7 +70,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
while (inProgress) {
|
while (inProgress) {
|
||||||
switch (idempotencyKey.recovery_point) {
|
switch (idempotencyKey.recovery_point) {
|
||||||
case "started": {
|
case "started": {
|
||||||
await this.manager_
|
await this.activeManager_
|
||||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||||
idempotencyKey = await this.idempotencyKeyService_
|
idempotencyKey = await this.idempotencyKeyService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
@@ -90,7 +87,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case "tax_lines_created": {
|
case "tax_lines_created": {
|
||||||
await this.manager_
|
await this.activeManager_
|
||||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||||
idempotencyKey = await this.idempotencyKeyService_
|
idempotencyKey = await this.idempotencyKeyService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
@@ -111,7 +108,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "payment_authorized": {
|
case "payment_authorized": {
|
||||||
await this.manager_
|
await this.activeManager_
|
||||||
.transaction("SERIALIZABLE", async (transactionManager) => {
|
.transaction("SERIALIZABLE", async (transactionManager) => {
|
||||||
idempotencyKey = await this.idempotencyKeyService_
|
idempotencyKey = await this.idempotencyKeyService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
@@ -134,7 +131,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
await this.manager_.transaction(async (transactionManager) => {
|
await this.activeManager_.transaction(async (transactionManager) => {
|
||||||
idempotencyKey = await this.idempotencyKeyService_
|
idempotencyKey = await this.idempotencyKeyService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.update(idempotencyKey.idempotency_key, {
|
.update(idempotencyKey.idempotency_key, {
|
||||||
@@ -149,7 +146,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
if (idempotencyKey.recovery_point !== "started") {
|
if (idempotencyKey.recovery_point !== "started") {
|
||||||
await this.manager_.transaction(async (transactionManager) => {
|
await this.activeManager_.transaction(async (transactionManager) => {
|
||||||
try {
|
try {
|
||||||
await this.orderService_
|
await this.orderService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
@@ -264,16 +261,19 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
|||||||
const cartServiceTx = this.cartService_.withTransaction(manager)
|
const cartServiceTx = this.cartService_.withTransaction(manager)
|
||||||
|
|
||||||
const cart = await cartServiceTx.retrieveWithTotals(id, {
|
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 allowBackorder = false
|
||||||
let swapId: string
|
|
||||||
|
|
||||||
if (cart.type === "swap") {
|
if (cart.type === "swap") {
|
||||||
const swap = await swapServiceTx.retrieveByCartId(id)
|
const swap = await swapServiceTx.retrieveByCartId(id)
|
||||||
allowBackorder = swap.allow_backorder
|
allowBackorder = swap.allow_backorder
|
||||||
swapId = swap.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allowBackorder) {
|
if (!allowBackorder) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export type CreateIdempotencyKeyInput = {
|
export type CreateIdempotencyKeyInput = {
|
||||||
request_method: string
|
request_method?: string
|
||||||
request_params: Record<string, unknown>
|
request_params?: Record<string, unknown>
|
||||||
request_path: string
|
request_path?: string
|
||||||
idempotency_key?: string
|
idempotency_key?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
yarn.lock
45
yarn.lock
@@ -9215,6 +9215,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:*":
|
"@types/superagent@npm:*":
|
||||||
version: 4.1.15
|
version: 4.1.15
|
||||||
resolution: "@types/superagent@npm:4.1.15"
|
resolution: "@types/superagent@npm:4.1.15"
|
||||||
@@ -25402,7 +25411,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 0.0.0-use.local
|
||||||
resolution: "medusa-core-utils@workspace:packages/medusa-core-utils"
|
resolution: "medusa-core-utils@workspace:packages/medusa-core-utils"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -25719,28 +25728,16 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "medusa-payment-stripe@workspace:packages/medusa-payment-stripe"
|
resolution: "medusa-payment-stripe@workspace:packages/medusa-payment-stripe"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/cli": ^7.7.5
|
"@medusajs/medusa": ^1.7.7
|
||||||
"@babel/core": ^7.7.5
|
"@types/stripe": ^8.0.417
|
||||||
"@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
|
|
||||||
body-parser: ^1.19.0
|
body-parser: ^1.19.0
|
||||||
client-sessions: ^0.8.0
|
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
express: ^4.17.1
|
express: ^4.17.1
|
||||||
jest: ^25.5.4
|
jest: ^25.5.4
|
||||||
medusa-core-utils: ^1.1.39
|
medusa-core-utils: ^1.1.38
|
||||||
medusa-interfaces: ^1.3.6
|
stripe: ^11.10.0
|
||||||
medusa-test-utils: ^1.1.37
|
|
||||||
stripe: ^8.50.0
|
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
medusa-interfaces: 1.3.6
|
"@medusajs/medusa": ^1.7.7
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -30339,7 +30336,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 6.11.0
|
||||||
resolution: "qs@npm:6.11.0"
|
resolution: "qs@npm:6.11.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -33904,6 +33901,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"stripe@npm:^8.50.0":
|
||||||
version: 8.222.0
|
version: 8.222.0
|
||||||
resolution: "stripe@npm:8.222.0"
|
resolution: "stripe@npm:8.222.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user