chore(): rm payment plugins (#7217)

This commit is contained in:
Adrien de Peretti
2024-05-03 08:42:03 +02:00
committed by GitHub
parent f129415650
commit 0140fd63ab
101 changed files with 2 additions and 9000 deletions

View File

@@ -100,8 +100,6 @@ module.exports = {
"./packages/fulfillment-manual/tsconfig.spec.json",
"./packages/medusa-payment-stripe/tsconfig.spec.json",
"./packages/medusa-payment-paypal/tsconfig.spec.json",
"./packages/medusa-plugin-meilisearch/tsconfig.spec.json",
"./packages/medusa-plugin-algolia/tsconfig.spec.json",

View File

@@ -1,13 +0,0 @@
{
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-instanceof",
"@babel/plugin-transform-classes"
],
"presets": ["@babel/preset-env"],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-runtime"]
}
}
}

View File

@@ -1,16 +0,0 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
!jest.config.js
/dist
/api
/services
/models
/subscribers
/__mocks__

View File

@@ -1,8 +0,0 @@
.DS_store
src
dist
yarn.lock
.babelrc
.turbo
.yarn

View File

@@ -1,392 +0,0 @@
# Change Log
## 1.4.0
### Minor Changes
- [#4483](https://github.com/medusajs/medusa/pull/4483) [`5d0e9ac00`](https://github.com/medusajs/medusa/commit/5d0e9ac007ecd1a60a4d5b8736024d2627da0b6d) Thanks [@edinskeja](https://github.com/edinskeja)! - Add language support
## 1.3.10
### Patch Changes
- [#4276](https://github.com/medusajs/medusa/pull/4276) [`afd1b67f1`](https://github.com/medusajs/medusa/commit/afd1b67f1c7de8cf07fd9fcbdde599a37914e9b5) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Use caret range
## 1.3.9
### Patch Changes
- Updated dependencies [[`121b42acf`](https://github.com/medusajs/medusa/commit/121b42acfe98c12dd593f9b1f2072ff0f3b61724), [`aa690beed`](https://github.com/medusajs/medusa/commit/aa690beed775646cbc86b445fb5dc90dcac087d5), [`54dcc1871`](https://github.com/medusajs/medusa/commit/54dcc1871c8f28bea962dbb9df6e79b038d56449), [`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73)]:
- medusa-core-utils@1.2.0
- medusa-interfaces@1.3.7
- medusa-test-utils@1.1.40
## 1.3.9-rc.0
### Patch Changes
- Updated dependencies [[`121b42acf`](https://github.com/medusajs/medusa/commit/121b42acfe98c12dd593f9b1f2072ff0f3b61724), [`aa690beed`](https://github.com/medusajs/medusa/commit/aa690beed775646cbc86b445fb5dc90dcac087d5), [`54dcc1871`](https://github.com/medusajs/medusa/commit/54dcc1871c8f28bea962dbb9df6e79b038d56449), [`77d46220c`](https://github.com/medusajs/medusa/commit/77d46220c23bfe19e575cbc445874eb6c22f3c73)]:
- medusa-core-utils@1.2.0-rc.0
- medusa-interfaces@1.3.7-rc.0
- medusa-test-utils@1.1.40-rc.0
## 1.3.8
### Patch Changes
- [#3217](https://github.com/medusajs/medusa/pull/3217) [`8c5219a31`](https://github.com/medusajs/medusa/commit/8c5219a31ef76ee571fbce84d7d57a63abe56eb0) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Fix npm packages files included
- Updated dependencies [[`8c5219a31`](https://github.com/medusajs/medusa/commit/8c5219a31ef76ee571fbce84d7d57a63abe56eb0)]:
- medusa-core-utils@1.1.39
- medusa-interfaces@1.3.6
## 1.3.7
### Patch Changes
- [#3011](https://github.com/medusajs/medusa/pull/3011) [`ce866475b`](https://github.com/medusajs/medusa/commit/ce866475b4b6c8b453638000f7b1df7a27daf45d) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(_-payment-_): cleanup payment provider plugins
- [#3185](https://github.com/medusajs/medusa/pull/3185) [`08324355a`](https://github.com/medusajs/medusa/commit/08324355a4466b017a0bc7ab1d333ee3cd27b8c4) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Patches all dependencies + minor bumps `winston` to include a [fix for a significant memory leak](https://github.com/winstonjs/winston/pull/2057)
- Updated dependencies [[`08324355a`](https://github.com/medusajs/medusa/commit/08324355a4466b017a0bc7ab1d333ee3cd27b8c4)]:
- medusa-core-utils@1.1.38
- medusa-interfaces@1.3.5
## 1.3.6
### Patch Changes
- [#3010](https://github.com/medusajs/medusa/pull/3010) [`142c8aa70`](https://github.com/medusajs/medusa/commit/142c8aa70f583d9b11a6add2b8f988e9ba4cf979) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa): Payment collection should provider the region_id, total and id in the partial cart data
- [#3025](https://github.com/medusajs/medusa/pull/3025) [`93d0dc1bd`](https://github.com/medusajs/medusa/commit/93d0dc1bdcb54cf6e87428a7bb9b0dac196b4de2) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa): test, build and watch scripts
- Updated dependencies [[`93d0dc1bd`](https://github.com/medusajs/medusa/commit/93d0dc1bdcb54cf6e87428a7bb9b0dac196b4de2)]:
- medusa-interfaces@1.3.4
## 1.3.5
### Patch Changes
- [#2808](https://github.com/medusajs/medusa/pull/2808) [`0a9c89185`](https://github.com/medusajs/medusa/commit/0a9c891853c4d16b553d38268a3408ca1daa71f0) Thanks [@patrick-medusajs](https://github.com/patrick-medusajs)! - chore: explicitly add devDependencies for monorepo peerDependencies
- Updated dependencies [[`7cced6006`](https://github.com/medusajs/medusa/commit/7cced6006a9a6f9108009e9f3e191e9f3ba1b168)]:
- medusa-core-utils@1.1.37
## 1.3.4
### Patch Changes
- [#2381](https://github.com/medusajs/medusa/pull/2381) [`a908a7716`](https://github.com/medusajs/medusa/commit/a908a7716c94222f340531a5b13db0867b511519) Thanks [@srindom](https://github.com/srindom)! - Rely on cart totals in payment providers
- Updated dependencies [[`7dc8d3a0c`](https://github.com/medusajs/medusa/commit/7dc8d3a0c90ce06e3f11a6a46dec1f9ec3f26e81)]:
- medusa-core-utils@1.1.32
## 1.3.3
### Patch Changes
- Updated dependencies [[`c97ccd3fb`](https://github.com/medusajs/medusa/commit/c97ccd3fb5dbe796b0e4fbf37def5bb6e8201557)]:
- medusa-interfaces@1.3.3
## 1.3.2
### Patch Changes
- [#1848](https://github.com/medusajs/medusa/pull/1848) [`0e5f0d8cd`](https://github.com/medusajs/medusa/commit/0e5f0d8cd60a0040b717c57dccd2056f476199d2) Thanks [@srindom](https://github.com/srindom)! - Fixes issue where shipping totals are calculated incorrectly
* [#1840](https://github.com/medusajs/medusa/pull/1840) [`c20d72004`](https://github.com/medusajs/medusa/commit/c20d72004041d946feda5897920df7d66aad5228) Thanks [@srindom](https://github.com/srindom)! - Bug fixed where the free shipping tax rate was incorrect due to division by zero.
* Updated dependencies [[`1dec44287`](https://github.com/medusajs/medusa/commit/1dec44287df5ac69b4c5769b59f9ebef58d3da68), [`b8ddb31f6`](https://github.com/medusajs/medusa/commit/b8ddb31f6fe296a11d2d988276ba8e991c37fa9b)]:
- medusa-interfaces@1.3.2
## 1.3.1
### Patch Changes
- [#1791](https://github.com/medusajs/medusa/pull/1791) [`e115518d`](https://github.com/medusajs/medusa/commit/e115518dda6f81e302d5c1708b3fecad35e51533) Thanks [@srindom](https://github.com/srindom)! - Join LineItemAdjustments to ensure correct total calculations.
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.3.0](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.2.1...medusa-payment-klarna@1.3.0) (2022-05-01)
**Note:** Version bump only for package medusa-payment-klarna
## [1.2.1](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.41...medusa-payment-klarna@1.2.1) (2022-02-28)
### Features
- new tax api ([#979](https://github.com/medusajs/medusa/issues/979)) ([47588e7](https://github.com/medusajs/medusa/commit/47588e7a8d3b2ae2fed0c1e87fdf1ee2db6bcdc2)), closes [#885](https://github.com/medusajs/medusa/issues/885) [#896](https://github.com/medusajs/medusa/issues/896) [#911](https://github.com/medusajs/medusa/issues/911) [#945](https://github.com/medusajs/medusa/issues/945) [#950](https://github.com/medusajs/medusa/issues/950) [#951](https://github.com/medusajs/medusa/issues/951) [#954](https://github.com/medusajs/medusa/issues/954) [#969](https://github.com/medusajs/medusa/issues/969) [#998](https://github.com/medusajs/medusa/issues/998) [#1017](https://github.com/medusajs/medusa/issues/1017) [#1110](https://github.com/medusajs/medusa/issues/1110)
# [1.2.0](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.41...medusa-payment-klarna@1.2.0) (2022-02-25)
### Features
- new tax api ([#979](https://github.com/medusajs/medusa/issues/979)) ([c56660f](https://github.com/medusajs/medusa/commit/c56660fca9921a3f3637bc137d9794781c5b090f)), closes [#885](https://github.com/medusajs/medusa/issues/885) [#896](https://github.com/medusajs/medusa/issues/896) [#911](https://github.com/medusajs/medusa/issues/911) [#945](https://github.com/medusajs/medusa/issues/945) [#950](https://github.com/medusajs/medusa/issues/950) [#951](https://github.com/medusajs/medusa/issues/951) [#954](https://github.com/medusajs/medusa/issues/954) [#969](https://github.com/medusajs/medusa/issues/969) [#998](https://github.com/medusajs/medusa/issues/998) [#1017](https://github.com/medusajs/medusa/issues/1017) [#1110](https://github.com/medusajs/medusa/issues/1110)
## [1.1.41](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.40...medusa-payment-klarna@1.1.41) (2022-02-06)
### Bug Fixes
- release ([fc3fbc8](https://github.com/medusajs/medusa/commit/fc3fbc897fad5c8a5d3eea828ac7277fba9d70af))
## [1.1.40](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.39...medusa-payment-klarna@1.1.40) (2022-02-06)
### Features
- medusa-react admin hooks ([#978](https://github.com/medusajs/medusa/issues/978)) ([2e38484](https://github.com/medusajs/medusa/commit/2e384842d5b2e9742a86b96f28a8f00357795b86)), closes [#1019](https://github.com/medusajs/medusa/issues/1019)
## [1.1.39](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.38...medusa-payment-klarna@1.1.39) (2022-01-11)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.38](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.37...medusa-payment-klarna@1.1.38) (2021-12-29)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.37](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.36...medusa-payment-klarna@1.1.37) (2021-12-17)
### Bug Fixes
- **medusa-payment-klarna:** Default shipping fee name in Klarna plugin ([#926](https://github.com/medusajs/medusa/issues/926)) ([8782016](https://github.com/medusajs/medusa/commit/8782016095e82a21ac3e623f7d6fa0413d86ae1e))
## [1.1.36](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.35...medusa-payment-klarna@1.1.36) (2021-12-08)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.35](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.34...medusa-payment-klarna@1.1.35) (2021-11-23)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.34](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.33...medusa-payment-klarna@1.1.34) (2021-11-22)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.33](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.32...medusa-payment-klarna@1.1.33) (2021-11-19)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.32](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.31...medusa-payment-klarna@1.1.32) (2021-11-19)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.31](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.30...medusa-payment-klarna@1.1.31) (2021-10-18)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.30](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.29...medusa-payment-klarna@1.1.30) (2021-10-18)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.29](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.27...medusa-payment-klarna@1.1.29) (2021-10-18)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.28](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.27...medusa-payment-klarna@1.1.28) (2021-10-18)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.27](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.26...medusa-payment-klarna@1.1.27) (2021-09-15)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.26](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.25...medusa-payment-klarna@1.1.26) (2021-09-14)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.25](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.24...medusa-payment-klarna@1.1.25) (2021-08-05)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.24](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.23...medusa-payment-klarna@1.1.24) (2021-07-26)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.23](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.21...medusa-payment-klarna@1.1.23) (2021-07-15)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.22](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.21...medusa-payment-klarna@1.1.22) (2021-07-15)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.21](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.20...medusa-payment-klarna@1.1.21) (2021-07-02)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.20](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.18...medusa-payment-klarna@1.1.20) (2021-06-22)
### Bug Fixes
- address fix ([dbf9ba7](https://github.com/medusajs/medusa/commit/dbf9ba7b632570e5fc8efde522837fa44bbf5427))
## [1.1.18](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.17...medusa-payment-klarna@1.1.18) (2021-06-09)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.17](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.16...medusa-payment-klarna@1.1.17) (2021-06-09)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.16](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.15...medusa-payment-klarna@1.1.16) (2021-06-09)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.15](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.14...medusa-payment-klarna@1.1.15) (2021-06-09)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.14](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.13...medusa-payment-klarna@1.1.14) (2021-06-08)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.13](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.10...medusa-payment-klarna@1.1.13) (2021-04-28)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.12](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.11...medusa-payment-klarna@1.1.12) (2021-04-20)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.11](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.10...medusa-payment-klarna@1.1.11) (2021-04-20)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.10](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.9...medusa-payment-klarna@1.1.10) (2021-04-13)
### Bug Fixes
- merge develop ([2982a8e](https://github.com/medusajs/medusa/commit/2982a8e682e90beb4549d969d9d3b04d78a46a2d))
- merge develop ([a468c45](https://github.com/medusajs/medusa/commit/a468c451e82c68f41b5005a2e480057f6124aaa6))
## [1.1.9](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.8...medusa-payment-klarna@1.1.9) (2021-04-13)
### Bug Fixes
- publish assist ([7719957](https://github.com/medusajs/medusa/commit/7719957b44a0c0d950eff948faf31188fe0e3ef1))
## [1.1.8](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.8...medusa-payment-klarna@1.1.8) (2021-03-30)
### Bug Fixes
- publish assist ([7719957](https://github.com/medusajs/medusa/commit/7719957b44a0c0d950eff948faf31188fe0e3ef1))
## [1.1.8](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.7...medusa-payment-klarna@1.1.8) (2021-03-23)
### Bug Fixes
- klarna doesn't treat negative discounts well ([e275184](https://github.com/medusajs/medusa/commit/e27518435ea1fafe556168c36475916a4243eabe))
## [1.1.7](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.6...medusa-payment-klarna@1.1.7) (2021-03-17)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.6](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.4...medusa-payment-klarna@1.1.6) (2021-03-17)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.5](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.4...medusa-payment-klarna@1.1.5) (2021-03-17)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.3...medusa-payment-klarna@1.1.4) (2021-02-17)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.2...medusa-payment-klarna@1.1.3) (2021-02-09)
### Bug Fixes
- expired klarna orders ([5ef13d4](https://github.com/medusajs/medusa/commit/5ef13d49c0a2f6d82c5c2342ad800749e41d46fb))
## [1.1.2](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.1...medusa-payment-klarna@1.1.2) (2021-02-03)
**Note:** Version bump only for package medusa-payment-klarna
## [1.1.1](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.1.0...medusa-payment-klarna@1.1.1) (2021-01-27)
**Note:** Version bump only for package medusa-payment-klarna
# [1.1.0](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.0.14...medusa-payment-klarna@1.1.0) (2021-01-26)
**Note:** Version bump only for package medusa-payment-klarna
## [1.0.14](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.0.13...medusa-payment-klarna@1.0.14) (2020-12-17)
**Note:** Version bump only for package medusa-payment-klarna
## [1.0.13](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.0.12...medusa-payment-klarna@1.0.13) (2020-11-24)
**Note:** Version bump only for package medusa-payment-klarna
## [1.0.12](https://github.com/medusajs/medusa/compare/medusa-payment-klarna@1.0.11...medusa-payment-klarna@1.0.12) (2020-10-19)
**Note:** Version bump only for package medusa-payment-klarna
## 1.0.11 (2020-09-17)
## 1.0.10 (2020-09-09)
### Bug Fixes
- updates license ([db519fb](https://github.com/medusajs/medusa/commit/db519fbaa6f8ad02c19cbecba5d4f28ba1ee81aa))
## 1.0.7 (2020-09-07)
## 1.0.1 (2020-09-05)
## 1.0.1-beta.0 (2020-09-04)
# 1.0.0 (2020-09-03)
# 1.0.0-y.6 (2020-08-31)
### Bug Fixes
- parse response correctly ([9adc52d](https://github.com/medusajs/medusa/commit/9adc52d0c59c4bc2fd9c3c9d3418c9c7d57c63b0))
# 1.0.0-y.5 (2020-08-31)
### Bug Fixes
- use correct path for captures ([91befc0](https://github.com/medusajs/medusa/commit/91befc03cd63829e57fd853b2b34f92e9d43e4e3))
# 1.0.0-y.3 (2020-08-31)
### Bug Fixes
- auto pick country in 1 country regions ([d11eb8f](https://github.com/medusajs/medusa/commit/d11eb8fa65e0d036f88890ed7fd771a607f75cbc))
# 1.0.0-y.2 (2020-08-31)
### Bug Fixes
- **medusa-payment-klarna:** get correct country ([daa425d](https://github.com/medusajs/medusa/commit/daa425de16297e4315a85c1f844eab7a249dc6de))
# 1.0.0-y.1 (2020-08-31)
### Bug Fixes
- add backend url as option ([84f600c](https://github.com/medusajs/medusa/commit/84f600c1bc9888805e3588785c8fac1faa509207))
# 1.0.0-alpha.30 (2020-08-28)
# 1.0.0-alpha.27 (2020-08-27)
# 1.0.0-alpha.26 (2020-08-27)
# 1.0.0-alpha.24 (2020-08-27)
# 1.0.0-alpha.3 (2020-08-20)
# 1.0.0-alpha.2 (2020-08-20)
# 1.0.0-alpha.1 (2020-08-20)
# 1.0.0-alpha.0 (2020-08-20)
## [1.0.10](https://github.com/medusajs/medusa/compare/v1.0.9...v1.0.10) (2020-09-09)
### Bug Fixes
- updates license ([db519fb](https://github.com/medusajs/medusa/commit/db519fbaa6f8ad02c19cbecba5d4f28ba1ee81aa))

View File

@@ -1,84 +0,0 @@
# Klarna
Receive payments on your Medusa commerce application using Klarna.
[Klarna Plugin Documentation](https://docs.medusajs.com/plugins/payment/klarna) | [Medusa Website](https://medusajs.com/) | [Medusa Repository](https://github.com/medusajs/medusa)
## Features
- Authorize payments on orders from any sales channel.
- Capture payments from the admin dashboard.
- Support for Webhooks.
---
## Prerequisites
- [Medusa backend](https://docs.medusajs.com/development/backend/install)
- [Klarna account](https://www.klarna.com/)
---
## How to Install
1\. Run the following command in the directory of the Medusa backend:
```bash
npm install medusa-payment-klarna
```
2\. Set the following environment variables in `.env`:
```bash
KLARNA_BACKEND_URL=<YOUR_KLARNA_BACKEND_URL>
KLARNA_URL=<YOUR_KLARNA_URL>
KLARNA_USER=<YOUR_KLARNA_USER>
KLARNA_PASSWORD=<YOUR_KLARNA_PASSWORD>
KLARNA_TERMS_URL=<YOUR_KLARNA_TERMS_URL>
KLARNA_CHECKOUT_URL=<YOUR_KLARNA_CHECKOUT_URL>
KLARNA_CONFIRMATION_URL=<YOUR_KLARNA_CONFIRMATION_URL>
KLARNA_LANGUAGE=<YOUR_KLARNA_LANGUAGE>
```
3\. In `medusa-config.js` add the following at the end of the `plugins` array:
```js
const plugins = [
// other plugins...
{
resolve: `medusa-payment-klarna`,
options: {
backend_url: process.env.KLARNA_BACKEND_URL,
url: process.env.KLARNA_URL,
user: process.env.KLARNA_USER,
password: process.env.KLARNA_PASSWORD,
language: process.env.KLARNA_LANGUAGE,
merchant_urls: {
terms: process.env.KLARNA_TERMS_URL,
checkout: process.env.KLARNA_CHECKOUT_URL,
confirmation: process.env.KLARNA_CONFIRMATION_URL,
},
},
},
]
```
---
## Test the Plugin
1\. Run the following command in the directory of the Medusa backend to run the backend:
```bash
npm run start
```
2\. Enable Klarna in a region in the admin. You can refer to [this User Guide](https://docs.medusajs.com/user-guide/regions/providers) to learn how to do that. Alternatively, you can use the [Admin APIs](https://docs.medusajs.com/api/admin#tag/Region/operation/PostRegionsRegion).
3\. Place an order using a storefront or the [Store APIs](https://docs.medusajs.com/api/store). You should be able to use Stripe as a payment method.
---
## Additional Resources
- [Klarna Plugin Documentation](https://docs.medusajs.com/plugins/payment/klarna)

View File

@@ -1 +0,0 @@
// noop

View File

@@ -1,6 +0,0 @@
module.exports = {
testEnvironment: "node",
transform: {
"^.+\\.[jt]s?$": `../../jest-transformer.js`,
},
}

View File

@@ -1,55 +0,0 @@
{
"name": "medusa-payment-klarna",
"version": "1.4.0",
"description": "Klarna Payment provider for Medusa Commerce",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-payment-klarna"
},
"engines": {
"node": ">=16"
},
"author": "Oliver Juhl",
"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-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",
"axios-mock-adapter": "^1.19.0",
"client-sessions": "^0.8.0",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.7",
"medusa-test-utils": "^1.1.40"
},
"scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build",
"test": "jest --passWithNoTests src",
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
},
"peerDependencies": {
"medusa-interfaces": "^1.3.7"
},
"dependencies": {
"@babel/plugin-transform-classes": "^7.9.5",
"axios": "^0.21.4",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"lodash": "^4.17.21",
"medusa-core-utils": "^1.2.0"
},
"gitHead": "cd1f5afa5aa8c0b15ea957008ee19f1d695cbd2e",
"keywords": [
"medusa-plugin",
"medusa-plugin-payment"
]
}

View File

@@ -1,135 +0,0 @@
import { IdMap } from "medusa-test-utils"
export const carts = {
frCart: {
id: IdMap.getId("fr-cart"),
email: "lebron@james.com",
title: "test",
region: {
tax_rate: 2500,
currency_code: "eur",
},
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",
tax_lines: [],
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",
tax_lines: [],
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"),
name: "Free shipping",
tax_lines: [],
data: {
name: "test",
},
shipping_option: {
id: IdMap.getId("freeShipping"),
name: "Free shipping",
},
profile_id: "default_profile",
},
],
shipping_options: [
{
id: IdMap.getId("freeShipping"),
name: "Free shipping",
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"),
},
},
shipping_address: {},
billing_address: {},
discounts: [
{
code: "MEDUSA_FREE",
rule: {
type: "percent",
value: 20,
allocation: "item",
},
},
],
customer_id: IdMap.getId("lebron"),
},
}
export const CartServiceMock = {
retrieve: jest.fn().mockImplementation((cartId) => {
if (cartId === IdMap.getId("fr-cart")) {
return Promise.resolve(carts.frCart)
}
return Promise.resolve(undefined)
}),
updatePaymentSession: jest
.fn()
.mockImplementation((cartId, stripe, paymentIntent) => {
return Promise.resolve()
}),
}
const mock = jest.fn().mockImplementation(() => {
return CartServiceMock
})
export default mock

View File

@@ -1,21 +0,0 @@
import { IdMap } from "medusa-test-utils"
export const RegionServiceMock = {
retrieve: jest.fn().mockImplementation((regionId) => {
return Promise.resolve({
_id: IdMap.getId("testRegion"),
name: "Test Region",
countries: ["DK", "US", "DE"],
tax_rate: 0.25,
payment_providers: ["default_provider", "unregistered"],
fulfillment_providers: ["test_shipper"],
currency_code: "usd",
})
}),
}
const mock = jest.fn().mockImplementation(() => {
return RegionServiceMock
})
export default mock

View File

@@ -1,24 +0,0 @@
export const TotalsServiceMock = {
getTotal: jest.fn(),
getTaxTotal: jest.fn(),
getAllocationItemDiscounts: jest.fn(),
getDiscountTotal: jest.fn(),
getLineItemTotals: jest.fn(() => {
return {
total: 10,
tax_lines: [],
}
}),
getShippingMethodTotals: jest.fn(() => {
return {
total: 10,
tax_lines: [],
}
}),
}
const mock = jest.fn().mockImplementation(() => {
return TotalsServiceMock
})
export default mock

View File

@@ -1,10 +0,0 @@
import { Router } from "express"
import hooks from "./routes/hooks"
export default (container) => {
const app = Router()
hooks(app)
return app
}

View File

@@ -1 +0,0 @@
export default (fn) => (...args) => fn(...args).catch(args[2])

View File

@@ -1,5 +0,0 @@
import { default as wrap } from "./await-middleware"
export default {
wrap,
}

View File

@@ -1,77 +0,0 @@
export default async (req, res) => {
// In Medusa, we store the cart id in merchant_data
const { shipping_address, merchant_data } = req.body
const cartService = req.scope.resolve("cartService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService")
if (shipping_address) {
const shippingAddress = {
first_name: shipping_address.given_name,
last_name: shipping_address.family_name,
address_1: shipping_address.street_address,
address_2: shipping_address.street_address2,
city: shipping_address.city,
country_code: shipping_address.country,
postal_code: shipping_address.postal_code,
phone: shipping_address.phone,
}
let billingAddress = {
first_name: shipping_address.given_name,
last_name: shipping_address.family_name,
address_1: shipping_address.street_address,
address_2: shipping_address.street_address2,
city: shipping_address.city,
country_code: shipping_address.country,
postal_code: shipping_address.postal_code,
phone: shipping_address.phone,
}
await cartService.update(merchant_data, {
shipping_address: shippingAddress,
billing_address: billingAddress,
email: shipping_address.email,
})
let cart = await cartService.retrieveWithTotals(merchant_data, {
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items.variant.product.profiles",
],
})
const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
if (shippingOptions?.length) {
const option = shippingOptions.find(
(o) => o.data && !o.data.require_drop_point
)
if (option) {
await cartService.addShippingMethod(cart.id, option.id, option.data)
cart = await cartService.retrieveWithTotals(cart.id, {
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
}
}
const order = await klarnaProviderService.cartToKlarnaOrder(cart)
res.json(order)
} else {
res.sendStatus(400)
return
}
}

View File

@@ -1,16 +0,0 @@
import { Router } from "express"
import bodyParser from "body-parser"
import middlewares from "../../middlewares"
const route = Router()
export default (app) => {
app.use("/klarna", route)
route.use(bodyParser.json())
route.post("/address", middlewares.wrap(require("./address").default))
route.post("/shipping", middlewares.wrap(require("./shipping").default))
route.post("/push", middlewares.wrap(require("./push").default))
return app
}

View File

@@ -1,34 +0,0 @@
export default async (req, res) => {
const { klarna_order_id } = req.query
function isPaymentCollection(id) {
return id && id.startsWith("paycol")
}
try {
const orderService = req.scope.resolve("orderService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
const klarnaOrder = await klarnaProviderService.retrieveCompletedOrder(
klarna_order_id
)
const resourceId = klarnaOrder.merchant_data
if (isPaymentCollection(resourceId)) {
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id)
} else {
const order = await orderService.retrieveByCartId(resourceId)
await klarnaProviderService.acknowledgeOrder(
klarnaOrder.order_id,
order.id
)
}
res.sendStatus(200)
} catch (error) {
console.log(error)
throw error
}
}

View File

@@ -1,65 +0,0 @@
export default async (req, res) => {
// In Medusa, we store the cart id in merchant_data
const { merchant_data, selected_shipping_option } = req.body
try {
const cartService = req.scope.resolve("cartService")
const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService")
const cart = await cartService.retrieveWithTotals(
merchant_data,
{
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.adjustments",
"items.variant",
"items.variant.product",
],
},
{ force_taxes: true }
)
let shippingOptions = await shippingProfileService.fetchCartOptions(cart)
shippingOptions = shippingOptions.filter(
(so) => !so.data?.require_drop_point
)
const ids = selected_shipping_option.id.split(".")
for (const id of ids) {
const option = shippingOptions.find((so) => so.id === id)
if (option) {
await cartService.addShippingMethod(cart.id, option.id, option.data)
}
}
const newCart = await cartService.retrieveWithTotals(
cart.id,
{
relations: [
"shipping_address",
"billing_address",
"shipping_methods",
"shipping_methods.shipping_option",
"region",
"items",
"items.adjustments",
"items.variant",
"items.variant.product",
],
},
{ force_taxes: true }
)
const order = await klarnaProviderService.cartToKlarnaOrder(newCart)
res.json(order)
} catch (error) {
throw error
}
}

View File

@@ -1,41 +0,0 @@
import { IdMap } from "medusa-test-utils"
export const KlarnaProviderServiceMock = {
retrievePayment: jest.fn().mockImplementation((cart) => {
if (cart._id === IdMap.getId("fr-cart")) {
return Promise.resolve({
order_id: "123456789",
})
}
return Promise.resolve(undefined)
}),
cancelPayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
updatePayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
capturePayment: jest.fn().mockImplementation((cart) => {
if (cart._id === IdMap.getId("fr-cart")) {
return Promise.resolve({
id: "123456789",
})
}
return Promise.resolve(undefined)
}),
createPayment: jest.fn().mockImplementation((cart) => {
if (cart._id === IdMap.getId("fr-cart")) {
return Promise.resolve({
id: "123456789",
order_amount: 100,
})
}
return Promise.resolve(undefined)
}),
}
const mock = jest.fn().mockImplementation(() => {
return KlarnaProviderServiceMock
})
export default mock

View File

@@ -1,393 +0,0 @@
jest.unmock("axios")
import MockAdapter from "axios-mock-adapter"
import { carts } from "../../__mocks__/cart"
import { RegionServiceMock } from "../../__mocks__/region"
import { TotalsServiceMock } from "../../__mocks__/totals"
import KlarnaProviderService from "../klarna-provider"
describe("KlarnaProviderService", () => {
describe("createPayment", () => {
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
merchant_urls: {
terms: "terms",
checkout: "checkout",
confirmation: "confirmation",
},
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer.onPost("/checkout/v3/orders").reply(() => {
return [200, { order_id: "123456789", order_amount: 100 }]
})
beforeEach(() => {
jest.clearAllMocks()
})
it("creates Klarna order", async () => {
const result = await klarnaProviderService.createPayment(carts.frCart)
expect(result).toEqual({
order_id: "123456789",
order_amount: 100,
})
})
it("creates Klarna order using new API", async () => {
const result = await klarnaProviderService.createPayment({
email: "",
context: {},
shipping_methods: [],
shipping_address: null,
id: "",
region_id: carts.frCart.region_id,
total: carts.frCart.total,
resource_id: "resource_id",
currency_code: carts.frCart.region.currency_code,
amount: carts.frCart.total
})
expect(result).toEqual({
order_id: "123456789",
order_amount: 100,
})
})
})
describe("retrievePayment", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer.onGet("/checkout/v3/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns Klarna order", async () => {
result = await klarnaProviderService.retrievePayment({
order_id: "123456789",
})
expect(result).toEqual({
order_id: "123456789",
})
})
})
describe("retrieveCompletedOrder", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns completed Klarna order", async () => {
result = await klarnaProviderService.retrieveCompletedOrder("123456789")
expect(result).toEqual({
order_id: "123456789",
})
})
})
describe("updatePayment", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
merchant_urls: {
terms: "terms",
checkout: "checkout",
confirmation: "confirmation",
},
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer.onPost("/checkout/v3/orders/123456789").reply(() => {
return [
200,
{
order_id: "123456789",
order_amount: 1000,
},
]
})
it("returns updated Klarna order", async () => {
result = await klarnaProviderService.updatePayment(
{
order_id: "123456789",
},
carts.frCart
)
expect(result).toEqual({
order_id: "123456789",
})
})
it("returns updated Klarna order using new API", async () => {
result = await klarnaProviderService.updatePayment(
{
order_id: "123456789",
},
{
email: "",
context: {},
shipping_methods: [],
shipping_address: null,
id: "",
region_id: carts.frCart.region_id,
total: carts.frCart.total,
resource_id: "resource_id",
currency_code: carts.frCart.region.currency_code,
amount: carts.frCart.total
}
)
expect(result).toEqual({
order_id: "123456789",
})
})
})
describe("cancelPayment", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer
.onPost("/ordermanagement/v1/orders/123456789/cancel")
.reply(() => {
return [200, { order_id: "123456789" }]
})
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.cancelPayment({
data: { order_id: "123456789" },
})
expect(result).toEqual({ order_id: "123456789" })
})
})
describe("acknowledgeOrder", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer
.onPost("/ordermanagement/v1/orders/123456789/acknowledge")
.reply(() => {
return [200]
})
mockServer
.onPatch("/ordermanagement/v1/orders/123456789/merchant-references")
.reply(() => {
return [200]
})
it("returns order id", async () => {
result = await klarnaProviderService.acknowledgeOrder("123456789")
expect(result).toEqual("123456789")
})
})
describe("addOrderToKlarnaOrder", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer
.onPost("/ordermanagement/v1/orders/123456789/merchant-references")
.reply(() => {
return [200]
})
it("returns order id", async () => {
result = await klarnaProviderService.addOrderToKlarnaOrder(
"123456789",
"order123456789"
)
expect(result).toEqual("123456789")
})
})
describe("capturePayment", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [
200,
{
order: { order_amount: 1000 },
},
]
})
mockServer
.onPost("/ordermanagement/v1/orders/123456789/captures")
.reply(() => {
return [200, { order_id: "123456789" }]
})
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.capturePayment({
data: { order_id: "123456789" },
})
expect(result).toEqual({ order_id: "123456789" })
})
})
describe("refundPayment", () => {
let result
beforeEach(async () => {
jest.clearAllMocks()
})
const klarnaProviderService = new KlarnaProviderService(
{
totalsService: TotalsServiceMock,
regionService: RegionServiceMock,
},
{
url: "medusajs/tests",
user: "lebronjames",
password: "123456789",
}
)
const mockServer = new MockAdapter(klarnaProviderService.klarna_)
mockServer
.onPost("/ordermanagement/v1/orders/123456789/refunds")
.reply(() => {
return [200, { order_id: "123456789" }]
})
mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.refundPayment(
{
data: { order_id: "123456789" },
},
1000
)
expect(result).toEqual({ order_id: "123456789" })
})
})
})

View File

@@ -1,554 +0,0 @@
import axios from "axios"
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class KlarnaProviderService extends PaymentService {
static identifier = "klarna"
constructor({ logger, shippingProfileService }, options) {
super()
/**
* Required Klarna options:
* {
* backend_url: "",
* url: "",
* user: "",
* password: "",
* merchant_urls: {
* terms: ``,
* checkout: ``,
* confirmation: ``,
* }
* }
*/
this.options_ = options
this.logger_ = logger
/** @private @const {Klarna} */
this.klarna_ = axios.create({
baseURL: options.url,
auth: {
username: options.user,
password: options.password,
},
})
this.klarnaOrderUrl_ = "/checkout/v3/orders"
this.klarnaOrderManagementUrl_ = "/ordermanagement/v1/orders"
this.backendUrl_ = options.backend_url
this.locale_ = options.language ?? 'en-US'
/** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService
}
async lineItemsToOrderLines_(cart) {
let order_lines = []
for (const item of cart.items ?? []) {
// Withdraw discount from the total item amount
const quantity = item.quantity
const tax = item.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
order_lines.push({
name: item.title,
tax_rate: tax * 10000,
quantity,
unit_price: Math.round(item.original_total / item.quantity),
total_amount: item.total,
total_tax_amount: item.tax_total,
total_discount_amount: item.original_total - item.total,
})
}
if (cart.shipping_methods?.length) {
const name = []
let total = 0
let tax = 0
let taxRate = 0
if (cart.shipping_total > 0) {
for (const method of cart.shipping_methods) {
const methodTaxRate =
method.tax_lines.reduce((acc, next) => acc + next.rate, 0) / 100
name.push(method.shipping_option.name)
total += method.total
taxRate += (method.price / cart.shipping_total) * methodTaxRate
tax += method.tax_total
}
}
order_lines.push({
name: name?.join(" + ") || "Shipping fee",
quantity: 1,
type: "shipping_fee",
unit_price: total,
tax_rate: taxRate * 10000,
total_amount: total,
total_tax_amount: tax,
})
}
return order_lines
}
async cartToKlarnaOrder(cart) {
let order = {
// Cart id is stored, such that we can use it for hooks
merchant_data: cart.resource_id ?? cart.id,
locale: this.locale_,
}
const { region, gift_card_total, tax_total, total } = cart
order.order_lines = await this.lineItemsToOrderLines_(cart)
if (gift_card_total) {
const taxRate = cart.gift_card_tax_total / cart.gift_card_total
order.order_lines.push({
name: "Gift Card",
quantity: 1,
type: "gift_card",
unit_price: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
tax_rate: Math.round(taxRate * 10000),
total_amount: -1 * (cart.gift_card_total + cart.gift_card_tax_total),
total_tax_amount: -1 * cart.gift_card_tax_total,
})
}
if (!_.isEmpty(cart.billing_address)) {
order.billing_address = {
email: cart.email,
street_address: cart.billing_address.address_1,
street_address2: cart.billing_address.address_2,
postal_code: cart.billing_address.postal_code,
city: cart.billing_address.city,
country: cart.billing_address.country_code,
}
}
const hasCountry =
!_.isEmpty(cart.shipping_address) && cart.shipping_address.country_code
if (hasCountry) {
order.purchase_country = cart.shipping_address.country_code.toUpperCase()
} else {
// Defaults to Sweden
order.purchase_country = "SE"
}
order.order_amount = total
order.order_tax_amount = tax_total - cart.gift_card_tax_total ?? 0
order.purchase_currency = region?.currency_code?.toUpperCase() ?? "SE"
order.merchant_urls = {
terms: this.options_.merchant_urls.terms,
checkout: this.options_.merchant_urls.checkout,
confirmation: this.options_.merchant_urls.confirmation,
push: `${this.backendUrl_}/klarna/push?klarna_order_id={checkout.order.id}`,
shipping_option_update: `${this.backendUrl_}/klarna/shipping`,
address_update: `${this.backendUrl_}/klarna/address`,
}
if (cart.shipping_address && cart.shipping_address.first_name) {
let shippingOptions = await this.shippingProfileService_.fetchCartOptions(
cart
)
shippingOptions = shippingOptions.filter(
(so) => !so.data?.require_drop_point
)
// If the cart does not have shipping methods yet, preselect one from
// shipping_options and set the selected shipping method
if (cart.shipping_methods.length) {
const method = cart.shipping_methods[0]
const taxRate = method.tax_total / (method.total - method.tax_total)
order.selected_shipping_option = {
id: method.shipping_option.id,
name: method.shipping_option.name,
price: method.total,
tax_amount: method.tax_total,
tax_rate: taxRate * 10000,
}
}
const partitioned = shippingOptions.reduce((acc, next) => {
if (acc[next.profile_id]) {
acc[next.profile_id] = [...acc[next.profile_id], next]
} else {
acc[next.profile_id] = [next]
}
return acc
}, {})
// Helper function that calculates the cartesian product of multiple arrays
// Don't touch :D
// From: https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript
const f = (a, b) =>
[].concat(...a.map((d) => b.map((e) => [].concat(d, e))))
const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a)
const methods = Object.keys(partitioned).map((k) => partitioned[k])
const combinations = cartesian(...methods)
const taxRate = region.tax_rate / 100
// Use the cartesian product of shipping methods to generate correct
// format for the Klarna Widget
order.shipping_options = combinations.map((combination) => {
combination = Array.isArray(combination) ? combination : [combination]
const details = combination.reduce(
(acc, next) => {
acc.id = [...acc.id, next.id]
acc.name = [...acc.name, next.name]
acc.price += next.amount
return acc
},
{ id: [], name: [], price: 0 }
)
return {
id: details.id.join("."),
name: details.name.join(" + "),
price: details.price * (1 + taxRate),
tax_amount: details.price * taxRate,
tax_rate: taxRate * 10000,
}
})
}
return order
}
validateKlarnaOrderUrls(property) {
const required = ["terms", "checkout", "confirmation"]
const isMissing = required.some((prop) => !this.options_[property]?.[prop])
if (isMissing) {
throw new Error(
`options.${property} is required to create a Klarna Order.\n` +
`medusa-config.js file has to contain ${property} { ${required.join(
", "
)}}`
)
}
}
static replaceStringWithPropertyValue(string, obj) {
const keys = Object.keys(obj)
for (const key of keys) {
if (string.includes(`{${key}}`)) {
string = string.replace(`{${key}}`, obj[key])
}
}
return string
}
async paymentInputToKlarnaOrder(paymentInput) {
if (paymentInput.cart) {
this.validateKlarnaOrderUrls("merchant_urls")
return this.cartToKlarnaOrder(paymentInput.cart)
}
this.validateKlarnaOrderUrls("payment_collection_urls")
const { currency_code, amount, resource_id } = paymentInput
const order = {
// Custom id is stored, such that we can use it for hooks
merchant_data: resource_id,
locale: this.locale_,
order_lines: [
{
name: "Payment Collection",
quantity: 1,
unit_price: amount,
tax_rate: 0,
total_amount: amount,
total_tax_amount: 0,
},
],
// Defaults to Sweden
purchase_country: "SE",
order_amount: amount,
order_tax_amount: 0,
purchase_currency: currency_code.toUpperCase(),
merchant_urls: {
terms: KlarnaProviderService.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.terms,
paymentInput
),
checkout: KlarnaProviderService.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.checkout,
paymentInput
),
confirmation: KlarnaProviderService.replaceStringWithPropertyValue(
this.options_.payment_collection_urls.confirmation,
paymentInput
),
push: `${this.backendUrl_}/klarna/push?klarna_order_id={checkout.order.id}`,
},
}
return order
}
/**
* Status for Klarna order.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the Klarna order
*/
async getStatus(paymentData) {
const { order_id } = paymentData
const { data: order } = await this.klarna_.get(
`${this.klarnaOrderUrl_}/${order_id}`
)
let status = "pending"
if (order.status === "checkout_complete") {
status = "authorized"
}
return status
}
/**
* Creates Klarna PaymentIntent.
* @param {string} cart - the cart to create a payment for
* @returns {string} id of payment intent
*/
async createPayment(cart) {
try {
const order = await this.cartToKlarnaOrder(cart)
return await this.klarna_
.post(this.klarnaOrderUrl_, order)
.then(({ data }) => data)
} catch (error) {
this.logger_.error(error)
throw error
}
}
/**
* Retrieves Klarna Order.
* @param {string} cart - the cart to retrieve order for
* @returns {Object} Klarna order
*/
async retrievePayment(paymentData) {
try {
return this.klarna_
.get(`${this.klarnaOrderUrl_}/${paymentData.order_id}`)
.then(({ data }) => data)
} catch (error) {
throw error
}
}
/**
* Gets a Klarna payment objec.
* @param {object} sessionData - the data of the payment to retrieve
* @returns {Promise<object>} Stripe payment intent
*/
async getPaymentData(sessionData) {
try {
return this.klarna_
.get(`${this.klarnaOrderUrl_}/${sessionData.data.order_id}`)
.then(({ data }) => data)
} catch (error) {
throw error
}
}
/**
* Retrieves completed Klarna Order.
* @param {string} klarnaOrderId - id of the order to retrieve
* @returns {Object} Klarna order
*/
async retrieveCompletedOrder(klarnaOrderId) {
try {
return this.klarna_
.get(`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`)
.then(({ data }) => data)
} catch (error) {
throw error
}
}
/**
* Authorizes Klarna payment by simply returning the status for the payment
* in use.
* @param {object} sessionData - payment session data
* @param {object} context - properties relevant to current context
* @returns {Promise<{ status: string, data: object }>} result with data and status
*/
async authorizePayment(sessionData, context = {}) {
try {
const paymentStatus = await this.getStatus(sessionData.data)
return { data: sessionData.data, status: paymentStatus }
} catch (error) {
throw error
}
}
/**
* Acknowledges a Klarna order as part of the order completion process
* @param {string} klarnaOrderId - id of the order to acknowledge
* @returns {string} id of acknowledged order
*/
async acknowledgeOrder(klarnaOrderId, orderId = null) {
try {
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/acknowledge`
)
if (orderId !== null) {
await this.klarna_.patch(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
{
merchant_reference1: orderId,
}
)
}
return klarnaOrderId
} catch (error) {
throw error
}
}
/**
* Adds the id of the Medusa order to the Klarna Order to create a relation
* @param {string} klarnaOrderId - id of the klarna order
* @param {string} orderId - id of the Medusa order
* @returns {string} id of updated order
*/
async addOrderToKlarnaOrder(klarnaOrderId, orderId) {
try {
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
{
merchant_reference1: orderId,
}
)
return klarnaOrderId
} catch (error) {
throw error
}
}
async updatePaymentData(sessionData, update) {
try {
return { ...sessionData, ...update }
} catch (error) {
throw error
}
}
/**
* Updates Klarna order.
* @param {string} paymentData
* @param {Object} cart
* @returns {Object} updated order
*/
async updatePayment(paymentData, cart) {
if (cart.total !== paymentData.order_amount) {
const order = await this.cartToKlarnaOrder(cart)
return this.klarna_
.post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order)
.then(({ data }) => data)
.catch(async (_) => {
return this.klarna_
.post(this.klarnaOrderUrl_, order)
.then(({ data }) => data)
})
}
return paymentData
}
/**
* Captures Klarna order.
* @param {Object} paymentData - payment method data from cart
* @returns {string} id of captured order
*/
async capturePayment(payment) {
const { order_id } = payment.data
try {
const { data: order } = await this.klarna_.get(
`${this.klarnaOrderManagementUrl_}/${order_id}`
)
const { order_amount } = order
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${order_id}/captures`,
{
captured_amount: order_amount,
}
)
return this.retrieveCompletedOrder(order_id)
} catch (error) {
throw error
}
}
/**
* Refunds payment for Klarna Order.
* @param {Object} paymentData - payment method data from cart
* @returns {string} id of refunded order
*/
async refundPayment(payment, amountToRefund) {
const { order_id } = payment.data
try {
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${order_id}/refunds`,
{
refunded_amount: amountToRefund,
}
)
return this.retrieveCompletedOrder(order_id)
} catch (error) {
throw error
}
}
/**
* Cancels payment for Klarna Order.
* @param {Object} paymentData - payment method data from cart
* @returns {string} id of cancelled order
*/
async cancelPayment(payment) {
const { order_id } = payment.data
try {
await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${order_id}/cancel`
)
return this.retrieveCompletedOrder(order_id)
} catch (error) {
throw error
}
}
async deletePayment(_) {
return Promise.resolve()
}
}
export default KlarnaProviderService

View File

@@ -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"]
}
}
}

View File

@@ -1,15 +0,0 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock
/dist
/api
/services
/models
/subscribers
/__mocks__

View File

@@ -1,8 +0,0 @@
.DS_store
src
dist
yarn.lock
.babelrc
.turbo
.yarn

View File

@@ -1,147 +0,0 @@
# Change Log
## 1.0.24
### Patch Changes
- [#4276](https://github.com/medusajs/medusa/pull/4276) [`afd1b67f1`](https://github.com/medusajs/medusa/commit/afd1b67f1c7de8cf07fd9fcbdde599a37914e9b5) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Use caret range
## 1.0.23
### Patch Changes
- Updated dependencies [[`121b42acf`](https://github.com/medusajs/medusa/commit/121b42acfe98c12dd593f9b1f2072ff0f3b61724)]:
- medusa-interfaces@1.3.7
## 1.0.23-rc.0
### Patch Changes
- Updated dependencies [[`121b42acf`](https://github.com/medusajs/medusa/commit/121b42acfe98c12dd593f9b1f2072ff0f3b61724)]:
- medusa-interfaces@1.3.7-rc.0
## 1.0.22
### Patch Changes
- [#3217](https://github.com/medusajs/medusa/pull/3217) [`8c5219a31`](https://github.com/medusajs/medusa/commit/8c5219a31ef76ee571fbce84d7d57a63abe56eb0) Thanks [@adrien2p](https://github.com/adrien2p)! - chore: Fix npm packages files included
- Updated dependencies [[`8c5219a31`](https://github.com/medusajs/medusa/commit/8c5219a31ef76ee571fbce84d7d57a63abe56eb0)]:
- medusa-interfaces@1.3.6
## 1.0.21
### Patch Changes
- [#3011](https://github.com/medusajs/medusa/pull/3011) [`ce866475b`](https://github.com/medusajs/medusa/commit/ce866475b4b6c8b453638000f7b1df7a27daf45d) Thanks [@adrien2p](https://github.com/adrien2p)! - chore(_-payment-_): cleanup payment provider plugins
- [#3185](https://github.com/medusajs/medusa/pull/3185) [`08324355a`](https://github.com/medusajs/medusa/commit/08324355a4466b017a0bc7ab1d333ee3cd27b8c4) Thanks [@olivermrbl](https://github.com/olivermrbl)! - chore: Patches all dependencies + minor bumps `winston` to include a [fix for a significant memory leak](https://github.com/winstonjs/winston/pull/2057)
- Updated dependencies [[`08324355a`](https://github.com/medusajs/medusa/commit/08324355a4466b017a0bc7ab1d333ee3cd27b8c4)]:
- medusa-interfaces@1.3.5
## 1.0.20
### Patch Changes
- [#3025](https://github.com/medusajs/medusa/pull/3025) [`93d0dc1bd`](https://github.com/medusajs/medusa/commit/93d0dc1bdcb54cf6e87428a7bb9b0dac196b4de2) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa): test, build and watch scripts
- Updated dependencies [[`93d0dc1bd`](https://github.com/medusajs/medusa/commit/93d0dc1bdcb54cf6e87428a7bb9b0dac196b4de2)]:
- medusa-interfaces@1.3.4
## 1.0.19
### Patch Changes
- [#2979](https://github.com/medusajs/medusa/pull/2979) [`a2df11fc1`](https://github.com/medusajs/medusa/commit/a2df11fc10c5f91d492233fe094a65022d2590aa) Thanks [@adrien2p](https://github.com/adrien2p)! - fix(medusa-payment-manual): Correct wrongly used input data
## 1.0.18
### Patch Changes
- [#2808](https://github.com/medusajs/medusa/pull/2808) [`0a9c89185`](https://github.com/medusajs/medusa/commit/0a9c891853c4d16b553d38268a3408ca1daa71f0) Thanks [@patrick-medusajs](https://github.com/patrick-medusajs)! - chore: explicitly add devDependencies for monorepo peerDependencies
## 1.0.17
### Patch Changes
- [#2254](https://github.com/medusajs/medusa/pull/2254) [`1024ac1a3`](https://github.com/medusajs/medusa/commit/1024ac1a3679180eb9097e33d13776dc6f4b4a75) Thanks [@olivermrbl](https://github.com/olivermrbl)! - fix(medusa-payment-manual): Add missing update method
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.16](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.15...medusa-payment-manual@1.0.16) (2022-01-11)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.15](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.14...medusa-payment-manual@1.0.15) (2021-12-29)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.14](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.13...medusa-payment-manual@1.0.14) (2021-12-17)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.13](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.12...medusa-payment-manual@1.0.13) (2021-12-08)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.12](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.11...medusa-payment-manual@1.0.12) (2021-11-23)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.11](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.10...medusa-payment-manual@1.0.11) (2021-11-22)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.10](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.9...medusa-payment-manual@1.0.10) (2021-11-19)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.9](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.8...medusa-payment-manual@1.0.9) (2021-11-19)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.8](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.7...medusa-payment-manual@1.0.8) (2021-10-18)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.7](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.6...medusa-payment-manual@1.0.7) (2021-10-18)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.6](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.4...medusa-payment-manual@1.0.6) (2021-10-18)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.5](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.4...medusa-payment-manual@1.0.5) (2021-10-18)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.4](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.4...medusa-payment-manual@1.0.4) (2021-09-15)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.3](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.4...medusa-payment-manual@1.0.3) (2021-09-14)
**Note:** Version bump only for package medusa-payment-manual
## [1.0.4](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.2...medusa-payment-manual@1.0.4) (2021-09-02)
### Features
- Update to API references look and feel ([#343](https://github.com/medusajs/medusa/issues/343)) ([143f06a](https://github.com/medusajs/medusa/commit/143f06aa397dcc16991405a6143c22eaa0e3ffd9))
## [1.0.3](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.2...medusa-payment-manual@1.0.3) (2021-08-31)
### Features
- Update to API references look and feel ([#343](https://github.com/medusajs/medusa/issues/343)) ([143f06a](https://github.com/medusajs/medusa/commit/143f06aa397dcc16991405a6143c22eaa0e3ffd9))
## [1.0.2](https://github.com/medusajs/medusa/compare/medusa-payment-manual@1.0.1...medusa-payment-manual@1.0.2) (2021-08-05)
**Note:** Version bump only for package medusa-payment-manual
## 1.0.1 (2021-07-26)
**Note:** Version bump only for package medusa-payment-manual

View File

@@ -1,48 +0,0 @@
# Manual Payment
A minimal payment provider that allows merchants to handle payments manually.
[Medusa Website](https://medusajs.com/) | [Medusa Repository](https://github.com/medusajs/medusa)
## Features
- Provides a restriction-free payment provider that can be used during checkout and while processing orders.
---
## Prerequisites
- [Medusa backend](https://docs.medusajs.com/development/backend/install)
---
## How to Install
1\. Run the following command in the directory of the Medusa backend:
```bash
npm install medusa-payment-manual
```
2\. In `medusa-config.js` add the following at the end of the `plugins` array:
```js
const plugins = [
// ...
`medusa-payment-manual`
]
```
---
## Test the Plugin
1\. Run the following command in the directory of the Medusa backend to run the backend:
```bash
npm run start
```
2\. Enable the payment provider in a region in the admin. You can refer to [this User Guide](https://docs.medusajs.com/user-guide/regions/providers) to learn how to do that. Alternatively, you can use the [Admin APIs](https://docs.medusajs.com/api/admin#tag/Region/operation/PostRegionsRegion).
3\. Place an order using a storefront or the [Store APIs](https://docs.medusajs.com/api/store). You should be able to use Stripe as a payment method.

View File

@@ -1 +0,0 @@
// noop

View File

@@ -1,47 +0,0 @@
{
"name": "medusa-payment-manual",
"version": "1.0.24",
"description": "A dummy payment provider to be used for testing or manual payments",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-payment-manual"
},
"engines": {
"node": ">=16"
},
"author": "Sebastian Rindom",
"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",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-interfaces": "^1.3.7",
"medusa-test-utils": "^1.1.40"
},
"scripts": {
"prepare": "cross-env NODE_ENV=production yarn run build",
"test": "jest --passWithNoTests src",
"build": "babel src --out-dir . --ignore '**/__tests__','**/__mocks__'",
"watch": "babel -w src --out-dir . --ignore '**/__tests__','**/__mocks__'"
},
"peerDependencies": {
"medusa-interfaces": "^1.3.7"
},
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
"keywords": [
"medusa-plugin",
"medusa-plugin-payment"
]
}

View File

@@ -1,104 +0,0 @@
import { PaymentService } from "medusa-interfaces"
class ManualPaymentService extends PaymentService {
static identifier = "manual"
constructor() {
super()
}
/**
* Returns the currently held status.
* @param {object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { status } = paymentData
return status
}
/**
* Creates a manual payment with status "pending"
* @param {object} cart - cart to create a payment for
* @returns {object} an object with staus
*/
async createPayment() {
return { status: "pending" }
}
/**
* Retrieves payment
* @param {object} data - the data of the payment to retrieve
* @returns {Promise<object>} returns data
*/
async retrievePayment(data) {
return data
}
/**
* Updates the payment status to authorized
* @returns {Promise<{ status: string, data: object }>} result with data and status
*/
async authorizePayment() {
return { status: "authorized", data: { status: "authorized" } }
}
/**
* Noop, simply returns existing data.
* @param {object} sessionData - payment session data.
* @returns {object} same data
*/
async updatePayment(sessionData) {
return sessionData
}
/**
* @param {object} sessionData - payment session data.
* @param {object} update - payment session update data.
* @returns {object} existing data merged with update data
*/
async updatePaymentData(sessionData, update) {
return { ...sessionData, ...update.data }
}
async deletePayment() {
return
}
/**
* Updates the payment status to captured
* @param {object} paymentData - payment method data from cart
* @returns {object} object with updated status
*/
async capturePayment() {
return { status: "captured" }
}
/**
* Returns the data currently held in a status
* @param {object} session - payment method data from cart
* @returns {object} the current data
*/
async getPaymentData(session) {
return session.data
}
/**
* Noop, resolves to allow manual refunds.
* @param {object} payment - payment method data from cart
* @returns {string} same data
*/
async refundPayment(payment) {
return payment.data
}
/**
* Updates the payment status to cancled
* @returns {object} object with canceled status
*/
async cancelPayment() {
return { status: "canceled" }
}
}
export default ManualPaymentService

View File

@@ -1,4 +0,0 @@
dist
node_modules
.DS_store
yarn.lock

View File

@@ -1,11 +0,0 @@
{
"jsc": {
"target": "es5",
"parser": {
"syntax": "typescript"
}
},
"module": {
"type": "commonjs"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,76 +0,0 @@
# PayPal
Receive payments on your Medusa commerce application using PayPal.
[PayPal Plugin Documentation](https://docs.medusajs.com/plugins/payment/paypal) | [Medusa Website](https://medusajs.com/) | [Medusa Repository](https://github.com/medusajs/medusa)
## Features
- Authorize payments on orders from any sales channel.
- Capture payments from the admin dashboard.
- View payment analytics through PayPal's dashboard.
- Ready-integration with [Medusa's Next.js starter storefront](https://docs.medusajs.com/starters/nextjs-medusa-starter).
- Support for Webhooks.
---
## Prerequisites
- [Medusa backend](https://docs.medusajs.com/development/backend/install)
- [PayPal account](https://www.paypal.com)
---
## How to Install
1\. Run the following command in the directory of the Medusa backend:
```bash
npm install medusa-payment-paypal
```
2\. Set the following environment variables in `.env`:
```bash
PAYPAL_SANDBOX=true
PAYPAL_CLIENT_ID=<CLIENT_ID>
PAYPAL_CLIENT_SECRET=<CLIENT_SECRET>
PAYPAL_AUTH_WEBHOOK_ID=<WEBHOOK_ID>
```
3\. In `medusa-config.js` add the following at the end of the `plugins` array:
```js
const plugins = [
// ...
{
resolve: `medusa-payment-paypal`,
options: {
sandbox: process.env.PAYPAL_SANDBOX,
client_id: process.env.PAYPAL_CLIENT_ID,
client_secret: process.env.PAYPAL_CLIENT_SECRET,
auth_webhook_id: process.env.PAYPAL_AUTH_WEBHOOK_ID,
},
},
]
```
---
## Test the Plugin
1\. Run the following command in the directory of the Medusa backend to run the backend:
```bash
npm run start
```
2\. Enable PayPal in a region in the admin. You can refer to [this User Guide](https://docs.medusajs.com/user-guide/regions/providers) to learn how to do that. Alternatively, you can use the [Admin APIs](https://docs.medusajs.com/api/admin#tag/Region/operation/PostRegionsRegion).
3\. Place an order using a storefront or the [Store APIs](https://docs.medusajs.com/api/store). You should be able to use Stripe as a payment method.
---
## Additional Resources
- [PayPal Plugin Documentation](https://docs.medusajs.com/plugins/payment/paypal)

View File

@@ -1,8 +0,0 @@
module.exports = {
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
transformIgnorePatterns: ["/node_modules/(?!(axios)/).*", "/dist"],
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
}

View File

@@ -1,51 +0,0 @@
{
"name": "medusa-payment-paypal",
"version": "6.0.3",
"description": "Paypal Payment provider for Medusa Commerce",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-payment-paypal"
},
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"test": "jest --silent --bail --maxWorkers=50% --forceExit",
"build": "rimraf dist && tsc",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/medusa": "^1.20.4",
"@medusajs/types": "^1.11.15",
"@swc/core": "^1.4.8",
"@swc/jest": "^0.2.36",
"@types/stripe": "^8.0.417",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"rimraf": "^5.0.1",
"typescript": "^4.4.4"
},
"peerDependencies": {
"@medusajs/medusa": "^1.12.0"
},
"dependencies": {
"@paypal/checkout-server-sdk": "^1.0.3",
"axios": "^1.3.4",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"medusa-core-utils": "^1.2.0"
},
"gitHead": "cd1f5afa5aa8c0b15ea957008ee19f1d695cbd2e",
"keywords": [
"medusa-plugin",
"medusa-plugin-payment"
]
}

View File

@@ -1,34 +0,0 @@
import { INVOICE_ID } from "../__mocks__/@paypal/checkout-server-sdk"
export const PaymentIntentDataByStatus = {
CREATED: {
id: "CREATED",
status: "CREATED",
invoice_id: INVOICE_ID,
},
COMPLETED: {
id: "COMPLETED",
status: "COMPLETED",
invoice_id: INVOICE_ID,
},
SAVED: {
id: "SAVED",
status: "SAVED",
invoice_id: INVOICE_ID,
},
APPROVED: {
id: "APPROVED",
status: "APPROVED",
invoice_id: INVOICE_ID,
},
PAYER_ACTION_REQUIRED: {
id: "PAYER_ACTION_REQUIRED",
status: "PAYER_ACTION_REQUIRED",
invoice_id: INVOICE_ID,
},
VOIDED: {
id: "VOIDED",
status: "VOIDED",
invoice_id: INVOICE_ID,
},
}

View File

@@ -1,120 +0,0 @@
import { PaymentIntentDataByStatus } from "../../__fixtures__/data";
export const SUCCESS_INTENT = "right@test.fr"
export const FAIL_INTENT_ID = "unknown"
export const INVOICE_ID = "invoice_id"
export const PayPalClientMock = {
execute: jest.fn().mockImplementation((r) => {
return {
result: r.result,
}
}),
}
export const PayPalMock = {
core: {
env: {},
SandboxEnvironment: function () {
this.env = {
sandbox: true,
live: false,
}
},
LiveEnvironment: function () {
this.env = {
sandbox: false,
live: true,
}
},
PayPalHttpClient: function () {
return PayPalClientMock
},
},
payments: {
AuthorizationsGetRequest: jest.fn().mockImplementation(() => {}),
AuthorizationsVoidRequest: jest.fn().mockImplementation(() => {
return {
status: "VOIDED"
}
}),
AuthorizationsCaptureRequest: jest.fn().mockImplementation((id) => {
if (id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
return {
result: {
id: "test",
},
capture: true,
}
}),
CapturesRefundRequest: jest.fn().mockImplementation((paymentId) => {
if (paymentId === FAIL_INTENT_ID) {
throw new Error("Error")
}
return {
result: {
id: "test",
},
status: "COMPLETED",
invoice_id: INVOICE_ID,
body: null,
requestBody: function (d) {
this.body = d
},
}
}),
},
orders: {
OrdersCreateRequest: jest.fn().mockImplementation(() => {
return {
result: {
id: "test",
},
order: true,
body: null,
requestBody: function (d) {
if (d.purchase_units[0].custom_id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
this.body = d
}
}
}),
OrdersPatchRequest: jest.fn().mockImplementation((id) => {
if (id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
return {
result: {
id: "test",
},
order: true,
body: null,
requestBody: function (d) {
this.body = d
},
}
}),
OrdersGetRequest: jest.fn().mockImplementation((paymentId) => {
if (paymentId === FAIL_INTENT_ID) {
throw new Error("Error")
}
return {
result: Object.values(PaymentIntentDataByStatus).find(value => {
return value.id === paymentId
}) ?? {}
}
}),
},
}
export default PayPalMock

View File

@@ -1,10 +0,0 @@
import { Router } from "express"
import hooks from "./routes/hooks"
export default () => {
const app = Router()
hooks(app)
return app
}

View File

@@ -1,15 +0,0 @@
import { Router } from "express"
import bodyParser from "body-parser"
import { wrapHandler } from "@medusajs/medusa"
import paypalWebhookHandler from "./paypal"
const route = Router()
export default (app) => {
app.use("/paypal/hooks", route)
route.use(bodyParser.json())
route.post("/", wrapHandler(paypalWebhookHandler))
return app
}

View File

@@ -1,120 +0,0 @@
import PaypalProvider from "../../../services/paypal-provider"
export default async (req, res) => {
const auth_algo = req.headers["paypal-auth-algo"]
const cert_url = req.headers["paypal-cert-url"]
const transmission_id = req.headers["paypal-transmission-id"]
const transmission_sig = req.headers["paypal-transmission-sig"]
const transmission_time = req.headers["paypal-transmission-time"]
const paypalService: PaypalProvider = req.scope.resolve(
"paypalProviderService"
)
try {
await paypalService.verifyWebhook({
auth_algo,
cert_url,
transmission_id,
transmission_sig,
transmission_time,
webhook_event: req.body,
})
} catch (err) {
res.sendStatus(401)
return
}
function isPaymentCollection(id) {
return id && id.startsWith("paycol")
}
async function autorizeCart(req, cartId) {
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService")
const swapService = req.scope.resolve("swapService")
const orderService = req.scope.resolve("orderService")
await manager.transaction(async (m) => {
const cart = await cartService.withTransaction(m).retrieve(cartId)
switch (cart.type) {
case "swap": {
const swap = await swapService
.withTransaction(m)
.retrieveByCartId(cartId)
.catch((_) => undefined)
if (swap && swap.confirmed_at === null) {
await cartService
.withTransaction(m)
.setPaymentSession(cartId, "paypal")
await cartService.withTransaction(m).authorizePayment(cartId)
await swapService.withTransaction(m).registerCartCompletion(swap.id)
}
break
}
default: {
const order = await orderService
.withTransaction(m)
.retrieveByCartId(cartId)
.catch((_) => undefined)
if (!order) {
await cartService
.withTransaction(m)
.setPaymentSession(cartId, "paypal")
await cartService.withTransaction(m).authorizePayment(cartId)
await orderService.withTransaction(m).createFromCart(cartId)
}
break
}
}
})
}
async function autorizePaymentCollection(req, id, orderId) {
const manager = req.scope.resolve("manager")
const paymentCollectionService = req.scope.resolve(
"paymentCollectionService"
)
await manager.transaction(async (manager) => {
await paymentCollectionService.withTransaction(manager).authorize(id)
})
}
try {
const body = req.body
const authId = body.resource.id
const auth = await paypalService.retrieveAuthorization(authId)
const order = await paypalService.retrieveOrderFromAuth(auth)
if (!order) {
res.sendStatus(200)
return
}
const purchaseUnit = order.purchase_units[0]
const customId = purchaseUnit.custom_id
if (!customId) {
res.sendStatus(200)
return
}
if (isPaymentCollection(customId)) {
const orderId = order.id
await autorizePaymentCollection(req, customId, orderId)
} else {
await autorizeCart(req, customId)
}
res.sendStatus(200)
} catch (err) {
console.error(err)
res.sendStatus(409)
}
}

View File

@@ -1,62 +0,0 @@
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
export const SUCCESS_INTENT = "right@test.fr"
export const FAIL_INTENT_ID = "unknown"
export const INVOICE_ID = "invoice_id"
export const PayPalMock = {
cancelAuthorizedPayment: jest.fn().mockImplementation(() => {
return {
status: "VOIDED"
}
}),
captureAuthorizedPayment: jest.fn().mockImplementation((id) => {
if (id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
return {
id: "test",
capture: true,
}
}),
refundPayment: jest.fn().mockImplementation((paymentId) => {
if (paymentId === FAIL_INTENT_ID) {
throw new Error("Error")
}
return undefined
}),
createOrder: jest.fn().mockImplementation((d) => {
if (d.purchase_units[0].custom_id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
return d
}),
patchOrder: jest.fn().mockImplementation((id) => {
if (id === FAIL_INTENT_ID) {
throw new Error("Error.")
}
return {
id: "test",
order: true,
body: null,
requestBody: function (d) {
this.body = d
},
}
}),
getOrder: jest.fn().mockImplementation((paymentId) => {
if (paymentId === FAIL_INTENT_ID) {
throw new Error("Error")
}
return Object.values(PaymentIntentDataByStatus).find(value => {
return value.id === paymentId
}) ?? {}
}),
}
export default PayPalMock

View File

@@ -1,238 +0,0 @@
import axios from "axios"
import { PaypalHttpClient } from "../paypal-http-client"
import { PaypalApiPath } from "../types"
jest.mock("axios")
const mockedAxios = axios as jest.Mocked<typeof axios>
const accessToken = "accessToken"
const responseData = { test: "test" }
const options = {
clientId: "fake",
clientSecret: "fake",
logger: {
error: jest.fn(),
} as any,
sandbox: true,
} as any
describe("PaypalHttpClient", function () {
let paypalHttpClient: PaypalHttpClient
beforeAll(() => {
mockedAxios.create.mockReturnThis()
paypalHttpClient = new PaypalHttpClient(options)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
mockedAxios.request.mockResolvedValue(Promise.resolve("resolve"))
const argument = { url: PaypalApiPath.CREATE_ORDER }
await paypalHttpClient.request(argument)
expect(mockedAxios.request).toHaveBeenCalledTimes(1)
expect(mockedAxios.request).toHaveBeenCalledWith(
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
undefined,
0
)
})
it("should fail and retry after authentication until reaches the maximum number of attempts", async () => {
mockedAxios.request.mockImplementation((async (
config,
originalConfig,
retryCount = 0
) => {
if (retryCount <= 2) {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({ response: { status: 401 } })
}
return { status: 200, data: responseData }
}) as any)
const argument = { url: PaypalApiPath.CREATE_ORDER }
await paypalHttpClient.request(argument).catch((e) => e)
expect(mockedAxios.request).toHaveBeenCalledTimes(3)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
undefined,
0
)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
method: "POST",
url: PaypalApiPath.AUTH,
auth: { password: options.clientId, username: options.clientSecret },
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
grant_type: "client_credentials",
},
}),
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
1
)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
method: "POST",
url: PaypalApiPath.AUTH,
auth: { password: options.clientId, username: options.clientSecret },
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
grant_type: "client_credentials",
},
}),
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
2
)
})
it("should fail and retry after authentication and then succeed", async () => {
mockedAxios.request.mockImplementation((async (
config,
originalConfig,
retryCount = 0
) => {
if (retryCount >= 2 && config.url === PaypalApiPath.AUTH) {
return {
data: {
access_token: accessToken,
},
}
}
if (retryCount < 2) {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({ response: { status: 401 } })
}
return { status: 200, data: responseData }
}) as any)
const argument = { url: PaypalApiPath.CREATE_ORDER }
await paypalHttpClient.request(argument).catch((e) => e)
expect(mockedAxios.request).toHaveBeenCalledTimes(4)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
undefined,
0
)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
method: "POST",
url: PaypalApiPath.AUTH,
auth: { password: options.clientId, username: options.clientSecret },
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
grant_type: "client_credentials",
},
}),
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
1
)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
3,
expect.objectContaining({
method: "POST",
url: PaypalApiPath.AUTH,
auth: { password: options.clientId, username: options.clientSecret },
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
grant_type: "client_credentials",
},
}),
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer undefined`,
},
}),
2
)
expect(mockedAxios.request).toHaveBeenNthCalledWith(
4,
expect.objectContaining({
method: "POST",
url: argument.url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`,
},
})
)
})
})

View File

@@ -1,2 +0,0 @@
export * from "./paypal-sdk"
export * from "./types"

View File

@@ -1,142 +0,0 @@
import { Logger } from "@medusajs/medusa"
import axios, { AxiosInstance, AxiosRequestConfig, Method } from "axios"
import {
PaypalApiPath,
PaypalEnvironmentPaths,
PaypalSdkOptions,
} from "./types"
const MAX_ATTEMPTS = 2
export class PaypalHttpClient {
protected readonly baseUrl_: string = PaypalEnvironmentPaths.LIVE
protected readonly httpClient_: AxiosInstance
protected readonly options_: PaypalSdkOptions
protected readonly logger_?: Logger
protected accessToken_: string
constructor(options: PaypalSdkOptions) {
this.options_ = options
this.logger_ = options.logger
if (options.sandbox) {
this.baseUrl_ = PaypalEnvironmentPaths.SANDBOX
}
const axiosInstance = axios.create({
baseURL: this.baseUrl_,
})
this.httpClient_ = new Proxy(axiosInstance, {
// Handle automatic retry mechanism
get: (target, prop) => {
return this.retryIfNecessary(target[prop].bind(target))
},
})
}
/**
* Run a request and return the result
* @param url
* @param data
* @param method
* @protected
*/
async request<T, TResponse>({
url,
data,
method,
}: {
url: string
data?: T
method?: Method
}): Promise<TResponse> {
return await this.httpClient_.request({
method: method ?? "POST",
url,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.accessToken_}`,
},
data,
})
}
/**
* Will run the original method and retry it if an unauthorized error is returned
* @param originalMethod
* @protected
*/
protected retryIfNecessary<T = unknown>(originalMethod: Function) {
return async (config, originalConfig, retryCount = 0) => {
if (retryCount > MAX_ATTEMPTS) {
throw new Error(
`An error occurred while requesting Paypal API after ${MAX_ATTEMPTS} attempts`
)
}
return await originalMethod
.apply(this.httpClient_, [config, originalConfig, retryCount])
.then((res) => res.data)
.catch(async (err) => {
if (err.response?.status === 401) {
++retryCount
if (!originalConfig) {
originalConfig = config
}
await this.authenticate(originalConfig, retryCount)
config = {
...(originalConfig ?? {}),
headers: {
...(originalConfig?.headers ?? {}),
Authorization: `Bearer ${this.accessToken_}`,
},
}
return await originalMethod
.apply(this.httpClient_, [config])
.then((res) => res.data)
}
this.logger_?.error(err.response.message)
throw err
})
}
}
/**
* Authenticate and store the access token
* @protected
*/
protected async authenticate(
originalConfig?: AxiosRequestConfig,
retryCount = 0
) {
const res: { access_token: string } = await (
this.httpClient_.request as any
)(
{
method: "POST",
url: PaypalApiPath.AUTH,
auth: {
username: this.options_.clientId ?? this.options_.client_id,
password: this.options_.clientSecret ?? this.options_.client_secret,
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: {
grant_type: "client_credentials",
},
},
originalConfig,
retryCount
)
this.accessToken_ = res.access_token
}
}

View File

@@ -1,127 +0,0 @@
import {
CreateOrder,
CreateOrderResponse,
GetOrderResponse,
PatchOrder,
PaypalApiPath,
PaypalSdkOptions,
} from "./types"
import {
CaptureAuthorizedPayment,
CapturesAuthorizationResponse,
CapturesRefundResponse,
GetAuthorizationPaymentResponse,
RefundPayment,
} from "./types/payment"
import { PaypalHttpClient } from "./paypal-http-client"
import { VerifyWebhookSignature } from "./types/webhook"
export class PaypalSdk {
protected readonly httpClient_: PaypalHttpClient
constructor(options: PaypalSdkOptions) {
this.httpClient_ = new PaypalHttpClient(options)
}
/**
* Create a new order.
* @param data
*/
async createOrder(data: CreateOrder): Promise<CreateOrderResponse> {
const url = PaypalApiPath.CREATE_ORDER
return await this.httpClient_.request({ url, data })
}
/**
* Retrieve an order.
* @param orderId
*/
async getOrder(orderId: string): Promise<GetOrderResponse> {
const url = PaypalApiPath.GET_ORDER.replace("{id}", orderId)
return await this.httpClient_.request({ url, method: "GET" })
}
/**
* Patch an order.
* @param orderId
* @param data
*/
async patchOrder(orderId: string, data?: PatchOrder[]): Promise<void> {
const url = PaypalApiPath.PATCH_ORDER.replace("{id}", orderId)
return await this.httpClient_.request({ url, method: "PATCH" })
}
/**
* Authorizes payment for an order. To successfully authorize payment for an order,
* the buyer must first approve the order or a valid payment_source must be provided in the request.
* A buyer can approve the order upon being redirected to the rel:approve URL that was returned in the HATEOAS links in the create order response.
* @param orderId
*/
async authorizeOrder(orderId: string): Promise<CreateOrderResponse> {
const url = PaypalApiPath.AUTHORIZE_ORDER.replace("{id}", orderId)
return await this.httpClient_.request({ url })
}
/**
* Refunds a captured payment, by ID. For a full refund, include an empty
* payload in the JSON request body. For a partial refund, include an amount
* object in the JSON request body.
* @param paymentId
* @param data
*/
async refundPayment(
paymentId: string,
data?: RefundPayment
): Promise<CapturesRefundResponse> {
const url = PaypalApiPath.CAPTURE_REFUND.replace("{id}", paymentId)
return await this.httpClient_.request({ url, data })
}
/**
* Voids, or cancels, an authorized payment, by ID. You cannot void an authorized payment that has been fully captured.
* @param authorizationId
*/
async cancelAuthorizedPayment(authorizationId: string): Promise<void> {
const url = PaypalApiPath.AUTHORIZATION_VOID.replace(
"{id}",
authorizationId
)
return await this.httpClient_.request({ url })
}
/**
* Captures an authorized payment, by ID.
* @param authorizationId
* @param data
*/
async captureAuthorizedPayment(
authorizationId: string,
data?: CaptureAuthorizedPayment
): Promise<CapturesAuthorizationResponse> {
const url = PaypalApiPath.AUTHORIZATION_CAPTURE.replace(
"{id}",
authorizationId
)
return await this.httpClient_.request({ url, data })
}
/**
* Captures an authorized payment, by ID.
* @param authorizationId
*/
async getAuthorizationPayment(
authorizationId: string
): Promise<GetAuthorizationPaymentResponse> {
const url = PaypalApiPath.AUTHORIZATION_GET.replace("{id}", authorizationId)
return await this.httpClient_.request({ url })
}
async verifyWebhook(data: VerifyWebhookSignature) {
const url = PaypalApiPath.VERIFY_WEBHOOK_SIGNATURE
return await this.httpClient_.request({ url, data })
}
}

View File

@@ -1,178 +0,0 @@
export type Links = { href: string; rel: string; method: string }[]
export interface Address {
country_code: string
address_line_1?: string
address_line_2?: string
admin_area_1?: string
admin_area_2?: string
postal_code?: string
}
export interface MoneyAmount {
value: string | number
currency_code: string
}
export interface MoneyBreakdown {
discount: MoneyAmount
handling: MoneyAmount
insurance: MoneyAmount
item_total: MoneyAmount
shipping: MoneyAmount
shipping_discount: MoneyAmount
tax_total: MoneyAmount
}
export interface PaymentInstruction {
disbursement_mode: "INSTANT" | "DELAYED"
payee_pricing_tier_id: string
payee_receivable_fx_rate_id: string
platform_fees: Array<PlatformFee>
}
export interface Payee {
email_address: string
merchant_id: string
}
export interface PlatformFee {
amount: MoneyAmount
payee: Payee
}
export interface PurchaseUnitItem {
name: string
quantity: number
unit_amount: MoneyAmount
category?: "DIGITAL_GOODS" | "PHYSICAL_GOODS" | "DONATION"
description?: string
sku?: string
tax?: MoneyAmount
}
export interface PurchaseUnit {
amount: MoneyAmount | MoneyBreakdown
custom_id?: string
description?: string
invoice_id?: string
items?: Array<PurchaseUnitItem>
payee?: Payee
payment_instruction?: PaymentInstruction
reference_id?: string
shipping?: {
address?: Address
name?: { full_name: string }
type?: "SHIPPING" | "PICKUP_IN_PERSON"
}
soft_descriptor?: string
}
export interface ExperienceContext {
brand_name?: string
cancel_url?: string
locale?: string
return_url?: string
shipping_preference?: "GET_FROM_FILE" | "NO_SHIPPING" | "SET_PROVIDED_ADDRESS"
}
export interface PaymentSourceBase {
name: string
country_name: string
experience_context?: ExperienceContext
}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface Bancontact extends PaymentSourceBase {}
export interface Blik extends PaymentSourceBase {
email?: string
}
export interface Card {
billing_address?: Address
expiry?: string
name?: string
number?: string
security_code?: string
stored_credential?: {
payment_initiator: "CUSTOMER" | "MERCHANT"
payment_type: "ONE_TIME" | "RECURRING" | "UNSCHEDULED"
previous_network_transaction_reference?: {
id: string
network:
| "VISA"
| "MASTERCARD"
| "DISCOVER"
| "AMEX"
| "SOLO"
| "JCB"
| "STAR"
| "DELTA"
| "SWITCH"
| "MAESTRO"
| "CB_NATIONALE"
| "CONFIGOGA"
| "CONFIDIS"
| "ELECTRON"
| "CETELEM"
| "CHINA_UNION_PAY"
date?: string
}
usageenum?: "FIRST" | "SUBSEQUENT" | "DERIVED"
}
vault_id?: string
}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface EPS extends PaymentSourceBase {}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface Giropay extends PaymentSourceBase {}
export interface Ideal extends PaymentSourceBase {
bic?: string
}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface MyBank extends PaymentSourceBase {}
export interface P24 extends PaymentSourceBase {
email: string
}
export interface Paypal {
address?: Address
birth_date?: string
email_address?: string
experience_context?: ExperienceContext
}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface Sofort extends PaymentSourceBase {}
export interface Token {
id: string
type: "BILLING_AGREEMENT"
}
export interface Trustly {
bic?: string
country_code?: string
iban_last_chars?: string
name?: string
}
export interface PaymentSource {
bancontact?: Bancontact
blik?: Blik
card?: Card
eps?: EPS
ideal?: Ideal
myBank?: MyBank
p24?: P24
paypal?: Paypal
sofort?: Sofort
token?: Token
trustly?: Trustly
}

View File

@@ -1,17 +0,0 @@
export const PaypalEnvironmentPaths = {
SANDBOX: "https://api-m.sandbox.paypal.com",
LIVE: "https://api-m.paypal.com",
}
export const PaypalApiPath = {
AUTH: "/v1/oauth2/token",
GET_ORDER: "/v2/checkout/orders/{id}",
PATCH_ORDER: "/v2/checkout/orders/{id}",
CREATE_ORDER: "/v2/checkout/orders",
AUTHORIZE_ORDER: "/v2/checkout/orders/{id}/authorize",
CAPTURE_REFUND: "/v2/payments/captures/{id}/refund",
AUTHORIZATION_GET: "/v2/payments/authorizations/{id}",
AUTHORIZATION_CAPTURE: "/v2/payments/authorizations/{id}/capture",
AUTHORIZATION_VOID: "/v2/payments/authorizations/{id}/void",
VERIFY_WEBHOOK_SIGNATURE: "/v1/notifications/verify-webhook-signature",
}

View File

@@ -1,10 +0,0 @@
import { Logger } from "@medusajs/medusa"
import { PaypalOptions } from "../../types"
export type PaypalSdkOptions = PaypalOptions & {
logger?: Logger
}
export * from "./common"
export * from "./order"
export * from "./constant"

View File

@@ -1,40 +0,0 @@
import { Links, PaymentSource, PurchaseUnit } from "./common"
export interface CreateOrder {
intent: "CAPTURE" | "AUTHORIZE"
purchase_units: Array<PurchaseUnit>
payment_source?: PaymentSource
}
export interface CreateOrderResponse {
id: string
status:
| "CREATED"
| "SAVED"
| "APPROVED"
| "VOIDED"
| "COMPLETED"
| "PAYER_ACTION_REQUIRED"
payment_source?: PaymentSource
links?: Links
intent?: CreateOrder["intent"]
processing_instruction?:
| "ORDER_COMPLETE_ON_PAYMENT_APPROVAL"
| "NO_INSTRUCTION"
purchase_units: Array<PurchaseUnit>
create_time?: string
update_time?: string
}
/* eslint @typescript-eslint/no-empty-interface: "off" */
export interface GetOrderResponse extends CreateOrderResponse {}
export interface PatchOrder {
op: "replace" | "add" | "remove"
path: string
value:
| CreateOrder["intent"]
| PurchaseUnit
| { client_configuration?: any }
| Record<string, unknown>
}

View File

@@ -1,76 +0,0 @@
import { Links, MoneyAmount, PaymentInstruction } from "./common"
export interface RefundPayment {
amount?: MoneyAmount
invoice_id?: string
note_to_payer?: string
payment_instruction?: PaymentInstruction
}
export interface CapturesRefundResponse {
id: string
status: "CANCELLED" | "FAILED" | "PENDING" | "COMPLETED"
status_details?: any
amount?: MoneyAmount
note_to_payer?: string
seller_payable_breakdown?: any
invoice_id?: string
create_time?: string
update_time?: string
links?: Links
}
export interface CaptureAuthorizedPayment {
amount?: MoneyAmount
final_capture?: boolean
invoice_id?: string
note_to_payer?: string
payment_instruction?: PaymentInstruction
soft_descriptor?: string
}
export interface CapturesAuthorizationResponse {
id: string
status:
| "COMPLETED"
| "DECLINED"
| "PARTIALLY_REFUNDED"
| "PENDING"
| "REFUNDED"
| "FAILED"
status_details?: any
amount?: MoneyAmount
created_time?: string
update_time?: string
custom_id?: string
disbursement_mode?: "INSTANT" | "DELAYED"
final_capture?: boolean
invoice_id?: string
links?: Links
processor_response?: any
seller_protection?: any
seller_receivable_breakdown?: any
supplementary_data?: any
}
export interface GetAuthorizationPaymentResponse {
amount?: MoneyAmount
create_time?: string
custom_id?: string
expiration_time?: string
id?: string
invoice_id?: string
links?: Links
seller_protection?: any
status?:
| "CREATED"
| "DENIED"
| "CAPTURED"
| "VOIDED"
| "EXPIRED"
| "PARTIALLY_CAPTURED"
| "PENDING"
status_details?: any
supplementary_data?: any
update_time?: string
}

View File

@@ -1,22 +0,0 @@
import { Links } from "./common"
export interface WebhookEvent {
id?: string
create_time?: string
event_version?: string
links?: Links
resource?: any
resource_type?: string
resource_version?: string
summary?: string
}
export interface VerifyWebhookSignature {
auth_algo: string
cert_url: string
transmission_id: string
transmission_sig: string
transmission_time: string
webhook_event: WebhookEvent
webhook_id: string
}

View File

@@ -1,2 +0,0 @@
export * from "./types"
export * from "./services/paypal-provider"

View File

@@ -1,233 +0,0 @@
import {
FAIL_INTENT_ID,
SUCCESS_INTENT,
} from "../../__mocks__/@paypal/checkout-server-sdk"
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
// INITIATE PAYMENT DATA
export const initiatePaymentContextSuccess = {
currency_code: "usd",
amount: 1000,
resource_id: SUCCESS_INTENT,
customer: {},
context: {},
paymentSessionData: {},
}
export const initiatePaymentContextFail = {
currency_code: "usd",
amount: 1000,
resource_id: FAIL_INTENT_ID,
customer: {
metadata: {
stripe_id: "test",
},
},
context: {},
paymentSessionData: {},
}
// AUTHORIZE PAYMENT DATA
export const authorizePaymentSuccessData = {
id: PaymentIntentDataByStatus.COMPLETED.id,
}
// CANCEL PAYMENT DATA
export const cancelPaymentSuccessData = {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
payments: {
authorizations: [
{
id: "id",
},
],
},
},
],
}
export const cancelPaymentRefundAlreadyCaptureSuccessData = {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
payments: {
captures: [
{
id: "id",
},
],
authorizations: [
{
id: "id",
},
],
},
},
],
}
export const cancelPaymentRefundAlreadyCanceledSuccessData = {
id: PaymentIntentDataByStatus.VOIDED.id,
}
export const cancelPaymentFailData = {
id: FAIL_INTENT_ID,
purchase_units: [
{
payments: {
captures: [
{
id: "id",
},
],
authorizations: [
{
id: "id",
},
],
},
},
],
}
// CAPTURE PAYMENT DATA
export const capturePaymentContextSuccessData = {
paymentSessionData: {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
payments: {
authorizations: [
{
id: SUCCESS_INTENT,
},
],
},
},
],
},
}
export const capturePaymentContextFailData = {
paymentSessionData: {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
payments: {
authorizations: [
{
id: FAIL_INTENT_ID,
},
],
},
},
],
},
}
// REFUND PAYMENT DATA
export const refundPaymentSuccessData = {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
amount: {
currency_code: "USD",
value: "100.00",
},
payments: {
captures: [
{
id: "id",
},
],
authorizations: [
{
id: FAIL_INTENT_ID,
},
],
},
},
],
}
export const refundPaymentFailNotYetCapturedData = {
id: PaymentIntentDataByStatus.APPROVED.id,
purchase_units: [
{
payments: {
captures: [],
authorizations: [
{
id: FAIL_INTENT_ID,
},
],
},
},
],
}
export const refundPaymentFailData = {
id: FAIL_INTENT_ID,
purchase_units: [
{
amount: {
currency_code: "USD",
value: "100.00",
},
payments: {
captures: [
{
id: "id",
},
],
authorizations: [
{
id: FAIL_INTENT_ID,
},
],
},
},
],
}
// RETRIEVE PAYMENT DATA
export const retrievePaymentSuccessData = {
id: PaymentIntentDataByStatus.APPROVED.id,
}
export const retrievePaymentFailData = {
id: FAIL_INTENT_ID,
}
// UPDATE PAYMENT DATA
export const updatePaymentSuccessData = {
paymentSessionData: {
id: PaymentIntentDataByStatus.APPROVED.id,
},
currency_code: "USD",
amount: 1000,
}
export const updatePaymentFailData = {
currency_code: "USD",
amount: 1000,
resource_id: FAIL_INTENT_ID,
customer: {
metadata: {
stripe_id: "test",
},
},
context: {},
paymentSessionData: {
id: FAIL_INTENT_ID,
},
}

View File

@@ -1,500 +0,0 @@
import { PaymentIntentDataByStatus } from "../../__fixtures__/data"
import { PaymentProcessorContext, PaymentSessionStatus } from "@medusajs/medusa"
import PaypalProvider from "../paypal-provider"
import {
authorizePaymentSuccessData,
cancelPaymentFailData,
cancelPaymentRefundAlreadyCanceledSuccessData,
cancelPaymentRefundAlreadyCaptureSuccessData,
cancelPaymentSuccessData,
capturePaymentContextFailData,
capturePaymentContextSuccessData,
initiatePaymentContextFail,
initiatePaymentContextSuccess,
refundPaymentFailData,
refundPaymentFailNotYetCapturedData,
refundPaymentSuccessData,
retrievePaymentFailData,
retrievePaymentSuccessData,
updatePaymentFailData,
updatePaymentSuccessData,
} from "../__fixtures__/data"
import axios from "axios"
import { INVOICE_ID, PayPalMock } from "../../core/__mocks__/paypal-sdk"
import { roundToTwo } from "../utils/utils"
import { humanizeAmount } from "medusa-core-utils"
jest.mock("axios")
const mockedAxios = axios as jest.Mocked<typeof axios>
jest.mock("../../core", () => {
return {
PaypalSdk: jest.fn().mockImplementation(() => PayPalMock),
}
})
const container = {
logger: {
error: jest.fn(),
} as any,
}
const paypalConfig = {
sandbox: true,
client_id: "fake",
client_secret: "fake",
}
describe("PaypalProvider", () => {
beforeAll(() => {
mockedAxios.create.mockReturnThis()
})
describe("getPaymentStatus", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should return the correct status", async () => {
let status: PaymentSessionStatus
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.CREATED.id,
})
expect(status).toBe(PaymentSessionStatus.PENDING)
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.SAVED.id,
})
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.APPROVED.id,
})
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.PAYER_ACTION_REQUIRED.id,
})
expect(status).toBe(PaymentSessionStatus.REQUIRES_MORE)
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.VOIDED.id,
})
expect(status).toBe(PaymentSessionStatus.CANCELED)
status = await paypalProvider.getPaymentStatus({
id: PaymentIntentDataByStatus.COMPLETED.id,
})
expect(status).toBe(PaymentSessionStatus.AUTHORIZED)
status = await paypalProvider.getPaymentStatus({
id: "unknown-id",
})
expect(status).toBe(PaymentSessionStatus.PENDING)
})
})
describe("initiatePayment", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed with an existing customer but no stripe id", async () => {
const result = await paypalProvider.initiatePayment(
initiatePaymentContextSuccess as PaymentProcessorContext
)
expect(PayPalMock.createOrder).toHaveBeenCalled()
expect(PayPalMock.createOrder).toHaveBeenCalledWith(
expect.objectContaining({
intent: "AUTHORIZE",
purchase_units: [
{
custom_id: initiatePaymentContextSuccess.resource_id,
amount: {
currency_code:
initiatePaymentContextSuccess.currency_code.toUpperCase(),
value: roundToTwo(
humanizeAmount(
initiatePaymentContextSuccess.amount,
initiatePaymentContextSuccess.currency_code
),
initiatePaymentContextSuccess.currency_code
),
},
},
],
})
)
expect(result).toEqual({
session_data: expect.any(Object),
})
})
it("should fail", async () => {
const result = await paypalProvider.initiatePayment(
initiatePaymentContextFail as unknown as PaymentProcessorContext
)
expect(PayPalMock.createOrder).toHaveBeenCalled()
expect(result).toEqual({
error: "An error occurred in initiatePayment",
code: "",
detail: "Error.",
})
})
})
describe("authorizePayment", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await paypalProvider.authorizePayment(
authorizePaymentSuccessData as Record<string, unknown>,
{}
)
expect(result).toEqual({
data: {
id: authorizePaymentSuccessData.id,
invoice_id: INVOICE_ID,
status:
PaymentIntentDataByStatus[authorizePaymentSuccessData.id].status,
},
status: "authorized",
})
})
})
describe("cancelPayment", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed to void the payment authorization", async () => {
const result = await paypalProvider.cancelPayment(
cancelPaymentSuccessData
)
expect(PayPalMock.cancelAuthorizedPayment).toHaveBeenCalledTimes(1)
expect(PayPalMock.cancelAuthorizedPayment).toHaveBeenCalledWith(
cancelPaymentSuccessData.purchase_units[0].payments.authorizations[0].id
)
expect(result).toEqual({
id: cancelPaymentSuccessData.id,
invoice_id: INVOICE_ID,
status: PaymentIntentDataByStatus[cancelPaymentSuccessData.id].status,
})
})
it("should succeed to refund an already captured payment", async () => {
const result = await paypalProvider.cancelPayment(
cancelPaymentRefundAlreadyCaptureSuccessData
)
expect(PayPalMock.refundPayment).toHaveBeenCalledTimes(1)
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
cancelPaymentRefundAlreadyCaptureSuccessData.purchase_units[0].payments
.captures[0].id
)
expect(result).toEqual({
id: cancelPaymentRefundAlreadyCaptureSuccessData.id,
invoice_id: INVOICE_ID,
status:
PaymentIntentDataByStatus[
cancelPaymentRefundAlreadyCaptureSuccessData.id
].status,
})
})
it("should succeed to do nothing if already canceled or already fully refund", async () => {
const result = await paypalProvider.cancelPayment(
cancelPaymentRefundAlreadyCanceledSuccessData
)
expect(PayPalMock.captureAuthorizedPayment).not.toHaveBeenCalled()
expect(PayPalMock.cancelAuthorizedPayment).not.toHaveBeenCalled()
expect(result).toEqual({
id: cancelPaymentRefundAlreadyCanceledSuccessData.id,
invoice_id: INVOICE_ID,
status:
PaymentIntentDataByStatus[
cancelPaymentRefundAlreadyCanceledSuccessData.id
].status,
})
})
it("should fail", async () => {
const result = await paypalProvider.cancelPayment(cancelPaymentFailData)
expect(result).toEqual({
code: "",
detail: "Error",
error: "An error occurred in retrievePayment",
})
})
})
describe("capturePayment", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await paypalProvider.capturePayment(
capturePaymentContextSuccessData.paymentSessionData
)
expect(result).toEqual({
id: capturePaymentContextSuccessData.paymentSessionData.id,
invoice_id: INVOICE_ID,
status:
PaymentIntentDataByStatus[
capturePaymentContextSuccessData.paymentSessionData.id
].status,
})
})
it("should fail", async () => {
const result = await paypalProvider.capturePayment(
capturePaymentContextFailData.paymentSessionData
)
expect(result).toEqual({
error: "An error occurred in capturePayment",
code: "",
detail: "Error.",
})
})
})
describe("refundPayment", function () {
let paypalProvider: PaypalProvider
const refundAmount = 500
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await paypalProvider.refundPayment(
refundPaymentSuccessData,
refundAmount
)
expect(PayPalMock.refundPayment).toHaveBeenCalled()
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
refundPaymentSuccessData.purchase_units[0].payments.captures[0].id,
{
amount: {
currency_code:
refundPaymentSuccessData.purchase_units[0].amount.currency_code,
value: "5.00",
},
}
)
expect(result).toEqual({
id: refundPaymentSuccessData.id,
invoice_id: INVOICE_ID,
status: PaymentIntentDataByStatus[refundPaymentSuccessData.id].status,
})
})
it("should fail if not already captured", async () => {
const result = await paypalProvider.refundPayment(
refundPaymentFailNotYetCapturedData,
refundAmount
)
expect(PayPalMock.refundPayment).not.toHaveBeenCalled()
expect(result).toEqual({
code: "",
detail: "Cannot refund an uncaptured payment",
error: "An error occurred in refundPayment",
})
})
it("should fail", async () => {
const result = await paypalProvider.refundPayment(
refundPaymentFailData,
refundAmount
)
expect(PayPalMock.refundPayment).toHaveBeenCalled()
expect(PayPalMock.refundPayment).toHaveBeenCalledWith(
refundPaymentFailData.purchase_units[0].payments.captures[0].id,
{
amount: {
currency_code:
refundPaymentFailData.purchase_units[0].amount.currency_code,
value: "5.00",
},
}
)
expect(result).toEqual({
code: "",
detail: "Error",
error: "An error occurred in retrievePayment",
})
})
})
describe("retrievePayment", function () {
let paypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await paypalProvider.retrievePayment(
retrievePaymentSuccessData
)
expect(result).toEqual({
id: retrievePaymentSuccessData.id,
invoice_id: INVOICE_ID,
status: PaymentIntentDataByStatus[retrievePaymentSuccessData.id].status,
})
})
it("should fail on refund creation", async () => {
const result = await paypalProvider.retrievePayment(
retrievePaymentFailData
)
expect(result).toEqual({
error: "An error occurred in retrievePayment",
code: "",
detail: "Error",
})
})
})
describe("updatePayment", function () {
let paypalProvider: PaypalProvider
beforeAll(async () => {
const scopedContainer = { ...container }
paypalProvider = new PaypalProvider(scopedContainer, paypalConfig)
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await paypalProvider.updatePayment(
updatePaymentSuccessData as unknown as PaymentProcessorContext
)
expect(PayPalMock.patchOrder).toHaveBeenCalled()
expect(PayPalMock.patchOrder).toHaveBeenCalledWith(
updatePaymentSuccessData.paymentSessionData.id,
[
{
op: "replace",
path: "/purchase_units/@reference_id=='default'",
value: {
amount: {
currency_code: updatePaymentSuccessData.currency_code,
value: "10.00",
},
},
},
]
)
expect(result).toEqual(
expect.objectContaining({
session_data: expect.any(Object),
})
)
})
it("should fail", async () => {
const result = await paypalProvider.updatePayment(
updatePaymentFailData as unknown as PaymentProcessorContext
)
expect(PayPalMock.patchOrder).toHaveBeenCalled()
expect(PayPalMock.patchOrder).toHaveBeenCalledWith(
updatePaymentFailData.paymentSessionData.id,
[
{
op: "replace",
path: "/purchase_units/@reference_id=='default'",
value: {
amount: {
currency_code: updatePaymentFailData.currency_code,
value: "10.00",
},
},
},
]
)
expect(result).toEqual({
code: "",
detail: "Error.",
error: "An error occurred in initiatePayment",
})
})
})
})

View File

@@ -1,343 +0,0 @@
import { EOL } from "os"
import {
AbstractPaymentProcessor,
isPaymentProcessorError,
PaymentProcessorContext,
PaymentProcessorError,
PaymentProcessorSessionResponse,
PaymentSessionStatus,
} from "@medusajs/medusa"
import {
PaypalOptions,
PaypalOrder,
PaypalOrderStatus,
PurchaseUnits,
} from "../types"
import { humanizeAmount } from "medusa-core-utils"
import { roundToTwo } from "./utils/utils"
import { CreateOrder, PaypalSdk } from "../core"
import { Logger } from "@medusajs/types"
import { MedusaError } from "@medusajs/utils"
class PayPalProviderService extends AbstractPaymentProcessor {
static identifier = "paypal"
protected readonly options_: PaypalOptions
protected paypal_: PaypalSdk
protected readonly logger_: Logger | undefined
constructor({ logger }: { logger?: Logger }, options) {
// @ts-ignore
// eslint-disable-next-line prefer-rest-params
super(...arguments)
this.logger_ = logger
this.options_ = options
this.init()
}
protected init(): void {
this.paypal_ = new PaypalSdk({
...this.options_,
logger: this.logger_,
})
}
async getPaymentStatus(
paymentSessionData: Record<string, unknown>
): Promise<PaymentSessionStatus> {
const order = (await this.retrievePayment(
paymentSessionData
)) as PaypalOrder
switch (order.status) {
case PaypalOrderStatus.CREATED:
return PaymentSessionStatus.PENDING
case PaypalOrderStatus.SAVED:
case PaypalOrderStatus.APPROVED:
case PaypalOrderStatus.PAYER_ACTION_REQUIRED:
return PaymentSessionStatus.REQUIRES_MORE
case PaypalOrderStatus.VOIDED:
return PaymentSessionStatus.CANCELED
case PaypalOrderStatus.COMPLETED:
return PaymentSessionStatus.AUTHORIZED
default:
return PaymentSessionStatus.PENDING
}
}
async initiatePayment(
context: PaymentProcessorContext
): Promise<PaymentProcessorError | PaymentProcessorSessionResponse> {
const { currency_code, amount, resource_id } = context
let session_data
try {
const intent: CreateOrder["intent"] = this.options_.capture
? "CAPTURE"
: "AUTHORIZE"
session_data = await this.paypal_.createOrder({
intent,
purchase_units: [
{
custom_id: resource_id,
amount: {
currency_code: currency_code.toUpperCase(),
value: roundToTwo(
humanizeAmount(amount, currency_code),
currency_code
),
},
},
],
})
} catch (e) {
return this.buildError("An error occurred in initiatePayment", e)
}
return {
session_data,
}
}
async authorizePayment(
paymentSessionData: Record<string, unknown>,
context: Record<string, unknown>
): Promise<
| PaymentProcessorError
| {
status: PaymentSessionStatus
data: PaymentProcessorSessionResponse["session_data"]
}
> {
try {
const stat = await this.getPaymentStatus(paymentSessionData)
const order = (await this.retrievePayment(
paymentSessionData
)) as PaypalOrder
return { data: order, status: stat }
} catch (error) {
return this.buildError("An error occurred in authorizePayment", error)
}
}
async cancelPayment(
paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
const order = (await this.retrievePayment(
paymentSessionData
)) as PaypalOrder
const isAlreadyCanceled = order.status === PaypalOrderStatus.VOIDED
const isCanceledAndFullyRefund =
order.status === PaypalOrderStatus.COMPLETED && !!order.invoice_id
if (isAlreadyCanceled || isCanceledAndFullyRefund) {
return order
}
try {
const { purchase_units } = paymentSessionData as {
purchase_units: PurchaseUnits
}
const isAlreadyCaptured = purchase_units.some(
(pu) => pu.payments.captures?.length
)
if (isAlreadyCaptured) {
const payments = purchase_units[0].payments
const payId = payments.captures[0].id
await this.paypal_.refundPayment(payId)
} else {
const id = purchase_units[0].payments.authorizations[0].id
await this.paypal_.cancelAuthorizedPayment(id)
}
return (await this.retrievePayment(
paymentSessionData
)) as unknown as PaymentProcessorSessionResponse["session_data"]
} catch (error) {
return this.buildError("An error occurred in cancelPayment", error)
}
}
async capturePayment(
paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
const { purchase_units } = paymentSessionData as {
purchase_units: PurchaseUnits
}
const id = purchase_units[0].payments.authorizations[0].id
try {
await this.paypal_.captureAuthorizedPayment(id)
return await this.retrievePayment(paymentSessionData)
} catch (error) {
return this.buildError("An error occurred in capturePayment", error)
}
}
/**
* Paypal does not provide such feature
* @param paymentSessionData
*/
async deletePayment(
paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
return paymentSessionData
}
async refundPayment(
paymentSessionData: Record<string, unknown>,
refundAmount: number
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
const { purchase_units } = paymentSessionData as {
purchase_units: PurchaseUnits
}
try {
const purchaseUnit = purchase_units[0]
const payments = purchaseUnit.payments
const isAlreadyCaptured = purchase_units.some(
(pu) => pu.payments.captures?.length
)
if (!isAlreadyCaptured) {
throw new Error("Cannot refund an uncaptured payment")
}
const paymentId = payments.captures[0].id
const currencyCode = purchaseUnit.amount.currency_code
await this.paypal_.refundPayment(paymentId, {
amount: {
currency_code: currencyCode,
value: roundToTwo(
humanizeAmount(refundAmount, currencyCode),
currencyCode
),
},
})
return await this.retrievePayment(paymentSessionData)
} catch (error) {
return this.buildError("An error occurred in refundPayment", error)
}
}
async retrievePayment(
paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
try {
const id = paymentSessionData.id as string
return (await this.paypal_.getOrder(
id
)) 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> {
try {
const { currency_code, amount } = context
const id = context.paymentSessionData.id as string
await this.paypal_.patchOrder(id, [
{
op: "replace",
path: "/purchase_units/@reference_id=='default'",
value: {
amount: {
currency_code: currency_code.toUpperCase(),
value: roundToTwo(
humanizeAmount(amount, currency_code),
currency_code
),
},
},
},
])
return { session_data: context.paymentSessionData }
} catch (error) {
return await this.initiatePayment(context).catch((e) => {
return this.buildError("An error occurred in updatePayment", e)
})
}
}
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (data.amount) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return data
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", e)
}
}
async retrieveOrderFromAuth(authorization) {
const link = authorization.links.find((l) => l.rel === "up")
const parts = link.href.split("/")
const orderId = parts[parts.length - 1]
if (!orderId) {
return null
}
return await this.paypal_.getOrder(orderId)
}
async retrieveAuthorization(id) {
return await this.paypal_.getAuthorizationPayment(id)
}
protected buildError(
message: string,
e: PaymentProcessorError | Error
): PaymentProcessorError {
return {
error: message,
code: "code" in e ? e.code : "",
detail: isPaymentProcessorError(e)
? `${e.error}${EOL}${e.detail ?? ""}`
: e.message ?? "",
}
}
/**
* Checks if a webhook is verified.
* @param {object} data - the verficiation data.
* @returns {Promise<object>} the response of the verification request.
*/
async verifyWebhook(data) {
return await this.paypal_.verifyWebhook({
webhook_id: this.options_.auth_webhook_id || this.options_.authWebhookId,
...data,
})
}
}
export default PayPalProviderService

View File

@@ -1,8 +0,0 @@
import { zeroDecimalCurrencies } from "medusa-core-utils";
export function roundToTwo(num: number, currency: string): string {
if (zeroDecimalCurrencies.includes(currency.toLowerCase())) {
return `${num}`
}
return num.toFixed(2)
}

View File

@@ -1,52 +0,0 @@
export interface PaypalOptions {
/**
* Indicate if it should run as sandbox, default false
*/
sandbox?: boolean
clientId: string
clientSecret: string
authWebhookId: string
capture?: boolean
/**
* Backward compatibility options below
*/
/**
* @deprecated use clientId instead
*/
client_id: string
/**
* @deprecated use clientSecret instead
*/
client_secret: string
/**
* @deprecated use authWebhookId instead
*/
auth_webhook_id: string
}
export type PaypalOrder = {
status: keyof typeof PaypalOrderStatus
invoice_id: string
}
export type PurchaseUnits = {
payments: {
captures: { id: string }[]
authorizations: { id: string }[]
}
amount: {
currency_code: string
value: string
}
}[]
export const PaypalOrderStatus = {
CREATED: "CREATED",
COMPLETED: "COMPLETED",
SAVED: "SAVED",
APPROVED: "APPROVED",
PAYER_ACTION_REQUIRED: "PAYER_ACTION_REQUIRED",
VOIDED: "VOIDED",
}

View File

@@ -1,33 +0,0 @@
{
"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"
]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -1,4 +0,0 @@
dist
node_modules
.DS_store
yarn.lock

File diff suppressed because one or more lines are too long

View File

@@ -1,74 +0,0 @@
# Stripe
Receive payments on your Medusa commerce application using Stripe.
[Stripe Plugin Documentation](https://docs.medusajs.com/plugins/payment/stripe) | [Medusa Website](https://medusajs.com/) | [Medusa Repository](https://github.com/medusajs/medusa)
## Features
- Authorize payments on orders from any sales channel.
- Support for Bancontact, BLIK, giropay, iDEAL, and Przelewy24.
- Capture payments from the admin dashboard.
- View payment analytics through Stripe's dashboard.
- Ready-integration with [Medusa's Next.js starter storefront](https://docs.medusajs.com/starters/nextjs-medusa-starter).
- Support for Stripe Webhooks.
---
## Prerequisites
- [Medusa backend](https://docs.medusajs.com/development/backend/install)
- [Stripe account](https://stripe.com/)
---
## How to Install
1\. Run the following command in the directory of the Medusa backend:
```bash
npm install medusa-payment-stripe
```
2\. Set the following environment variables in `.env`:
```bash
STRIPE_API_KEY=sk_...
# only necessary for production
STRIPE_WEBHOOK_SECRET=whsec_...
```
3\. In `medusa-config.js` add the following at the end of the `plugins` array:
```js
const plugins = [
// ...
{
resolve: `medusa-payment-stripe`,
options: {
api_key: process.env.STRIPE_API_KEY,
webhook_secret: process.env.STRIPE_WEBHOOK_SECRET,
},
},
]
```
---
## Test the Plugin
1\. Run the following command in the directory of the Medusa backend to run the backend:
```bash
npm run start
```
2\. Enable Stripe in a region in the admin. You can refer to [this User Guide](https://docs.medusajs.com/user-guide/regions/providers) to learn how to do that. Alternatively, you can use the [Admin APIs](https://docs.medusajs.com/api/admin#tag/Region/operation/PostRegionsRegion).
3\. Place an order using a storefront or the [Store APIs](https://docs.medusajs.com/api/store). You should be able to use Stripe as a payment method.
---
## Additional Resources
- [Stripe Plugin Documentation](https://docs.medusajs.com/plugins/payment/stripe)

View File

@@ -1,14 +0,0 @@
module.exports = {
globals: {
"ts-jest": {
tsconfig: "tsconfig.spec.json",
isolatedModules: false,
},
},
transform: {
"^.+\\.[jt]s?$": "@swc/jest",
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`, `json`],
modulePathIgnorePatterns: ["dist", "node_modules"],
}

View File

@@ -1,63 +0,0 @@
{
"name": "medusa-payment-stripe",
"version": "6.0.9",
"description": "Stripe Payment provider for Medusa Commerce",
"main": "dist/index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-payment-stripe"
},
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"author": "Medusa",
"license": "MIT",
"scripts": {
"prepublishOnly": "cross-env NODE_ENV=production tsc --build",
"test": "jest --silent --bail --maxWorkers=50% --forceExit",
"build": "rimraf dist && tsc -p ./tsconfig.server.json && medusa-admin bundle",
"watch": "tsc --watch"
},
"devDependencies": {
"@medusajs/admin": "^7.1.12",
"@medusajs/medusa": "^1.20.4",
"@swc/core": "^1.4.8",
"@swc/jest": "^0.2.36",
"@tanstack/react-table": "^8.7.9",
"@types/stripe": "^8.0.417",
"awilix": "^8.0.1",
"cross-env": "^5.2.1",
"jest": "^25.5.4",
"medusa-react": "^9.0.16",
"rimraf": "^5.0.1",
"typescript": "^4.9.5"
},
"peerDependenciesMeta": {
"@tanstack/react-table": {
"optional": true
},
"medusa-react": {
"optional": true
}
},
"peerDependencies": {
"@medusajs/medusa": "^1.12.0",
"@tanstack/react-table": "^8.7.9",
"medusa-react": "^9.0.0"
},
"dependencies": {
"@medusajs/utils": "^1.11.8",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"stripe": "^11.10.0"
},
"gitHead": "81a7ff73d012fda722f6e9ef0bd9ba0232d37808",
"keywords": [
"medusa-plugin",
"medusa-plugin-payment"
]
}

View File

@@ -1,30 +0,0 @@
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",
},
}

View File

@@ -1,99 +0,0 @@
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

View File

@@ -1,17 +0,0 @@
import { HTMLAttributes, FC } from "react"
type BadgeProps = HTMLAttributes<HTMLDivElement>
const Badge: FC<BadgeProps> = ({ children, onClick, ...props }) => {
return (
<div
className="badge badge-ghost rounded-full py-1 px-3"
onClick={onClick}
{...props}
>
{children}
</div>
)
}
export default Badge

View File

@@ -1,24 +0,0 @@
import { PropsWithChildren, ReactNode } from "react"
type Props = PropsWithChildren<{
title: string
description?: string
icon?: ReactNode
}>
export const Container = ({ title, description, icon, children }: Props) => {
return (
<div className="border-grey-20 rounded-rounded flex flex-col gap-y-4 border bg-white pt-6 pb-8">
<div className="px-8">
<div className="flex items-center">
{icon && <span className="mr-4">{icon}</span>}
<h2 className="text-[24px] font-semibold leading-9">{title}</h2>
</div>
{description && (
<p className="mt-2 text-sm text-gray-500">{description}</p>
)}
</div>
<div>{children}</div>
</div>
)
}

View File

@@ -1,33 +0,0 @@
import clsx from "clsx"
import { HTMLAttributes, FC } from "react"
type StatusIndicatorProps = {
title?: string
variant: "primary" | "danger" | "warning" | "success" | "active" | "default"
} & HTMLAttributes<HTMLDivElement>
const StatusIndicator: FC<StatusIndicatorProps> = ({
title,
variant = "success",
className,
...props
}) => {
const dotClass = clsx({
"bg-teal-50": variant === "success",
"bg-rose-50": variant === "danger",
"bg-yellow-50": variant === "warning",
})
return (
<div
className={clsx("inter-small-regular flex items-center", className, {
"hover:bg-grey-5 cursor-pointer": !!props.onClick,
})}
{...props}
>
<div className={clsx("h-1.5 w-1.5 self-center rounded-full", dotClass)} />
{title && <span className="ml-2">{title}</span>}
</div>
)
}
export default StatusIndicator

View File

@@ -1,174 +0,0 @@
import {
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table"
import moment from "moment"
import Badge from "./badge"
import StatusIndicator from "./dot"
import { formatAmount } from "medusa-react"
import { WidgetPayment } from "../../../types"
import LinkIcon from "../icons/link"
const STRIPE_DASHBOARD_URL = "https://dashboard.stripe.com"
type Props = {
payments: WidgetPayment[]
}
const capitalize = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1)
}
const riskScoreToStatusMapper = (riskScore: number | null) => {
if (!riskScore) {
return "default"
}
switch (true) {
case riskScore <= 15:
return "success"
case 15 < riskScore && riskScore <= 75:
return "warning"
case 75 < riskScore:
return "danger"
default:
return "default"
}
}
const columns: ColumnDef<WidgetPayment>[] = [
{
id: "intent",
header: () => <div className="flex items-center text-left">Intent</div>,
accessorFn: (row) => row.id,
cell: ({ row }) => {
return (
<div className="text-violet-60 flex">
<a
className="max-w-[100px] truncate text-blue-500"
href={`${STRIPE_DASHBOARD_URL}/payments/${row.original.id}`}
target="_blank"
rel="noopener noreferrer"
>
{row.original.id}
</a>
<LinkIcon />
</div>
)
},
},
{
id: "payment",
header: () => <div className="flex items-center text-right">Payment</div>,
accessorFn: (row) => row.amount,
cell: ({ row }) => {
return (
<div className="flex flex-col gap-y-1 ">
<p className="text-gray-900">
{formatAmount({
amount: row.original.amount,
region: row.original.region,
})}{" "}
</p>
</div>
)
},
},
{
id: "created",
header: () => <div className=" flex items-center">Created</div>,
accessorFn: (row) => row.created,
cell: ({ row }) => {
return (
<div className="flex flex-col gap-y-1 ">
{moment(new Date(row.original.created * 1000)).format("MMM D, YYYY")}
</div>
)
},
},
{
id: "payment_type",
header: () => <div className=" flex items-center">Payment Type</div>,
accessorFn: (row) => row.created,
cell: ({ row }) => {
return (
<div className="flex flex-col gap-y-1 ">
<p className="text-gray-900">{capitalize(row.original.type)}</p>
</div>
)
},
},
{
id: "fraud_score",
header: () => <div className="flex items-center">Risk Evaluation</div>,
accessorFn: (row) => row.risk_score,
cell: ({ row }) => {
const riskLevel = row.original.risk_level
const riskScore = row.original.risk_score
return (
<div className="flex flex-col gap-y-1">
{!row.original.risk_level ? (
"N/A"
) : (
<Badge>
<StatusIndicator
title={`${capitalize(riskLevel)} - ${riskScore}`}
variant={riskScoreToStatusMapper(riskScore)}
/>
</Badge>
)}
</div>
)
},
},
]
const Table = ({ payments }: Props) => {
const table = useReactTable({
data: payments,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<table className="w-full">
<thead className="border-y border-gray-200">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="py-4 text-[12px] font-semibold text-gray-500 first:pl-8 last:pr-8"
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b border-gray-200">
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
className="py-4 text-[12px] leading-5 first:pl-8 last:pr-8"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
)
}
export default Table

View File

@@ -1,23 +0,0 @@
import React from "react"
const LinkIcon: React.FC<React.SVGAttributes<SVGElement>> = ({}) => {
return (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.05356 5.32683H4.74268C4.2779 5.32683 3.83216 5.51146 3.50351 5.84011C3.17487 6.16875 2.99023 6.6145 2.99023 7.07927V15.2574C2.99023 15.7221 3.17487 16.1679 3.50351 16.4965C3.83216 16.8252 4.2779 17.0098 4.74268 17.0098H12.9208C13.3855 17.0098 13.8313 16.8252 14.1599 16.4965C14.4886 16.1679 14.6732 15.7221 14.6732 15.2574V11.0207M7.00323 13.0137L17.0098 2.99023M17.0098 2.99023H12.9208M17.0098 2.99023V7.07927"
stroke="#0081F1"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
)
}
export default LinkIcon

View File

@@ -1,25 +0,0 @@
import React from "react"
const StripeLogo: React.FC<React.SVGAttributes<SVGElement>> = ({}) => {
return (
<svg
width="40"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="0.5" y="0.5" width="39" height="39" rx="7.5" fill="white" />
<rect x="4" y="4" width="32" height="32" rx="4" fill="#6772E5" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.7941 16.555C18.7941 15.8272 19.3997 15.5457 20.3783 15.5457C21.9978 15.5806 23.587 15.9919 25.0203 16.7468V12.3534C23.5424 11.7726 21.9661 11.4831 20.3783 11.5008C16.6064 11.5008 14.0767 13.4765 14.0767 16.7755C14.0767 21.937 21.1645 21.0987 21.1645 23.3236C21.1645 24.1938 20.4208 24.4636 19.3742 24.4636C17.831 24.4636 15.8377 23.8251 14.2743 22.9751V27.4259C15.884 28.1262 17.6193 28.4917 19.3748 28.5C23.2512 28.5 25.9234 26.5849 25.9234 23.2285C25.9234 17.6579 18.7941 18.6529 18.7941 16.5561V16.555Z"
fill="white"
/>
<rect x="0.5" y="0.5" width="39" height="39" rx="7.5" stroke="#E6E8EB" />
</svg>
)
}
export default StripeLogo

View File

@@ -1,34 +0,0 @@
import { OrderDetailsWidgetProps, WidgetConfig } from "@medusajs/admin"
import { useAdminCustomQuery } from "medusa-react"
import { ListStripeIntentRes } from "../../types"
import { Container } from "../shared/components/container"
import Table from "../shared/components/table"
import StripeLogo from "../shared/icons/stripe-logo"
const MyWidget = (props: OrderDetailsWidgetProps) => {
const { order } = props
const { data } = useAdminCustomQuery<{}, ListStripeIntentRes>(
`/orders/stripe-payments/${order.id}`,
["admin_stripe"]
)
if (!order.payments.some((p) => p.provider_id === "stripe")) {
return null
}
return (
<Container title="Stripe Payments" icon={<StripeLogo />}>
<div className="flex flex-col">
{data && data?.payments?.length ? (
<Table payments={data.payments} />
) : null}
</div>
</Container>
)
}
export const config: WidgetConfig = {
zone: "order.details.after",
}
export default MyWidget

View File

@@ -1,7 +0,0 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import { getStripePayments } from "../../../../../controllers/get-payments"
export const GET = async (req: MedusaRequest, res: MedusaResponse) => {
const payments = await getStripePayments(req)
res.json({ payments })
}

View File

@@ -1,12 +0,0 @@
import type { MiddlewaresConfig } from "@medusajs/medusa"
import { raw } from "body-parser"
export const config: MiddlewaresConfig = {
routes: [
{
bodyParser: false,
matcher: "/stripe/hooks",
middlewares: [raw({ type: "application/json" })],
},
],
}

View File

@@ -1,30 +0,0 @@
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"
import { constructWebhook } from "../../utils/utils"
import StripeProviderService from "../../../services/stripe-provider"
export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
try {
const pluginOptions = req.scope.resolve<StripeProviderService>(
"stripeProviderService"
).options
const event = constructWebhook({
signature: req.headers["stripe-signature"],
body: req.body,
container: req.scope,
})
const eventBus = req.scope.resolve("eventBusService")
// we delay the processing of the event to avoid a conflict caused by a race condition
await eventBus.emit("medusa.stripe_payment_intent_update", event, {
delay: pluginOptions.webhook_delay || 5000,
attempts: pluginOptions.webhook_retries || 3,
})
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`)
return
}
res.sendStatus(200)
}

View File

@@ -1,139 +0,0 @@
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().mockImplementation(async () => undefined),
create: jest.fn().mockImplementation(async () => ({})),
})
)
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(),
})
)

View File

@@ -1,14 +0,0 @@
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"

View File

@@ -1,351 +0,0 @@
import { PostgresError } from "@medusajs/medusa"
import { EOL } from "os"
import Stripe from "stripe"
import { container } from "../__fixtures__/container"
import {
existingCartId,
existingCartIdWithCapturedStatus,
existingResourceId,
existingResourceNotCapturedId,
nonExistingCartId,
orderIdForExistingCartId,
paymentId,
paymentIntentId,
} from "../__fixtures__/data"
import { buildError, handlePaymentHook, isPaymentCollection } from "../utils"
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" } as any,
}
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}`
)
})
})
})
})

View File

@@ -1,258 +0,0 @@
import {
AbstractCartCompletionStrategy,
CartService,
IdempotencyKeyService,
PostgresError,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils"
import { AwilixContainer } from "awilix"
import { EOL } from "os"
import Stripe from "stripe"
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: Partial<Stripe.Event>
container: AwilixContainer
paymentIntent: Partial<Stripe.PaymentIntent>
}): 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) // TODO: revisit - this method doesn't exists ATM
})
}
}
}
async function capturePaymentIfNecessary({
cartId,
transactionManager,
container,
}) {
const orderService = container.resolve("orderService")
const order = await orderService
.withTransaction(transactionManager)
.retrieveByCartId(cartId)
.catch(() => undefined)
if (order && 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,
})
.catch(() => undefined)
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"] as string,
response_body["code"] as string
)
}
}
}

View File

@@ -1,51 +0,0 @@
import { OrderService } from "@medusajs/medusa"
import Stripe from "stripe"
import StripeBase from "../core/stripe-base"
import { WidgetPayment } from "../types"
export async function getStripePayments(req): Promise<WidgetPayment[]> {
const { order_id } = req.params
const orderService: OrderService = req.scope.resolve("orderService")
const stripeBase: StripeBase = req.scope.resolve("stripeProviderService")
const order = await orderService.retrieve(order_id, {
relations: ["payments", "swaps", "swaps.payment", "region"],
})
const paymentIds = order.payments
.filter((p) => p.provider_id === "stripe")
.map((p) => ({ id: p.data.id as string, type: "order" }))
if (order.swaps.length) {
const swapPayments = order.swaps
.filter((p) => p.payment.provider_id === "stripe")
.map((p) => ({ id: p.payment.data.id as string, type: "swap" }))
paymentIds.push(...swapPayments)
}
const payments = await Promise.all(
paymentIds.map(async (payment) => {
const intent = await stripeBase
.getStripe()
.paymentIntents.retrieve(payment.id, {
expand: ["latest_charge"],
})
const charge = intent.latest_charge as Stripe.Charge
return {
id: intent.id,
amount: intent.amount,
created: intent.created,
risk_score: charge?.outcome?.risk_score ?? null,
risk_level: charge?.outcome?.risk_level ?? null,
type: payment.type as "order" | "swap",
region: order.region,
}
})
)
return payments
}

View File

@@ -1,225 +0,0 @@
import {
EXISTING_CUSTOMER_EMAIL,
FAIL_INTENT_ID,
PARTIALLY_FAIL_INTENT_ID,
STRIPE_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,
},
}
export const updatePaymentDataWithAmountData = {
sessionId: STRIPE_ID,
amount: 2000,
}
export const updatePaymentDataWithoutAmountData = {
sessionId: STRIPE_ID,
customProp: "test",
}

View File

@@ -1,12 +0,0 @@
import StripeBase from "../stripe-base"
import { PaymentIntentOptions } from "../../types"
export class StripeTest extends StripeBase {
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {}
}
}

View File

@@ -1,625 +0,0 @@
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,
updatePaymentDataWithAmountData,
updatePaymentDataWithoutAmountData,
} from "../__fixtures__/data"
import {
PARTIALLY_FAIL_INTENT_ID,
STRIPE_ID,
StripeMock,
} from "../../__mocks__/stripe"
import { ErrorIntentStatus } from "../../types"
const container = {}
describe.skip("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" })
})
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" })
})
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" })
})
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" })
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed", async () => {
const result = await stripeTest.capturePayment(
capturePaymentContextSuccessData.paymentSessionData
)
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.paymentSessionData
)
expect(result).toEqual({
id: PARTIALLY_FAIL_INTENT_ID,
status: ErrorIntentStatus.SUCCEEDED,
})
})
it("should fail on intent capture", async () => {
const result = await stripeTest.capturePayment(
capturePaymentContextFailData.paymentSessionData
)
expect(result).toEqual({
error: "An error occurred in capturePayment",
code: "",
detail: "Error",
})
})
})
describe("deletePayment", function () {
let stripeTest
beforeAll(async () => {
const scopedContainer = { ...container }
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
})
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" })
})
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" })
})
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" })
})
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",
})
})
})
describe("updatePaymentData", function () {
let stripeTest
beforeAll(async () => {
const scopedContainer = { ...container }
stripeTest = new StripeTest(scopedContainer, { api_key: "test" })
})
beforeEach(() => {
jest.clearAllMocks()
})
it("should succeed to update the payment data", async () => {
const result = await stripeTest.updatePaymentData(
updatePaymentDataWithoutAmountData.sessionId,
{ ...updatePaymentDataWithoutAmountData, sessionId: undefined }
)
expect(StripeMock.paymentIntents.update).toHaveBeenCalled()
expect(StripeMock.paymentIntents.update).toHaveBeenCalledWith(
updatePaymentDataWithoutAmountData.sessionId,
{
customProp: updatePaymentDataWithoutAmountData.customProp,
}
)
expect(result).toEqual(
expect.objectContaining({
customProp: updatePaymentDataWithoutAmountData.customProp,
})
)
})
it("should fail to update the payment data if the amount is present", async () => {
const result = await stripeTest.updatePaymentData(
updatePaymentDataWithAmountData.sessionId,
{ ...updatePaymentDataWithAmountData, sessionId: undefined }
)
expect(StripeMock.paymentIntents.update).not.toHaveBeenCalled()
expect(result).toEqual({
error: "An error occurred in updatePaymentData",
code: undefined,
detail: "Cannot update amount, use updatePayment instead",
})
})
})
})

