chore(): rm payment plugins (#7217)
This commit is contained in:
committed by
GitHub
parent
f129415650
commit
0140fd63ab
@@ -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",
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/medusa-payment-klarna/.gitignore
vendored
16
packages/medusa-payment-klarna/.gitignore
vendored
@@ -1,16 +0,0 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
!jest.config.js
|
||||
|
||||
/dist
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.DS_store
|
||||
src
|
||||
dist
|
||||
yarn.lock
|
||||
.babelrc
|
||||
|
||||
.turbo
|
||||
.yarn
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
// noop
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": `../../jest-transformer.js`,
|
||||
},
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
|
||||
hooks(app)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -1,5 +0,0 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
@@ -1,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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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" })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
15
packages/medusa-payment-manual/.gitignore
vendored
15
packages/medusa-payment-manual/.gitignore
vendored
@@ -1,15 +0,0 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
@@ -1,8 +0,0 @@
|
||||
.DS_store
|
||||
src
|
||||
dist
|
||||
yarn.lock
|
||||
.babelrc
|
||||
|
||||
.turbo
|
||||
.yarn
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -1 +0,0 @@
|
||||
// noop
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
4
packages/medusa-payment-paypal/.gitignore
vendored
4
packages/medusa-payment-paypal/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.DS_store
|
||||
yarn.lock
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"jsc": {
|
||||
"target": "es5",
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
}
|
||||
},
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "@swc/jest",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(axios)/).*", "/dist"],
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default () => {
|
||||
const app = Router()
|
||||
|
||||
hooks(app)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}`,
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./paypal-sdk"
|
||||
export * from "./types"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./types"
|
||||
export * from "./services/paypal-provider"
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
4
packages/medusa-payment-stripe/.gitignore
vendored
4
packages/medusa-payment-stripe/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
dist
|
||||
node_modules
|
||||
.DS_store
|
||||
yarn.lock
|
||||
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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" })],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/admin/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {},
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "esnext"
|
||||
},
|
||||
"include": ["src/admin"],
|
||||
"exclude": ["**/*.spec.js"]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user