View File

@@ -1,343 +0,0 @@
import {
AbstractPaymentProcessor,
isPaymentProcessorError,
PaymentProcessorContext,
PaymentProcessorError,
PaymentProcessorSessionResponse,
PaymentSessionStatus,
} from "@medusajs/medusa"
import { MedusaError } from "@medusajs/utils"
import { EOL } from "os"
import Stripe from "stripe"
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
get options(): StripeOptions {
return this.options_
}
getStripe() {
return this.stripe_
}
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(
paymentSessionData: Record<string, unknown>
): Promise<
PaymentProcessorError | PaymentProcessorSessionResponse["session_data"]
> {
const id = 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 capturePayment", 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)
}
}
}
async updatePaymentData(sessionId: string, data: Record<string, unknown>) {
try {
// Prevent from updating the amount from here as it should go through
// the updatePayment method to perform the correct logic
if (data.amount) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot update amount, use updatePayment instead"
)
}
return (await this.stripe_.paymentIntents.update(sessionId, {
...data,
})) as unknown as PaymentProcessorSessionResponse["session_data"]
} catch (e) {
return this.buildError("An error occurred in updatePaymentData", 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,
error: Stripe.StripeRawError | PaymentProcessorError | Error
): PaymentProcessorError {
return {
error: message,
code: "code" in error ? error.code : "unknown",
detail: isPaymentProcessorError(error)
? `${error.error}${EOL}${error.detail ?? ""}`
: "detail" in error
? error.detail
: error.message ?? "",
}
}
}
export default StripeBase

View File

@@ -1,8 +0,0 @@
export * from "./core/stripe-base"
export * from "./services/stripe-bancontact"
export * from "./services/stripe-blik"
export * from "./services/stripe-giropay"
export * from "./services/stripe-ideal"
export * from "./services/stripe-provider"
export * from "./services/stripe-przelewy24"
export * from "./types"

View File

@@ -1,19 +0,0 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class BancontactProviderService extends StripeBase {
static identifier = PaymentProviderKeys.BAN_CONTACT
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["bancontact"],
capture_method: "automatic",
}
}
}
export default BancontactProviderService

View File

@@ -1,19 +0,0 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class BlikProviderService extends StripeBase {
static identifier = PaymentProviderKeys.BLIK
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["blik"],
capture_method: "automatic",
}
}
}
export default BlikProviderService

View File

@@ -1,19 +0,0 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class GiropayProviderService extends StripeBase {
static identifier = PaymentProviderKeys.GIROPAY
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["giropay"],
capture_method: "automatic",
}
}
}
export default GiropayProviderService

View File

@@ -1,19 +0,0 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class IdealProviderService extends StripeBase {
static identifier = PaymentProviderKeys.IDEAL
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["ideal"],
capture_method: "automatic",
}
}
}
export default IdealProviderService

View File

@@ -1,16 +0,0 @@
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

View File

@@ -1,19 +0,0 @@
import StripeBase from "../core/stripe-base"
import { PaymentIntentOptions, PaymentProviderKeys } from "../types"
class Przelewy24ProviderService extends StripeBase {
static identifier = PaymentProviderKeys.PRZELEWY_24
constructor(_, options) {
super(_, options)
}
get paymentIntentOptions(): PaymentIntentOptions {
return {
payment_method_types: ["p24"],
capture_method: "automatic",
}
}
}
export default Przelewy24ProviderService

View File

@@ -1,24 +0,0 @@
import { type SubscriberArgs, type SubscriberConfig } from "@medusajs/medusa"
import Stripe from "stripe"
import { handlePaymentHook } from "../api/utils/utils"
export default async function stripeHandler({
data,
container,
}: SubscriberArgs<Stripe.Event>) {
const event = data
const paymentIntent = event.data.object as Stripe.PaymentIntent
await handlePaymentHook({
event,
container,
paymentIntent,
})
}
export const config: SubscriberConfig = {
event: "medusa.stripe_payment_intent_update",
context: {
subscriberId: "medusa.stripe_payment_intent_update",
},
}

View File

@@ -1,66 +0,0 @@
import { Region } from "@medusajs/medusa"
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
/**
* The delay in milliseconds before processing the webhook event.
* @defaultValue 5000
*/
webhook_delay?: number
/**
* The number of times to retry the webhook event processing in case of an error.
* @defaultValue 3
*/
webhook_retries?: number
}
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",
}
export type WidgetPayment = {
id: string
amount: number
created: number
risk_score: number | null
risk_level: string | null
region: Region
type: "order" | "swap"
}
export type ListStripeIntentRes = {
payments: WidgetPayment[]
}

View File

@@ -1,5 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/admin/**/*.{js,jsx,ts,tsx}"],
theme: {},
};

View File

@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "esnext"
},
"include": ["src/admin"],
"exclude": ["**/*.spec.js"]
}

View File

@@ -1,35 +0,0 @@
{
"compilerOptions": {
"lib": [
"es5",
"es6",
"es2019"
],
"target": "es5",
"jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
"outDir": "./dist",
"esModuleInterop": true,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noImplicitReturns": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"allowJs": true,
"skipLibCheck": true,
"downlevelIteration": true // to use ES5 specific tooling
},
"include": ["src"],
"exclude": [
"dist",
"build",
"src/**/__tests__",
"src/**/__mocks__",
"src/**/__fixtures__",
"node_modules",
".eslintrc.js"
]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
},
"exclude": ["src/admin", "**/*.spec.js"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"exclude": ["node_modules"]
}

Some files were not shown because too many files have changed in this diff Show More