Replaces MongoDB support with PostgreSQL (#151)

- All schemas have been rewritten to a relational model
- All services have been rewritten to accommodate the new data model
- Adds idempotency keys to core endpoints allowing you to retry requests with no additional side effects
- Adds staged jobs to avoid putting jobs in the queue when transactions abort
- Adds atomic transactions to all methods with access to the data layer

Co-authored-by: Oliver Windall Juhl <oliver@mrbltech.com>
This commit is contained in:
Sebastian RindomandOliver Windall Juhl committed 2021-01-26 10:26:14 +01:00
1 parent 5f819486fc
commit f1baca3cbd
499 files changed
+25908 -16127

No files matched your search

+101 -101
View File
@@ -7,12 +7,12 @@ routes:
path: /orders/:id path: /orders/:id
route: /orders route: /orders
description: > description: >
An order represents a selection of items purchased and holds information about An Order represents a selection of items purchased and holds information about
how the items have been purchased and will be fulfilled. how the items have been purchased and will be fulfilled.
endpoints: endpoints:
- path: /:id - path: /:id
method: GET method: GET
title: Retrieve an order title: Retrieve an Order
params: params:
- name: id - name: id
type: String type: String
@@ -34,104 +34,104 @@ response: |
{ {
"order": { "order": {
"id": "order_fi13oadpo2r3vc2g4592", "id": "order_fi13oadpo2r3vc2g4592",
"status": "pending", "status": "pending",
"fulfillment_status": "shipped", "fulfillment_status": "shipped",
"payment_status": "captured", "payment_status": "captured",
"email": "iron@man.com", "email": "iron@man.com",
"billing_address": { "billing_address": {
"id": "addr_WgNn0BSfIu", "id": "addr_WgNn0BSfIu",
"customer_id": "cus_4eThzYSuGv", "customer_id": "cus_4eThzYSuGv",
"company": "Stark Industries", "company": "Stark Industries",
"first_name": "Tony", "first_name": "Tony",
"last_name": "Stark", "last_name": "Stark",
"address_1": "Hollywood Boulevard 1", "address_1": "Hollywood Boulevard 1",
"address_2": null, "address_2": null,
"city": "Los Angeles", "city": "Los Angeles",
"country_code": "US", "country_code": "US",
"province": "CA", "province": "CA",
"postal_code": "90046", "postal_code": "90046",
"phone": null, "phone": null,
"created_at": "2020-12-11T17:03:54.458Z", "created_at": "2020-12-11T17:03:54.458Z",
"updated_at": "2020-12-11T17:03:54.458Z", "updated_at": "2020-12-11T17:03:54.458Z",
"deleted_at": null, "deleted_at": null,
"metadata": null "metadata": null
}, },
"shipping_address": { "shipping_address": {
"id": "addr_WgNn0BSfIu", "id": "addr_WgNn0BSfIu",
"customer_id": "cus_4eThzYSuGv", "customer_id": "cus_4eThzYSuGv",
"company": "Stark Industries", "company": "Stark Industries",
"first_name": "Tony", "first_name": "Tony",
"last_name": "Stark", "last_name": "Stark",
"address_1": "Hollywood Boulevard 1", "address_1": "Hollywood Boulevard 1",
"address_2": null, "address_2": null,
"city": "Los Angeles", "city": "Los Angeles",
"country_code": "US", "country_code": "US",
"province": "CA", "province": "CA",
"postal_code": "90046", "postal_code": "90046",
"phone": null, "phone": null,
"created_at": "2020-12-11T17:03:54.458Z", "created_at": "2020-12-11T17:03:54.458Z",
"updated_at": "2020-12-11T17:03:54.458Z", "updated_at": "2020-12-11T17:03:54.458Z",
"deleted_at": null, "deleted_at": null,
"metadata": null "metadata": null
}, },
"items": [ "items": [
{ {
"id": "item_fn2uaQH95vG9ZMnhj2aU03xg", "id": "item_fn2uaQH95vG9ZMnhj2aU03xg",
"cart_id": null, "cart_id": null,
"order_id": "order_s9RojwCU2AM8RztcldM2Uof7", "order_id": "order_s9RojwCU2AM8RztcldM2Uof7",
"swap_id": null, "swap_id": null,
"title": "Ironman suit", "title": "Ironman suit",
"description": "Awesome Ironman suit", "description": "Awesome Ironman suit",
"thumbnail": null, "thumbnail": null,
"is_giftcard": false, "is_giftcard": false,
"should_merge": false, "should_merge": false,
"allow_discounts": true, "allow_discounts": true,
"unit_price": 119600, "unit_price": 119600,
"variant_id": "variant_rdEH6PykBuH57giw", "variant_id": "variant_rdEH6PykBuH57giw",
"quantity": 1, "quantity": 1,
"fulfilled_quantity": 1, "fulfilled_quantity": 1,
"returned_quantity": 0, "returned_quantity": 0,
"shipped_quantity": 0, "shipped_quantity": 0,
"created_at": "2020-12-11T17:03:54.458Z", "created_at": "2020-12-11T17:03:54.458Z",
"updated_at": "2020-12-11T17:03:54.458Z", "updated_at": "2020-12-11T17:03:54.458Z",
"metadata": null "metadata": null
} }
], ],
"discounts": [ "discounts": [
{ {
"id": "disc_OpYQN4H8MOWHN2en", "id": "disc_OpYQN4H8MOWHN2en",
"code": "CCC3C4LL88", "code": "CCC3C4LL88",
"is_dynamic": true, "is_dynamic": true,
"discount_rule_id": "dru_MDyr3lJLa00uxOsY", "discount_rule_id": "dru_MDyr3lJLa00uxOsY",
"parent_discount_id": "disc_ubtdlkASI7bfUj81", "parent_discount_id": "disc_ubtdlkASI7bfUj81",
"metadata": null "metadata": null
} }
], ],
"customer_id": "cus_4eThzYSuGv", "customer_id": "cus_4eThzYSuGv",
"shipping_methods": [ "shipping_methods": [
{ {
"id": "sm_77bEapbO8tkCqw3yo1NBuCUl", "id": "sm_77bEapbO8tkCqw3yo1NBuCUl",
"shipping_option_id": "so_nRvwHhEdZw", "shipping_option_id": "so_nRvwHhEdZw",
"order_id": "order_s9RojwCU2AM8RztcldM2Uof7", "order_id": "order_s9RojwCU2AM8RztcldM2Uof7",
"cart_id": null, "cart_id": null,
"swap_id": null, "swap_id": null,
"return_id": null, "return_id": null,
"price": 0, "price": 0,
"data": { "data": {
"id": "Parcel shop", "id": "Parcel shop",
"city": "Los Angeles", "city": "Los Angeles",
"postal": "90046" "postal": "90046"
} }
} }
], ],
"metadata": null, "metadata": null,
"display_id": 50433, "display_id": 50433,
"currency_code": "dkk", "currency_code": "dkk",
"region_id": "reg_HMnixPlOicAs7aBlXuchAGxd", "region_id": "reg_HMnixPlOicAs7aBlXuchAGxd",
"shipping_total": 0, "shipping_total": 0,
"discount_total": 0, "discount_total": 0,
"tax_total": 3850000, "tax_total": 3850000,
"subtotal": 154000, "subtotal": 154000,
"total": 4004000 "total": 4004000
} }
} }
+8 -9
View File
@@ -1,10 +1,9 @@
0 silly argv { _: [ 'publish' ], 0 silly argv { _: [ 'bootstrap' ],
0 silly argv composed: 'publish', 0 silly argv lernaVersion: '3.22.1',
0 silly argv lernaVersion: '3.20.2', 0 silly argv '$0': '/usr/local/bin/lerna' }
0 silly argv '$0': '/usr/local/bin/lerna', 1 notice cli v3.22.1
0 silly argv bump: 'from-git' }
1 notice cli v3.20.2
2 verbose rootPath /Users/srindom/Developer/medusa-js 2 verbose rootPath /Users/srindom/Developer/medusa-js
3 error JSONError: Unexpected token : in JSON at position 9 while parsing near ' "name": "@medusajs/medusa"...' in packages/medusa/package.json 3 info versioning independent
3 error at module.exports (/Users/srindom/Developer/medusa-js/node_modules/parse-json/index.js:26:19) 4 error JSONError: Unexpected token < in JSON at position 34 while parsing near '...edusa-file-spaces",<<<<<<< HEAD "vers...' in packages/medusa-file-spaces/package.json
3 error at parse (/Users/srindom/Developer/medusa-js/node_modules/load-json-file/index.js:15:9) 4 error at module.exports (/Users/srindom/Developer/medusa-js/node_modules/parse-json/index.js:26:19)
4 error at parse (/Users/srindom/Developer/medusa-js/node_modules/load-json-file/index.js:15:9)
@@ -15,7 +15,10 @@ function preset(context, options = {}) {
return { return {
presets: [r(`@babel/preset-env`)], presets: [r(`@babel/preset-env`)],
plugins: [ plugins: [
r(`@babel/plugin-proposal-class-properties`), r(`babel-plugin-transform-typescript-metadata`),
r(`@babel/plugin-proposal-optional-chaining`),
[r(`@babel/plugin-proposal-decorators`), { legacy: true }],
[r(`@babel/plugin-proposal-class-properties`), { loose: true }],
r(`@babel/plugin-transform-classes`), r(`@babel/plugin-transform-classes`),
r(`@babel/plugin-transform-instanceof`), r(`@babel/plugin-transform-instanceof`),
r(`@babel/plugin-transform-runtime`), r(`@babel/plugin-transform-runtime`),
@@ -1,6 +1,6 @@
{ {
"name": "babel-preset-medusa-package", "name": "babel-preset-medusa-package",
"version": "1.0.1", "version": "1.0.2-alpha.787+0646bd3",
"author": "Sebastian Rindom <sebastian@mrbltech.com>", "author": "Sebastian Rindom <sebastian@mrbltech.com>",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -9,11 +9,13 @@
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-decorators": "^7.12.1",
"@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@babel/plugin-transform-classes": "^7.12.1", "@babel/plugin-transform-classes": "^7.12.1",
"@babel/plugin-transform-instanceof": "^7.12.1", "@babel/plugin-transform-instanceof": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.1", "@babel/plugin-transform-runtime": "^7.12.1",
"@babel/preset-env": "^7.12.7", "@babel/preset-env": "^7.12.7",
"babel-plugin-transform-typescript-metadata": "^0.3.1",
"core-js": "^3.7.0" "core-js": "^3.7.0"
}, },
"peerDependencies": { "peerDependencies": {
@@ -23,5 +25,6 @@
"main": "index.js", "main": "index.js",
"engines": { "engines": {
"node": ">=10.14.0" "node": ">=10.14.0"
} },
"gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@medusajs/medusa-cli", "name": "@medusajs/medusa-cli",
"version": "1.0.11", "version": "1.0.12-alpha.787+0646bd3",
"description": "Command Line interface for Medusa Commerce", "description": "Command Line interface for Medusa Commerce",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
@@ -48,5 +48,5 @@
"resolve-cwd": "^3.0.0", "resolve-cwd": "^3.0.0",
"yargs": "^15.3.1" "yargs": "^15.3.1"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+16
View File
@@ -61,6 +61,22 @@ function buildLocalCommands(cli, isLocalProject) {
} }
cli cli
.command({
command: `migrations [action]`,
desc: `Migrate the database to the most recent version.`,
builder: {
action: {
demand: true,
choices: ["run", "show"],
},
},
handler: handlerP(
getCommandHandler(`migrate`, (args, cmd) => {
process.env.NODE_ENV = process.env.NODE_ENV || `development`
return cmd(args)
})
),
})
.command({ .command({
command: `develop`, command: `develop`,
desc: `Start development server. Watches file and rebuilds when something changes`, desc: `Start development server. Watches file and rebuilds when something changes`,
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-core-utils", "name": "medusa-core-utils",
"version": "1.0.11", "version": "1.0.12-alpha.787+0646bd3",
"description": "Core utils for Medusa", "description": "Core utils for Medusa",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -30,8 +30,8 @@
"prettier": "^1.19.1" "prettier": "^1.19.1"
}, },
"dependencies": { "dependencies": {
"joi": "^17.2.1", "joi": "^17.3.0",
"joi-objectid": "^3.0.1" "joi-objectid": "^3.0.1"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+3 -3
View File
@@ -7,13 +7,13 @@ Joi.address = () => {
first_name: Joi.string().required(), first_name: Joi.string().required(),
last_name: Joi.string().required(), last_name: Joi.string().required(),
address_1: Joi.string().required(), address_1: Joi.string().required(),
address_2: Joi.string().allow(""), address_2: Joi.string().allow(null),
city: Joi.string().required(), city: Joi.string().required(),
country_code: Joi.string().required(), country_code: Joi.string().required(),
province: Joi.string().allow(""), province: Joi.string().allow(null),
postal_code: Joi.string().required(), postal_code: Joi.string().required(),
phone: Joi.string().optional(), phone: Joi.string().optional(),
metadata: Joi.object(), metadata: Joi.object().allow(null),
}) })
} }
+24 -24
View File
@@ -941,28 +941,11 @@
exec-sh "^0.3.2" exec-sh "^0.3.2"
minimist "^1.2.0" minimist "^1.2.0"
"@hapi/address@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/formula@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
"@hapi/hoek@^9.0.0": "@hapi/hoek@^9.0.0":
version "9.1.0" version "9.1.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw== integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
"@hapi/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
"@hapi/topo@^5.0.0": "@hapi/topo@^5.0.0":
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7" resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
@@ -1153,6 +1136,23 @@
"@types/yargs" "^15.0.0" "@types/yargs" "^15.0.0"
chalk "^3.0.0" chalk "^3.0.0"
"@sideway/address@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d"
integrity sha512-wAH/JYRXeIFQRsxerIuLjgUu2Xszam+O5xKeatJ4oudShOOirfmsQ1D6LL54XOU2tizpCYku+s1wmU0SYdpoSA==
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df"
integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==
"@sinonjs/commons@^1.7.0": "@sinonjs/commons@^1.7.0":
version "1.7.2" version "1.7.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
@@ -3241,16 +3241,16 @@ joi-objectid@^3.0.1:
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
joi@^17.2.1: joi@^17.3.0:
version "17.2.1" version "17.3.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a" resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA== integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
dependencies: dependencies:
"@hapi/address" "^4.1.0"
"@hapi/formula" "^2.0.0"
"@hapi/hoek" "^9.0.0" "@hapi/hoek" "^9.0.0"
"@hapi/pinpoint" "^2.0.0"
"@hapi/topo" "^5.0.0" "@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-file-spaces", "name": "medusa-file-spaces",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Digital Ocean Spaces file connector for Medusa", "description": "Digital Ocean Spaces file connector for Medusa",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -40,9 +40,9 @@
"aws-sdk": "^2.710.0", "aws-sdk": "^2.710.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"stripe": "^8.50.0" "stripe": "^8.50.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -1,6 +1,6 @@
{ {
"name": "medusa-fulfillment-manual", "name": "medusa-fulfillment-manual",
"version": "1.0.12", "version": "1.0.12-alpha.208+0646bd3",
"description": "A manual fulfillment provider for Medusa", "description": "A manual fulfillment provider for Medusa",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -36,7 +36,7 @@
"@babel/plugin-transform-instanceof": "^7.8.3", "@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11" "medusa-core-utils": "^1.0.12-alpha.787+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -36,6 +36,11 @@ class ManualFulfillmentService extends FulfillmentService {
return Promise.resolve({}) return Promise.resolve({})
} }
createFulfillment() {
// No data is being sent anywhere
return Promise.resolve({})
}
cancelFulfillment() { cancelFulfillment() {
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -1,6 +1,6 @@
{ {
"name": "medusa-fulfillment-webshipper", "name": "medusa-fulfillment-webshipper",
"version": "1.0.11", "version": "1.0.10-alpha.166+0646bd3",
"description": "Webshipper Fulfillment provider for Medusa", "description": "Webshipper Fulfillment provider for Medusa",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -36,7 +36,7 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11" "medusa-core-utils": "^1.0.12-alpha.787+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -7,10 +7,18 @@ class WebshipperFulfillmentService extends FulfillmentService {
constructor({ logger, swapService, orderService }, options) { constructor({ logger, swapService, orderService }, options) {
super() super()
this.logger_ = logger
this.orderService_ = orderService
this.options_ = options this.options_ = options
/** @private @const {logger} */
this.logger_ = logger
/** @private @const {OrderService} */
this.orderService_ = orderService
/** @private @const {SwapService} */
this.swapService_ = swapService this.swapService_ = swapService
/** @private @const {AxiosClient} */
this.client_ = new Webshipper({ this.client_ = new Webshipper({
account: this.options_.account, account: this.options_.account,
token: this.options_.api_token, token: this.options_.api_token,
@@ -38,15 +46,19 @@ class WebshipperFulfillmentService extends FulfillmentService {
})) }))
} }
async validateFulfillmentData(data, _) { async validateFulfillmentData(optionData, data, _) {
if (data.require_drop_point) { if (optionData.require_drop_point) {
if (!data.drop_point_id) { if (!data.drop_point_id) {
throw new Error("Must have drop point id") throw new Error("Must have drop point id")
} else { } else {
// TODO: validate that the drop point exists // TODO: validate that the drop point exists
} }
} }
return data
return {
...optionData,
...data,
}
} }
async validateOption(data) { async validateOption(data) {
@@ -69,7 +81,22 @@ class WebshipperFulfillmentService extends FulfillmentService {
* Creates a return shipment in webshipper using the given method data, and * Creates a return shipment in webshipper using the given method data, and
* return lines. * return lines.
*/ */
async createReturn(methodData, returnLines, fromOrder) { async createReturn(returnOrder) {
let fromOrder
if (returnOrder.order_id) {
fromOrder = await this.orderService_.retrieve(returnOrder.order_id, {
select: ["total"],
relations: ["shipping_address", "returns"],
})
} else if (returnOrder.swap) {
fromOrder = await this.orderService_.retrieve(returnOrder.swap.order_id, {
select: ["total"],
relations: ["shipping_address", "returns"],
})
}
const methodData = returnOrder.shipping_method.data
const relationships = { const relationships = {
shipping_rate: { shipping_rate: {
data: { data: {
@@ -79,7 +106,8 @@ class WebshipperFulfillmentService extends FulfillmentService {
}, },
} }
const existing = fromOrder.metadata.webshipper_order_id const existing =
fromOrder.metadata && fromOrder.metadata.webshipper_order_id
if (existing) { if (existing) {
relationships.order = { relationships.order = {
data: { data: {
@@ -93,8 +121,9 @@ class WebshipperFulfillmentService extends FulfillmentService {
if (this.invoiceGenerator_) { if (this.invoiceGenerator_) {
const base64Invoice = await this.invoiceGenerator_.createReturnInvoice( const base64Invoice = await this.invoiceGenerator_.createReturnInvoice(
fromOrder, fromOrder,
returnLines returnOrder.items
) )
docs.push({ docs.push({
document_size: "A4", document_size: "A4",
document_format: "PDF", document_format: "PDF",
@@ -108,7 +137,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
type: "shipments", type: "shipments",
attributes: { attributes: {
reference: `R${fromOrder.display_id}-${fromOrder.returns.length + 1}`, reference: `R${fromOrder.display_id}-${fromOrder.returns.length + 1}`,
ext_ref: `${fromOrder._id}.${fromOrder.returns.length}`, ext_ref: `${fromOrder.id}.${returnOrder.id}`,
is_return: true, is_return: true,
included_documents: docs, included_documents: docs,
packages: [ packages: [
@@ -121,21 +150,20 @@ class WebshipperFulfillmentService extends FulfillmentService {
width: 15, width: 15,
length: 15, length: 15,
}, },
customs_lines: returnLines.map((item) => { customs_lines: returnOrder.items.map(({ item, quantity }) => {
return { return {
ext_ref: item._id, ext_ref: item.id,
sku: item.content.variant.sku, sku: item.variant.sku,
description: item.title, description: item.title,
quantity: item.quantity, quantity: quantity,
country_of_origin: country_of_origin:
item.content.variant.metadata && item.variant.origin_country ||
item.content.variant.metadata.origin_country, item.variant.product.origin_country,
tarif_number: tarif_number:
item.content.variant.metadata && item.variant.hs_code || item.variant.product.hs_code,
item.content.variant.metadata.hs_code, unit_price: item.unit_price / 100,
unit_price: item.content.unit_price, vat_percent: fromOrder.tax_rate,
vat_percent: fromOrder.tax_rate * 100, currency: fromOrder.currency_code.toUpperCase(),
currency: fromOrder.currency_code,
} }
}), }),
}, },
@@ -148,7 +176,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
address_2: shipping_address.address_2, address_2: shipping_address.address_2,
zip: shipping_address.postal_code, zip: shipping_address.postal_code,
city: shipping_address.city, city: shipping_address.city,
country_code: shipping_address.country_code, country_code: shipping_address.country_code.toUpperCase(),
state: shipping_address.province, state: shipping_address.province,
phone: shipping_address.phone, phone: shipping_address.phone,
email: fromOrder.email, email: fromOrder.email,
@@ -196,7 +224,12 @@ class WebshipperFulfillmentService extends FulfillmentService {
return toReturn return toReturn
} }
async createOrder(methodData, fulfillmentItems, fromOrder) { async createFulfillment(
methodData,
fulfillmentItems,
fromOrder,
fulfillment
) {
const existing = const existing =
fromOrder.metadata && fromOrder.metadata.webshipper_order_id fromOrder.metadata && fromOrder.metadata.webshipper_order_id
@@ -228,13 +261,12 @@ class WebshipperFulfillmentService extends FulfillmentService {
}) })
} }
let visible_ref = `${fromOrder.display_id}-${ let id = fulfillment.id
fromOrder.fulfillments.length + 1 let visible_ref = `${fromOrder.display_id}-${id.substr(id.length - 4)}`
}` let ext_ref = `${fromOrder.id}.${fulfillment.id}`
let ext_ref = `${fromOrder._id}.${fromOrder.fulfillments.length}`
if (fromOrder.is_swap) { if (fromOrder.is_swap) {
ext_ref = `S${fromOrder._id}.${fromOrder.fulfillments.length}` ext_ref = `${fromOrder.id}.${fulfillment.id}`
visible_ref = `S-${fromOrder.display_id}` visible_ref = `S-${fromOrder.display_id}`
} }
@@ -247,17 +279,17 @@ class WebshipperFulfillmentService extends FulfillmentService {
visible_ref, visible_ref,
order_lines: fulfillmentItems.map((item) => { order_lines: fulfillmentItems.map((item) => {
return { return {
ext_ref: item._id, ext_ref: item.id,
sku: item.content.variant.sku, sku: item.variant.sku,
description: item.title, description: item.title,
quantity: item.quantity, quantity: item.quantity,
country_of_origin: country_of_origin:
item.content.variant.metadata && item.variant.origin_country ||
item.content.variant.metadata.origin_country, item.variant.product.origin_country,
tarif_number: tarif_number:
item.content.variant.metadata && item.variant.hs_code || item.variant.product.hs_code,
item.content.variant.metadata.hs_code, unit_price: item.unit_price / 100,
unit_price: item.content.unit_price, vat_percent: fromOrder.tax_rate,
} }
}), }),
delivery_address: { delivery_address: {
@@ -266,12 +298,12 @@ class WebshipperFulfillmentService extends FulfillmentService {
address_2: shipping_address.address_2, address_2: shipping_address.address_2,
zip: shipping_address.postal_code, zip: shipping_address.postal_code,
city: shipping_address.city, city: shipping_address.city,
country_code: shipping_address.country_code, country_code: shipping_address.country_code.toUpperCase(),
state: shipping_address.province, state: shipping_address.province,
phone: shipping_address.phone, phone: shipping_address.phone,
email: fromOrder.email, email: fromOrder.email,
}, },
currency: fromOrder.currency_code, currency: fromOrder.currency_code.toUpperCase(),
}, },
relationships: { relationships: {
order_channel: { order_channel: {
@@ -296,7 +328,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
zip: methodData.drop_point_zip, zip: methodData.drop_point_zip,
address_1: methodData.drop_point_address_1, address_1: methodData.drop_point_address_1,
city: methodData.drop_point_city, city: methodData.drop_point_city,
country_code: methodData.drop_point_country_code, country_code: methodData.drop_point_country_code.toUpperCase(),
} }
} }
if (invoice) { if (invoice) {
@@ -326,7 +358,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
const wsOrder = await this.retrieveRelationship( const wsOrder = await this.retrieveRelationship(
body.data.relationships.order body.data.relationships.order
) )
if (wsOrder.data.attributes.ext_ref) { if (wsOrder.data && wsOrder.data.attributes.ext_ref) {
const trackingNumbers = body.data.attributes.tracking_links.map( const trackingNumbers = body.data.attributes.tracking_links.map(
(l) => l.number (l) => l.number
) )
@@ -334,23 +366,44 @@ class WebshipperFulfillmentService extends FulfillmentService {
"." "."
) )
if (orderId.charAt(0) === "S") { if (orderId.charAt(0).toLowerCase() === "s") {
const swap = await this.swapService_.retrieve(orderId.substring(1)) if (fulfillmentIndex.startsWith("ful")) {
const fulfillment = swap.fulfillments[fulfillmentIndex] return this.swapService_.createShipment(
await this.swapService_.createShipment( orderId,
swap._id, fulfillmentIndex,
fulfillment._id,
trackingNumbers
)
} else {
const order = await this.orderService_.retrieve(orderId)
const fulfillment = order.fulfillments[fulfillmentIndex]
if (fulfillment) {
await this.orderService_.createShipment(
order._id,
fulfillment._id,
trackingNumbers trackingNumbers
) )
} else {
const swap = await this.swapService_.retrieve(orderId.substring(1), {
relations: ["fulfillments"],
})
const fulfillment = swap.fulfillments[fulfillmentIndex]
return this.swapService_.createShipment(
swap.id,
fulfillment.id,
trackingNumbers
)
}
} else {
if (fulfillmentIndex.startsWith("ful")) {
return this.orderService_.createShipment(
orderId,
fulfillmentIndex,
trackingNumbers
)
} else {
const order = await this.orderService_.retrieve(orderId, {
relations: ["fulfillments"],
})
const fulfillment = order.fulfillments[fulfillmentIndex]
if (fulfillment) {
return this.orderService_.createShipment(
order.id,
fulfillment.id,
trackingNumbers
)
}
} }
} }
} }
@@ -392,7 +445,7 @@ class WebshipperFulfillmentService extends FulfillmentService {
shipping_rate_id: id, shipping_rate_id: id,
delivery_address: { delivery_address: {
zip, zip,
country_code: countryCode, country_code: countryCode.toUpperCase(),
address_1: address1, address_1: address1,
}, },
}, },
+6 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-interfaces", "name": "medusa-interfaces",
"version": "1.0.14", "version": "1.0.14-alpha.170+0646bd3",
"description": "Core interfaces for Medusa", "description": "Core interfaces for Medusa",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -28,13 +28,14 @@
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jest": "^25.5.2", "jest": "^25.5.2",
"prettier": "^1.19.1" "prettier": "^1.19.1",
"typeorm": "^0.2.29"
}, },
"peerDependencies": { "peerDependencies": {
"mongoose": "5.x" "typeorm": "0.x"
}, },
"dependencies": { "dependencies": {
"medusa-core-utils": "^1.0.11" "medusa-core-utils": "^1.0.12-alpha.787+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -1,6 +1,39 @@
import BaseService from "../base-service" import BaseService from "../base-service"
import { In, Not } from "typeorm"
describe("BaseService", () => { describe("BaseService", () => {
describe("buildQuery_", () => {
const baseService = new BaseService()
it("successfully creates query", () => {
const q = baseService.buildQuery_(
{
id: "1234",
test1: ["123", "12", "1"],
test2: Not("this"),
rec: {
first: ["1", "2", "3"],
},
},
{
relations: ["1234"],
}
)
expect(q).toEqual({
where: {
id: "1234",
test1: In(["123", "12", "1"]),
test2: Not("this"),
rec: {
first: In(["1", "2", "3"]),
},
},
relations: ["1234"],
})
})
})
describe("addDecorator", () => { describe("addDecorator", () => {
const baseService = new BaseService() const baseService = new BaseService()
@@ -1,3 +1,6 @@
import { MedusaError } from "medusa-core-utils"
import { In, FindOperator, getManager } from "typeorm"
/** /**
* Common functionality for Services * Common functionality for Services
* @interface * @interface
@@ -7,6 +10,179 @@ class BaseService {
this.decorators_ = [] this.decorators_ = []
} }
withTransaction() {
console.log("WARN: withTransaction called without custom implementation")
return this
}
/**
* Used to build TypeORM queries.
*/
buildQuery_(selector, config = {}) {
const build = obj => {
const where = Object.entries(obj).reduce((acc, [key, value]) => {
switch (true) {
case value instanceof FindOperator:
acc[key] = value
break
case Array.isArray(value):
acc[key] = In([...value])
break
case value !== null && typeof value === "object":
acc[key] = build(value)
break
default:
acc[key] = value
break
}
return acc
}, {})
return where
}
const query = {
where: build(selector),
}
if ("skip" in config) {
query.skip = config.skip
}
if ("take" in config) {
query.take = config.take
}
if ("relations" in config) {
query.relations = config.relations
}
if ("select" in config) {
query.select = config.select
}
if ("order" in config) {
query.order = config.order
}
return query
}
/**
* Confirms whether a given raw id is valid. Fails if the provided
* id is null or undefined. The validate function takes an optional config
* param, to support checking id prefix and length.
* @param {string} rawId - the id to validate.
* @param {object?} config - optional config
* @returns {string} the rawId given that nothing failed
*/
validateId_(rawId, config = {}) {
const { prefix, length } = config
if (!rawId) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Failed to validate id: ${rawId}`
)
}
if (prefix || length) {
const [pre, rand] = rawId.split("_")
if (prefix && pre !== prefix) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to prefix constraint: ${prefix}`
)
}
if (length && length !== rand.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The provided id: ${rawId} does not adhere to length constraint: ${length}`
)
}
}
return rawId
}
shouldRetryTransaction(err) {
const code = typeof err === "object" ? String(err.code) : null
return code === "40001" || code === "40P01"
}
/**
* Wraps some work within a transactional block. If the service already has
* a transaction manager attached this will be reused, otherwise a new
* transaction manager is created.
* @param {function} work - the transactional work to be done
* @param {string} isolation - the isolation level to be used for the work.
* @return {any} the result of the transactional work
*/
async atomicPhase_(work, isolation) {
if (this.transactionManager_) {
return work(this.transactionManager_)
} else {
const temp = this.manager_
const doWork = async m => {
this.manager_ = m
this.transactionManager_ = m
try {
const result = await work(m)
this.manager_ = temp
this.transactionManager_ = undefined
return result
} catch (error) {
this.manager_ = temp
this.transactionManager_ = undefined
throw error
}
}
if (isolation) {
let result
try {
result = await this.manager_.transaction(isolation, m => doWork(m))
return result
} catch (error) {
if (this.shouldRetryTransaction(error)) {
return this.manager_.transaction(isolation, m => doWork(m))
} else {
throw error
}
}
}
return this.manager_.transaction(m => doWork(m))
}
}
/**
* Dedicated method to set metadata.
* @param {string} obj - the entity to apply metadata to.
* @param {object} metadata - the metadata to set
* @return {Promise} resolves to the updated result.
*/
setMetadata_(obj, metadata) {
const existing = obj.metadata || {}
const newData = {}
for (const [key, value] of Object.entries(metadata)) {
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
newData[key] = value
}
const updated = {
...existing,
...newData,
}
return updated
}
/** /**
* Adds a decorator to a service. The decorator must be a function and should * Adds a decorator to a service. The decorator must be a function and should
* return a decorated object. * return a decorated object.
@@ -60,7 +60,7 @@ class BaseFulfillmentService extends BaseService {
throw Error("calculatePrice must be overridden by the child class") throw Error("calculatePrice must be overridden by the child class")
} }
createOrder() { createFulfillment() {
throw Error("createOrder must be overridden by the child class") throw Error("createOrder must be overridden by the child class")
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-payment-adyen", "name": "medusa-payment-adyen",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Adyen Payment provider for Medusa Commerce", "description": "Adyen Payment provider for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -24,7 +24,7 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jest": "^25.5.2", "jest": "^25.5.2",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"scripts": { "scripts": {
"build": "babel src -d .", "build": "babel src -d .",
@@ -42,7 +42,7 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11" "medusa-core-utils": "^1.0.12-alpha.787+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -0,0 +1,17 @@
export default async (req, res) => {
const adyenService = req.scope.resolve("adyenService")
const eventBus = req.scope.resolve("eventBusService")
const notification = req.body
const event = notification.notificationItems[0].NotificationRequestItem
const valid = adyenService.validateNotification(event)
if (!valid) {
res.status(401).send(`Unauthorized webhook event`)
return
}
eventBus.emit("adyen.notification_received", event)
res.status(200).send("[accepted]")
}
@@ -1,41 +0,0 @@
export default async (req, res) => {
const adyenService = req.scope.resolve("adyenService")
const notification = req.body
const event = notification.notificationItems[0].NotificationRequestItem
const valid = adyenService.validateNotification(event)
if (!valid) {
res.status(401).send(`Unauthorized webhook event`)
return
}
if (event.success === "true" && event.eventCode === "AUTHORISATION") {
const orderService = req.scope.resolve("orderService")
const cartService = req.scope.resolve("cartService")
const cartId = event.additionalData["metadata.cart_id"]
try {
const order = await orderService.retrieveByCartId(cartId)
console.log(order)
} catch (error) {
const cart = await cartService.retrieve(cartId)
await orderService.createFromCart(cart)
}
// Create from cart
res.status(200).send("[accepted]")
return
}
if (event.success === "true" && event.eventCode === "CAPTURE") {
// Create from cart
console.log("Captured")
res.status(200).send("[accepted]")
return
}
res.status(200).send("[accepted]")
}
@@ -18,12 +18,12 @@ export default (app, rootDirectory) => {
}) })
) )
app.use("/adyen-hooks", route) app.use("/adyen/webhooks", route)
route.post( route.post(
"/capture", "/notification",
bodyParser.json(), bodyParser.json(),
middlewares.wrap(require("./capture-hook").default) middlewares.wrap(require("./adyen-notification").default)
) )
return app return app
@@ -1,58 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
cart_id: Validator.string().required(),
provider_id: Validator.string().required(),
payment_data: Validator.object().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const cartService = req.scope.resolve("cartService")
const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`)
const cart = await cartService.retrieve(value.cart_id)
const { data } = await paymentProvider.authorizePayment(
cart,
value.payment_data.paymentMethod
)
const transactionReference = data.pspReference
let newPaymentSession = cart.payment_sessions.find(
(ps) => ps.provider_id === value.provider_id
)
newPaymentSession = {
...newPaymentSession,
data,
}
await cartService.setMetadata(
cart._id,
"adyen_transaction_ref",
transactionReference
)
await cartService.updatePaymentSession(
cart._id,
value.provider_id,
newPaymentSession
)
await cartService.setPaymentMethod(cart._id, {
provider_id: value.provider_id,
data,
})
res.status(200).json({ data })
} catch (err) {
throw err
}
}
@@ -1,30 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
payload: Validator.string().required(),
payment_data: Validator.string().required(),
provider_id: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const adyen = req.scope.resolve("adyenService")
const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`)
const { data } = await adyen.checkPaymentResult(
value.payment_data,
value.payload
)
const status = await paymentProvider.getStatus(data)
res.status(200).json({ status })
} catch (err) {
throw err
}
}
@@ -26,29 +26,5 @@ export default (app, rootDirectory) => {
middlewares.wrap(require("./retrieve-payment-methods").default) middlewares.wrap(require("./retrieve-payment-methods").default)
) )
route.post(
"/authorize",
bodyParser.json(),
middlewares.wrap(require("./authorize-payment").default)
)
route.post(
"/update",
bodyParser.json(),
middlewares.wrap(require("./update-payment").default)
)
route.post(
"/payment-status",
bodyParser.json(),
middlewares.wrap(require("./check-payment-status").default)
)
route.get(
"/payment-status",
bodyParser.json(),
middlewares.wrap(require("./check-payment-status").default)
)
return app return app
} }
@@ -13,39 +13,44 @@ export default async (req, res) => {
try { try {
const adyenService = req.scope.resolve("adyenService") const adyenService = req.scope.resolve("adyenService")
const cartService = req.scope.resolve("cartService") const cartService = req.scope.resolve("cartService")
const regionService = req.scope.resolve("regionService")
const totalsService = req.scope.resolve("totalsService")
const cart = await cartService.retrieve(value.cart_id) const cart = await cartService.retrieve(value.cart_id, {
const region = await regionService.retrieve(cart.region_id) select: ["total"],
const total = await totalsService.getTotal(cart) relations: ["region", "region.payment_providers", "payment_sessions"],
})
const allowedMethods = cart.payment_sessions.map( const allowedMethods = cart.payment_sessions.map((ps) => {
(ps) => ps.provider_id.split("Adyen")[0] if (ps.provider_id.includes("adyen")) {
) return ps.provider_id.split("-adyen")[0]
}
})
if (allowedMethods.length === 0) { if (allowedMethods.length === 0) {
res.status(200).json({ paymentMethods: {} }) res.status(200).json({ paymentMethods: {} })
return return
} }
const { data } = await adyenService.retrievePaymentMethods( const pmMethods = await adyenService.retrievePaymentMethods(
cart,
allowedMethods, allowedMethods,
total, cart.total,
region.currency_code cart.currency_code,
cart.customer_id || ""
) )
// Adyen does not behave 100% correctly in regards to allowed methods // Adyen does not behave 100% correctly in regards to allowed methods
// Therefore, we sanity filter before sending them to the storefront // Therefore, we sanity filter before sending them to the storefront
const { paymentMethods, groups } = data const { paymentMethods, groups, storedPaymentMethods } = pmMethods
const methods = paymentMethods.filter((pm) => const methods = paymentMethods.filter((pm) =>
allowedMethods.includes(pm.type) allowedMethods.includes(pm.type)
) )
res const response = {
.status(200) paymentMethods: methods,
.json({ paymentMethods: { paymentMethods: methods, groups } }) groups,
storedPaymentMethods,
}
res.status(200).json({ payment_methods: response })
} catch (err) { } catch (err) {
throw err throw err
} }
@@ -1,32 +0,0 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
cart_id: Validator.string().required(),
provider_id: Validator.string().required(),
payment_data: Validator.object().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const cartService = req.scope.resolve("cartService")
const paymentProvider = req.scope.resolve(`pp_${value.provider_id}`)
const cart = await cartService.retrieve(value.cart_id)
const { data } = await paymentProvider.updatePayment(
value.payment_data.paymentData,
value.payment_data.details
)
await cartService.updatePaymentSession(cart._id, value.provider_id, data)
res.status(200).json({ data })
} catch (err) {
throw err
}
}
@@ -2,72 +2,141 @@ import axios from "axios"
import _ from "lodash" import _ from "lodash"
import { hmacValidator } from "@adyen/api-library" import { hmacValidator } from "@adyen/api-library"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { Client, Config, CheckoutAPI } from "@adyen/api-library"
class AdyenService extends BaseService { class AdyenService extends BaseService {
constructor({ regionService, cartService, totalsService }, options) { constructor({ cartService }, options) {
super() super()
this.regionService_ = regionService /** @private @constant {CartService} */
this.cartService_ = cartService this.cartService_ = cartService
this.totalsService_ = totalsService /**
* {
* api_key: "",
* notification_hmac: "",
* return_url: "",
* merchant_account: "",
* origin: "",
* environment: "",
* live_endpoint_prefix: "",
* payment_endpoint: ""
* }
*/
this.options_ = options this.options_ = options
this.adyenCheckoutApi = axios.create({ /** @private @constant {AxiosClient} */
baseURL: "https://checkout-test.adyen.com/v53", this.adyenClient_ = this.initAdyenClient()
headers: {
"Content-Type": "application/json", /** @private @constant {AdyenClient} */
"x-API-key": this.options_.api_key, this.adyenPaymentApi = this.initPaymentClient()
}, }
withTransaction(transactionManager) {
if (!transactionManager) {
return this
}
const cloned = new AdyenService({
cartService: this.cartService_,
totalsService: this.totalsService_,
}) })
this.adyenPaymentApi = axios.create({ this.transactionManager_ = transactionManager
baseURL: "https://pal-test.adyen.com/pal/servlet/Payment/v52",
headers: { return cloned
"Content-Type": "application/json",
"x-API-key": this.options_.api_key,
},
})
} }
getOptions() { getOptions() {
return this.options_ return this.options_
} }
validateNotification(event) { initPaymentClient() {
return axios.create({
baseURL: this.options_.payment_endpoint,
headers: {
"Content-Type": "application/json",
"x-API-key": this.options_.api_key,
},
})
}
initAdyenClient() {
const config = new Config()
config.apiKey = this.options_.api_key
config.merchantAccount = this.options_.merchant_account
const client = new Client({
config,
})
client.setEnvironment(
this.options_.environment,
this.options_.live_endpoint_prefix
)
return client
}
/**
* Validates an Adyen webhook notification
* @param {object} notification - notification to validate
* @returns {string} the status of the payment
*/
validateNotification(notification) {
const validator = new hmacValidator() const validator = new hmacValidator()
const validated = validator.validateHMAC( const validated = validator.validateHMAC(
event, notification,
this.options_.notification_hmac this.options_.notification_hmac
) )
return validated return validated
} }
/** /**
* Retrieve payment methods from Ayden using country as filter. * Retrieve stored payment methods from Adyen.
* @param {string} countryCode - country code of cart * @param {Customer} customer - customer to retrieve methods for
* @param {string} shopperLocale - locale used on website * @returns {Promise} result containing the stored payment methods from Adyen
* @returns {string} the status of the payment
*/ */
async retrievePaymentMethods(cart, allowedMethods, total, currency) { async retrieveSavedMethods(customer) {
let request = { let request = {
allowedPaymentMethods: allowedMethods,
amount: {
value: total * 100,
currency: currency,
},
merchantAccount: this.options_.merchant_account, merchantAccount: this.options_.merchant_account,
channel: this.options_.channel, channel: "Web",
} shopperReference: customer.id,
if (cart.customer_id) {
request.shopperReference = cart.customer_id
} }
try { try {
return await this.adyenCheckoutApi.post("/paymentMethods", request) const checkout = new CheckoutAPI(this.adyenClient_)
const methods = await checkout.paymentMethods(request)
return methods.storedPaymentMethods || []
} catch (error) {
throw error
}
}
/**
* Retrieve payment methods from Adyen.
* @param {string[]} allowedMethods - the allowed methods based on region
* @param {number} total - total amount to be paid with payment methods
* @param {string} currency - currency code to use for the payment
* @param {string} customerId - id of the customer paying
* @returns {Promise} result containing the payment methods from Adyen
*/
async retrievePaymentMethods(allowedMethods, total, currency, customerId) {
let request = {
allowedPaymentMethods: allowedMethods,
amount: {
value: total,
currency: currency,
},
merchantAccount: this.options_.merchant_account,
channel: "Web",
shopperReference: customerId,
}
try {
const checkout = new CheckoutAPI(this.adyenClient_)
return checkout.paymentMethods(request)
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -75,89 +144,218 @@ class AdyenService extends BaseService {
/** /**
* Status for Adyen payment. * Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart * @param {object} paymentData - payment method data from cart
* @returns {string} the status of the payment * @returns {string} the status of the payment
*/ */
async getStatus(_) { getStatus(paymentData) {
let status = "initial" const { resultCode } = paymentData
let status = "pending"
if (resultCode === "Pending") {
return status
}
if (resultCode === "Refused") {
return status
}
if (resultCode === "Error") {
status = "error"
}
if (resultCode === "Authorised") {
status = "authorized"
}
if (resultCode === "Canceled") {
status = "canceled"
}
if (resultCode === "ChallengeShopper") {
status = "requires_more"
}
if (resultCode === "RedirectShopper") {
status = "requires_more"
}
if (resultCode === "IdentifyShopper") {
status = "requires_more"
}
return status return status
} }
/** /**
* Creates Adyen payment object. * Creates Adyen payment object.
* @param {any} _ - placeholder object * @param {Cart} cart - cart to initiate payment for
* @returns {Object} empty payment data * @returns {object} empty payment data
*/ */
async createPayment(_) { async createPayment(cart) {
return {} return { cart_id: cart.id }
}
async retrievePayment(data) {
return data
}
async updatePayment(paymentData, details) {
const request = {
paymentData,
details,
}
return this.adyenCheckoutApi.post("/payments/details", request)
} }
/** /**
* Creates and authorizes an Ayden payment * Retrieves Adyen payment. This is not supported by adyen, so we simply
* @returns {Object} payment data result * return the current payment method data
* @param {object} data - payment session
* @returns {object} payment method data
*/ */
async authorizePayment(cart, paymentMethod) { async getPaymentData(paymentSession) {
const region = await this.regionService_.retrieve(cart.region_id) return { ...paymentSession.data }
const total = await this.totalsService_.getTotal(cart) }
/**
* Retrieves Adyen payment. This is not supported by adyen, so we simply
* return the current payment method data
* @param {object} sessionData - the data of the payment to retrieve
* @returns {Promise<object>} Stripe payment intent
*/
async retrieve(sessionData) {
return sessionData
}
/**
* Creates and authorizes an Adyen payment.
* Requires cart_id in context for authorization.
* Return status of authorization result.
* @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(session, context) {
const sessionData = session.data
const status = this.getStatus(sessionData)
// If session data is present, we already called authorize once.
// Therefore, this is most likely a call for getting additional details
if (status === "requires_more") {
const updated = await this.updatePaymentData(sessionData, {
details: sessionData.details,
paymentData: sessionData.paymentData,
})
return { data: updated, status: this.getStatus(updated) }
}
if (status === "authorized") {
return { data: sessionData, status: "authorized" }
}
const cart = await this.cartService_.retrieve(session.cart_id, {
select: ["total"],
relations: ["region", "shipping_address"],
})
const amount = {
currency: cart.region.currency_code.toUpperCase(),
value: cart.total,
}
let request = { let request = {
amount: { amount,
currency: region.currency_code, shopperIP: context.ip_address || "",
value: total * 100,
},
shopperReference: cart.customer_id, shopperReference: cart.customer_id,
paymentMethod, paymentMethod: sessionData.paymentData.paymentMethod,
reference: cart._id, reference: cart.id,
merchantAccount: this.options_.merchant_account, merchantAccount: this.options_.merchant_account,
returnUrl: this.options_.return_url, returnUrl: this.options_.return_url,
origin: this.options_.origin,
channel: "Web",
redirectFromIssuerMethod: "GET",
browserInfo: sessionData.browserInfo || {},
billingAddress: {
city: cart.shipping_address.city,
country: cart.shipping_address.country_code,
houseNumberOrName: cart.shipping_address.address_2 || "",
postalCode: cart.shipping_address.postal_code,
stateOrProvice: cart.shipping_address.province || "",
street: cart.shipping_address.address_1,
},
metadata: { metadata: {
cart_id: cart._id, cart_id: cart.id,
}, },
} }
if (paymentMethod.storedPaymentMethodId) { // If customer chose to save the payment method
if (sessionData.storePaymentMethod) {
request.storePaymentMethod = "true"
request.shopperInteraction = "Ecommerce" request.shopperInteraction = "Ecommerce"
request.recurringProcessingModel = "CardOnFile" request.recurringProcessingModel = "CardOnFile"
} }
return await this.adyenCheckoutApi.post("/payments", request) const checkout = new CheckoutAPI(this.adyenClient_)
try {
const authorizedPayment = await checkout.payments(request, {
idempotencyKey: context.idempotency_key || "",
})
return {
data: authorizedPayment,
status: this.getStatus(authorizedPayment),
}
} catch (error) {
throw error
}
} }
async checkPaymentResult(paymentData, payload) { async updatePaymentData(sessionData, update) {
const request = { if (_.isEmpty(update.details)) {
paymentData, return { ...sessionData, ...update }
details: {
payload,
},
} }
return this.adyenCheckoutApi.post("/payments/details", request)
const checkout = new CheckoutAPI(this.adyenClient_)
const updated = await checkout.paymentsDetails(update)
return updated
} }
/** /**
* Captures an Ayden payment * Updates an Adyen payment.
* @param {Object} data - payment data to capture * @param {object} paymentData - payment data to update
* @returns {Object} payment data result of capture * @param {details} details - details to update
* @returns {Promise} result of the update operation
*/ */
async capturePayment(data) { async updatePayment(paymentData, details) {
const { pspReference, amount } = data return paymentData
}
/**
* Additional details
* @param {object} paymentData - payment data
* @param {object} details - payment details
* @returns {Promise} current payment result
*/
async additionalDetails(paymentData, details) {
const request = {
paymentData,
details,
}
const checkout = new CheckoutAPI(this.adyenClient_)
return checkout.paymentsDetails(request)
}
/**
* Captures an Adyen payment
* @param {object} payment - payment to capture
* @returns {string} status = processing_captures
*/
async capturePayment(payment) {
const { pspReference, merchantReference } = payment.data
const { amount, currency_code } = payment
try { try {
const captured = await this.adyenPaymentApi.post("/capture", { const captured = await this.adyenPaymentApi.post("/capture", {
originalReference: pspReference, originalReference: pspReference,
modificationAmount: amount, modificationAmount: {
value: amount,
currency: currency_code.toUpperCase(),
},
merchantAccount: this.options_.merchant_account, merchantAccount: this.options_.merchant_account,
reference: merchantReference,
}) })
if ( if (
@@ -170,40 +368,55 @@ class AdyenService extends BaseService {
) )
} }
return captured return { originalReference: pspReference, ...captured.data }
} catch (error) { } catch (error) {
console.log(error)
throw error throw error
} }
} }
/** /**
* Refunds an Ayden payment * Refunds an Adyen payment
* @param {Object} paymentData - payment data to refund * @param {object} payment - payment to refund
* @param {number} amountToRefund - amount to refund * @param {number} amountToRefund - amount to refund
* @returns {Object} payment data result of refund * @returns {object} payment data result of refund
*/ */
async refundPayment(data) { async refundPayment(payment, amountToRefund) {
const { pspReference, amount } = data const { originalReference, merchantReference } = payment.data
const { currency_code } = payment
const refundAmount = {
currency: currency_code.toUpperCase(),
value: amountToRefund,
}
try { try {
return this.adyenPaymentApi.post("/refund", { const refunded = await this.adyenPaymentApi.post("/refund", {
originalReference: pspReference, originalReference,
merchantAccount: this.options_.merchant_account, merchantAccount: this.options_.merchant_account,
modificationAmount: amount, modificationAmount: refundAmount,
reference: merchantReference,
}) })
return { originalReference, ...refunded.data }
} catch (error) { } catch (error) {
throw error throw error
} }
} }
/** /**
* Cancels an Ayden payment * Adyen does not have a way of deleting payments, hence the empty impl.
* @param {Object} paymentData - payment data to cancel
* @returns {Object} payment data result of cancel
*/ */
async cancelPayment(paymentData) { async deletePayment(_) {
const { pspReference } = paymentData return {}
}
/**
* Cancels an Adyen payment.
* @param {object} payment - payment to cancel
* @returns {object} payment data result of cancel
*/
async cancelPayment(payment) {
const { pspReference } = payment.data
try { try {
return this.adyenPaymentApi.post("/cancel", { return this.adyenPaymentApi.post("/cancel", {
@@ -0,0 +1,104 @@
import _ from "lodash"
import https from "https"
import fs from "fs"
import axios from "axios"
import { PaymentService } from "medusa-interfaces"
class ApplePayAdyenService extends PaymentService {
static identifier = "applepay-adyen"
constructor({ adyenService }, options) {
super()
this.adyenService_ = adyenService
this.options_ = options
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async getApplePaySession(validationUrl) {
let certificate
try {
// Place certificate in root folder
certificate = fs.readFileSync("./apple-pay-cert.pem")
} catch (error) {
throw new Error(
"Could not find ApplePay certificate. Make sure to place it in root folder of your server"
)
}
const httpsAgent = new https.Agent({
cert: certificate,
key: certificate,
rejectUnauthorized: false,
})
const request = {
merchantIdentifier: this.options_.applepay_merchant_id,
displayName: this.options_.applepay_display_name,
initiative: "web",
initiativeContext: this.options_.applepay_initiative_context,
}
return axios.post(validationUrl, request, {
httpsAgent,
})
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default ApplePayAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class ApplePayAdyenService extends PaymentService {
static identifier = "applepayAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default ApplePayAdyenService
@@ -0,0 +1,62 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class CardAdyenService extends PaymentService {
static identifier = "scheme-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async retrieveSavedMethods(customer) {
return this.adyenService_.retrieveSavedMethods(customer)
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default CardAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class CardAdyenService extends PaymentService {
static identifier = "schemeAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default CardAdyenService
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class GooglePayAdyenService extends PaymentService {
static identifier = "paywithgoogle-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default GooglePayAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class GooglePayAdyenService extends PaymentService {
static identifier = "googlepayAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default GooglePayAdyenService
@@ -0,0 +1,62 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class IdealAdyenService extends PaymentService {
static identifier = "ideal-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default IdealAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class IdealAdyenService extends PaymentService {
static identifier = "idealAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default IdealAdyenService
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class MobilePayAdyenService extends PaymentService {
static identifier = "mobilepay-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default MobilePayAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class MobilePayAdyenService extends PaymentService {
static identifier = "mobilepayAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
// let status = "initial"
// if (resultCode === "Authorised") {
// status = "authorized"
// }
return "authorized"
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default MobilePayAdyenService
@@ -0,0 +1,58 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class PayPalAdyenService extends PaymentService {
static identifier = "paypal-adyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
async getStatus(paymentData) {
return this.adyenService_.getStatus(paymentData)
}
async createPayment(data) {
return this.adyenService_.createPayment(data)
}
async authorizePayment(sessionData, context) {
return this.adyenService_.authorizePayment(sessionData, context)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async getPaymentData(data) {
return this.adyenService_.getPaymentData(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async updatePaymentData(sessionData, update) {
return this.adyenService_.updatePaymentData(sessionData, update)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
return this.adyenService_.capturePayment(data)
}
async refundPayment(data, amountToRefund) {
return this.adyenService_.refundPayment(data, amountToRefund)
}
async cancelPayment(data) {
return this.adyenService_.cancelPayment(data)
}
}
export default PayPalAdyenService
@@ -1,74 +0,0 @@
import _ from "lodash"
import { PaymentService } from "medusa-interfaces"
class PayPalAdyenService extends PaymentService {
static identifier = "paypalAdyen"
constructor({ adyenService }) {
super()
this.adyenService_ = adyenService
}
/**
* Status for Adyen payment.
* @param {Object} paymentData - payment method data from cart
* @returns {string} the status of the payment
*/
async getStatus(paymentData) {
const { resultCode } = paymentData
let status = "initial"
if (resultCode === "Authorised") {
status = "authorized"
}
return status
}
async createPayment(_) {
return {}
}
async authorizePayment(cart, paymentMethod) {
return this.adyenService_.authorizePayment(cart, paymentMethod)
}
async retrievePayment(data) {
return this.adyenService_.retrievePayment(data)
}
async updatePayment(data, _) {
return this.adyenService_.updatePayment(data)
}
async deletePayment(data) {
return this.adyenService_.deletePayment(data)
}
async capturePayment(data) {
try {
return this.adyenService_.capturePayment(data)
} catch (error) {
throw error
}
}
async refundPayment(data) {
try {
return this.adyenService_.refundPayment(data)
} catch (error) {
throw error
}
}
async cancelPayment(data) {
try {
return this.adyenService_.cancelPayment(data)
} catch (error) {
throw error
}
}
}
export default PayPalAdyenService
@@ -0,0 +1,159 @@
class AdyenSubscriber {
constructor(container) {
/** @private @const {CartService} */
this.cartService_ = container.cartService
/** @private @const {OrderService} */
this.orderService_ = container.orderService
/** @private @const {EventBusService} */
this.eventBus_ = container.eventBusService
/** @private @const {PaymentRepository} */
this.paymentRepository_ = container.paymentRepository
this.manager_ = container.manager
this.eventBus_.subscribe(
"adyen.notification_received",
async (notification) => this.handleAdyenNotification(notification)
)
}
/**
* Webhook handler for Adyen payment.
* @param {object} notification - webhook notification object
* @returns {string} the status of the payment intent
*/
async handleAdyenNotification(notification) {
switch (true) {
case notification.success === "true" &&
notification.eventCode === "AUTHORISATION": {
this.handleAuthorization_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "AUTHORISATION": {
this.handleFailedAuthorization_(notification)
break
}
case notification.success === "true" &&
notification.eventCode === "CAPTURE_FAILED": {
this.handleFailedCapture_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "CAPTURE": {
this.handleFailedCapture_(notification)
break
}
case notification.success === "false" &&
notification.eventCode === "REFUND": {
this.handleFailedRefund_(notification)
break
}
default:
break
}
}
async handleFailedAuthorization_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const cart = await this.cartService_.retrieve(cartId, {
relations: ["payment_sessions"],
})
const { payment_session } = cart
const updated = {
...payment_session,
status: "error",
data: {
...payment_session.data,
pspReference: notification.pspReference,
},
}
await this.cartService_.updatePaymentSession(cart.id, updated)
}
async handleAuthorization_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const paymentRepository = this.manager_.getCustomRepository(
this.paymentRepository_
)
// We need to ensure, that an order is created in situations, where the
// customer might have closed their browser prior to order creation
try {
const orderPayment = await paymentRepository.findOne({
where: { cart_id: cartId },
})
if (!orderPayment) {
throw new Error("Payment not found")
}
const updatedPayment = {
...orderPayment,
data: {
...orderPayment.data,
resultCode: "Authorised",
pspReference: notification.pspReference,
},
}
await this.paymentRepository_.save(updatedPayment)
} catch (error) {
console.log(error)
await this.manager_.transaction(async (manager) => {
const session = {
pspReference: notification.pspReference,
resultCode: "Authorised",
}
await this.cartService_
.withTransaction(manager)
.updatePaymentSession(cartId, session)
await this.cartService_
.withTransaction(manager)
.authorizePayment(cartId)
await this.orderService_.withTransaction(manager).createFromCart(cartId)
})
}
}
async handleFailedCapture_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const order = await this.orderService_.retrieveByCartId(cartId)
await this.orderService_.update(order.id, {
payment_status: "requires_action",
})
await this.eventBus_.emit("order.payment_capture_failed", {
order,
error: new Error(`Adyen payment capture: ${notification.reason}`),
})
}
async handleFailedRefund_(notification) {
const cartId = notification.additionalData["metadata.cart_id"]
const order = await this.orderService_.retrieveByCartId(cartId)
await this.orderService_.update(order.id, {
payment_status: "requires_action",
})
await this.eventBus_.emit("order.refund_failed", {
order,
error: new Error(`Adyen payment capture: ${notification.reason}`),
})
}
}
export default AdyenSubscriber
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-payment-klarna", "name": "medusa-payment-klarna",
"version": "1.0.14", "version": "1.0.13-alpha.176+0646bd3",
"description": "Klarna Payment provider for Medusa Commerce", "description": "Klarna Payment provider for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -40,8 +40,8 @@
"axios": "^0.21.0", "axios": "^0.21.0",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -2,13 +2,17 @@ import { IdMap } from "medusa-test-utils"
export const carts = { export const carts = {
frCart: { frCart: {
_id: IdMap.getId("fr-cart"), id: IdMap.getId("fr-cart"),
email: "lebron@james.com", email: "lebron@james.com",
title: "test", title: "test",
region: {
tax_rate: 2500,
currency_code: "eur",
},
region_id: IdMap.getId("region-france"), region_id: IdMap.getId("region-france"),
items: [ items: [
{ {
_id: IdMap.getId("line"), id: IdMap.getId("line"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
@@ -16,20 +20,20 @@ export const carts = {
{ {
unit_price: 8, unit_price: 8,
variant: { variant: {
_id: IdMap.getId("eur-8-us-10"), id: IdMap.getId("eur-8-us-10"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
{ {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: IdMap.getId("eur-10-us-12"), id: IdMap.getId("eur-10-us-12"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
@@ -37,17 +41,17 @@ export const carts = {
quantity: 10, quantity: 10,
}, },
{ {
_id: IdMap.getId("existingLine"), id: IdMap.getId("existingLine"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: { content: {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: IdMap.getId("eur-10-us-12"), id: IdMap.getId("eur-10-us-12"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
@@ -56,13 +60,16 @@ export const carts = {
], ],
shipping_methods: [ shipping_methods: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
data: {
name: "test",
},
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
shipping_options: [ shipping_options: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
@@ -87,7 +94,7 @@ export const carts = {
discounts: [ discounts: [
{ {
code: "MEDUSA_FREE", code: "MEDUSA_FREE",
discount_rule: { rule: {
type: "percent", type: "percent",
value: 20, value: 20,
allocation: "item", allocation: "item",
@@ -3,38 +3,107 @@ export default async (req, res) => {
const { shipping_address, merchant_data } = req.body const { shipping_address, merchant_data } = req.body
try { try {
const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService") const cartService = req.scope.resolve("cartService")
const klarnaProviderService = req.scope.resolve("pp_klarna") const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService") const shippingProfileService = req.scope.resolve("shippingProfileService")
const cart = await cartService.retrieve(merchant_data) const result = await manager.transaction("SERIALIZABLE", async (m) => {
const cart = await cartService.retrieve(merchant_data, {
select: ["subtotal"],
relations: [
"shipping_address",
"billing_address",
"region",
"items",
"shipping_methods",
"shipping_methods.shipping_option",
"items.variant",
"items.variant.product",
],
})
if (shipping_address) { if (shipping_address) {
const updatedAddress = { const shippingAddress = {
first_name: shipping_address.given_name, ...cart.shipping_address,
last_name: shipping_address.family_name, first_name: shipping_address.given_name,
address_1: shipping_address.street_address, last_name: shipping_address.family_name,
address_2: shipping_address.street_address2, address_1: shipping_address.street_address,
city: shipping_address.city, address_2: shipping_address.street_address2,
country_code: shipping_address.country.toUpperCase(), city: shipping_address.city,
postal_code: shipping_address.postal_code, country_code: shipping_address.country,
phone: shipping_address.phone 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,
}
if (cart.billing_address) {
billingAddress = {
...billingAddress,
...cart.billing_address,
}
}
await cartService.update(cart.id, {
shipping_address: shippingAddress,
billing_address: billingAddress,
email: shipping_address.email,
})
const shippingOptions = await shippingProfileService.fetchCartOptions(
cart
)
if (shippingOptions?.length) {
const option = shippingOptions.find(
(o) => o.data && !o.data.require_drop_point
)
await cartService
.withTransaction(m)
.addShippingMethod(cart.id, option.id, option.data)
}
// Fetch and return updated Klarna order
const updatedCart = await cartService
.withTransaction(m)
.retrieve(cart.id, {
select: [
"gift_card_total",
"subtotal",
"total",
"tax_total",
"discount_total",
"subtotal",
],
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
return klarnaProviderService.cartToKlarnaOrder(updatedCart)
} else {
return null
} }
})
await cartService.updateShippingAddress(cart._id, updatedAddress) if (result) {
await cartService.updateBillingAddress(cart._id, updatedAddress) res.json(result)
await cartService.updateEmail(cart._id, shipping_address.email)
const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
if (shippingOptions.length === 1) {
const option = shippingOptions[0]
await cartService.addShippingMethod(cart._id, option._id, option.data)
}
// Fetch and return updated Klarna order
const updatedCart = await cartService.retrieve(cart._id)
const order = await klarnaProviderService.cartToKlarnaOrder(updatedCart)
res.json(order)
return return
} else { } else {
res.sendStatus(400) res.sendStatus(400)
@@ -4,7 +4,6 @@ export default async (req, res) => {
const { klarna_order_id } = req.query const { klarna_order_id } = req.query
try { try {
const cartService = req.scope.resolve("cartService")
const orderService = req.scope.resolve("orderService") const orderService = req.scope.resolve("orderService")
const klarnaProviderService = req.scope.resolve("pp_klarna") const klarnaProviderService = req.scope.resolve("pp_klarna")
@@ -13,22 +12,9 @@ export default async (req, res) => {
) )
const cartId = klarnaOrder.merchant_data const cartId = klarnaOrder.merchant_data
try { const order = await orderService.retrieveByCartId(cartId)
const order = await orderService.retrieveByCartId(cartId)
await klarnaProviderService.acknowledgeOrder( await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order.id)
klarnaOrder.order_id,
order._id
)
} catch (err) {
if (err.type === MedusaError.Types.NOT_FOUND) {
const cart = await cartService.retrieve(cartId)
const order = await orderService.createFromCart(cart)
await klarnaProviderService.acknowledgeOrder(
klarnaOrder.order_id,
order._id
)
}
}
res.sendStatus(200) res.sendStatus(200)
} catch (error) { } catch (error) {
@@ -7,19 +7,57 @@ export default async (req, res) => {
const klarnaProviderService = req.scope.resolve("pp_klarna") const klarnaProviderService = req.scope.resolve("pp_klarna")
const shippingProfileService = req.scope.resolve("shippingProfileService") const shippingProfileService = req.scope.resolve("shippingProfileService")
const cart = await cartService.retrieve(merchant_data) const cart = await cartService.retrieve(merchant_data, {
const shippingOptions = await shippingProfileService.fetchCartOptions(cart) select: ["subtotal"],
relations: [
"shipping_address",
"billing_address",
"region",
"shipping_methods",
"shipping_methods.shipping_option",
"items",
"items.variant",
"items.variant.product",
],
})
let shippingOptions = await shippingProfileService.fetchCartOptions(cart)
shippingOptions = shippingOptions.filter(
(so) => !so.data?.require_drop_point
)
const ids = selected_shipping_option.id.split(".") const ids = selected_shipping_option.id.split(".")
await Promise.all(ids.map(async id => { await Promise.all(
const option = shippingOptions.find(({ _id }) => _id.equals(id)) ids.map(async (id) => {
const option = shippingOptions.find((so) => so.id === id)
if (option) { if (option) {
await cartService.addShippingMethod(cart._id, option._id, option.data) await cartService.addShippingMethod(cart.id, option.id, option.data)
} }
})) })
)
const newCart = await cartService.retrieve(cart.id, {
select: [
"gift_card_total",
"subtotal",
"total",
"tax_total",
"discount_total",
"subtotal",
],
relations: [
"shipping_address",
"billing_address",
"shipping_methods",
"shipping_methods.shipping_option",
"region",
"items",
"items.variant",
"items.variant.product",
],
})
const newCart = await cartService.retrieve(cart._id)
const order = await klarnaProviderService.cartToKlarnaOrder(newCart) const order = await klarnaProviderService.cartToKlarnaOrder(newCart)
res.json(order) res.json(order)
} catch (error) { } catch (error) {
@@ -1,3 +1,5 @@
jest.unmock("axios")
import axios from "axios"
import MockAdapter from "axios-mock-adapter" import MockAdapter from "axios-mock-adapter"
import KlarnaProviderService from "../klarna-provider" import KlarnaProviderService from "../klarna-provider"
@@ -151,7 +153,6 @@ describe("KlarnaProviderService", () => {
expect(result).toEqual({ expect(result).toEqual({
order_id: "123456789", order_id: "123456789",
order_amount: 1000,
}) })
}) })
}) })
@@ -176,15 +177,19 @@ describe("KlarnaProviderService", () => {
mockServer mockServer
.onPost("/ordermanagement/v1/orders/123456789/cancel") .onPost("/ordermanagement/v1/orders/123456789/cancel")
.reply(() => { .reply(() => {
return [200] return [200, { order_id: "123456789" }]
}) })
it("returns order id", async () => { mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.cancelPayment({ result = await klarnaProviderService.cancelPayment({
order_id: "123456789", data: { order_id: "123456789" },
}) })
expect(result).toEqual("123456789") expect(result).toEqual({ order_id: "123456789" })
}) })
}) })
@@ -289,15 +294,19 @@ describe("KlarnaProviderService", () => {
mockServer mockServer
.onPost("/ordermanagement/v1/orders/123456789/captures") .onPost("/ordermanagement/v1/orders/123456789/captures")
.reply(() => { .reply(() => {
return [200] return [200, { order_id: "123456789" }]
}) })
it("returns order id", async () => { mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.capturePayment({ result = await klarnaProviderService.capturePayment({
order_id: "123456789", data: { order_id: "123456789" },
}) })
expect(result).toEqual("123456789") expect(result).toEqual({ order_id: "123456789" })
}) })
}) })
@@ -322,18 +331,22 @@ describe("KlarnaProviderService", () => {
mockServer mockServer
.onPost("/ordermanagement/v1/orders/123456789/refunds") .onPost("/ordermanagement/v1/orders/123456789/refunds")
.reply(() => { .reply(() => {
return [200] return [200, { order_id: "123456789" }]
}) })
it("returns order id", async () => { mockServer.onGet("/ordermanagement/v1/orders/123456789").reply(() => {
return [200, { order_id: "123456789" }]
})
it("returns order", async () => {
result = await klarnaProviderService.refundPayment( result = await klarnaProviderService.refundPayment(
{ {
order_id: "123456789", data: { order_id: "123456789" },
}, },
1000 1000
) )
expect(result).toEqual("123456789") expect(result).toEqual({ order_id: "123456789" })
}) })
}) })
}) })
@@ -5,14 +5,26 @@ import { PaymentService } from "medusa-interfaces"
class KlarnaProviderService extends PaymentService { class KlarnaProviderService extends PaymentService {
static identifier = "klarna" static identifier = "klarna"
constructor( constructor({ shippingProfileService }, options) {
{ shippingProfileService, totalsService, regionService },
options
) {
super() super()
/**
* Required Klarna options:
* {
* backend_url: "",
* url: "",
* user: "",
* password: "",
* merchant_urls: {
* terms: ``,
* checkout: ``,
* confirmation: ``,
* }
* }
*/
this.options_ = options this.options_ = options
/** @private @const {Klarna} */
this.klarna_ = axios.create({ this.klarna_ = axios.create({
baseURL: options.url, baseURL: options.url,
auth: { auth: {
@@ -27,54 +39,36 @@ class KlarnaProviderService extends PaymentService {
this.backendUrl_ = options.backend_url this.backendUrl_ = options.backend_url
this.totalsService_ = totalsService /** @private @const {ShippingProfileService} */
this.regionService_ = regionService
this.shippingProfileService_ = shippingProfileService this.shippingProfileService_ = shippingProfileService
} }
async lineItemsToOrderLines_(cart, taxRate) { async lineItemsToOrderLines_(cart, taxRate) {
let order_lines = [] let order_lines = []
const tax = taxRate / 100
cart.items.forEach((item) => { cart.items.forEach((item) => {
// For bundles, we create an order line for each item in the bundle // Withdraw discount from the total item amount
if (Array.isArray(item.content)) { const quantity = item.quantity
item.content.forEach((c) => { const unit_price = item.unit_price * (tax + 1)
const total_amount = c.unit_price * c.quantity * (taxRate + 1) const total_amount = unit_price * quantity
const total_tax_amount = total_amount * taxRate const total_tax_amount = total_amount * (tax / (1 + tax))
order_lines.push({ order_lines.push({
name: item.title, name: item.title,
unit_price: c.unit_price, tax_rate: tax * 10000,
quantity: c.quantity, quantity,
tax_rate: taxRate * 10000, unit_price,
total_amount, total_amount,
total_tax_amount, total_tax_amount,
}) })
})
} else {
// Withdraw discount from the total item amount
const quantity = item.quantity
const unit_price = item.content.unit_price * 100 * (taxRate + 1)
const total_amount = unit_price * quantity
const total_tax_amount = total_amount * (taxRate / (1 + taxRate))
order_lines.push({
name: item.title,
tax_rate: taxRate * 10000,
quantity,
unit_price,
total_amount,
total_tax_amount,
})
}
}) })
if (cart.shipping_methods.length) { if (cart.shipping_methods.length) {
const { name, price } = cart.shipping_methods.reduce( const { name, price } = cart.shipping_methods.reduce(
(acc, next) => { (acc, next) => {
acc.name = [...acc.name, next.name] acc.name = [...acc.name, next.data.name]
acc.price += next.price acc.price += next.price
return acc return acc
}, },
@@ -85,10 +79,10 @@ class KlarnaProviderService extends PaymentService {
name: name.join(" + "), name: name.join(" + "),
quantity: 1, quantity: 1,
type: "shipping_fee", type: "shipping_fee",
unit_price: price * (1 + taxRate) * 100, unit_price: price * (1 + tax),
tax_rate: taxRate * 10000, tax_rate: tax * 10000,
total_amount: price * (1 + taxRate) * 100, total_amount: price * (1 + tax),
total_tax_amount: price * taxRate * 100, total_tax_amount: price * tax,
}) })
} }
@@ -98,28 +92,39 @@ class KlarnaProviderService extends PaymentService {
async cartToKlarnaOrder(cart) { async cartToKlarnaOrder(cart) {
let order = { let order = {
// Cart id is stored, such that we can use it for hooks // Cart id is stored, such that we can use it for hooks
merchant_data: cart._id, merchant_data: cart.id,
// TODO: Investigate if other locales are needed
locale: "en-US", locale: "en-US",
} }
const { tax_rate, currency_code } = await this.regionService_.retrieve( const { region, gift_card_total, discount_total, tax_total, total } = cart
cart.region_id
)
order.order_lines = await this.lineItemsToOrderLines_(cart, tax_rate) const taxRate = region.tax_rate / 100
const discount = (await this.totalsService_.getDiscountTotal(cart)) * 100 order.order_lines = await this.lineItemsToOrderLines_(cart, region.tax_rate)
if (discount) {
if (discount_total) {
order.order_lines.push({ order.order_lines.push({
name: `Discount`, name: `Discount`,
quantity: 1, quantity: 1,
type: "discount", type: "discount",
unit_price: 0, unit_price: 0,
total_discount_amount: discount * (1 + tax_rate), total_discount_amount: discount_total * (1 + taxRate),
tax_rate: tax_rate * 10000, tax_rate: taxRate * 10000,
total_amount: -discount * (1 + tax_rate), total_amount: -discount_total * (1 + taxRate),
total_tax_amount: -discount * tax_rate, total_tax_amount: -discount_total * taxRate,
})
}
if (gift_card_total) {
order.order_lines.push({
name: `Gift Card`,
quantity: 1,
type: "gift_card",
unit_price: 0,
total_discount_amount: gift_card_total * (1 + taxRate),
tax_rate: taxRate * 10000,
total_amount: -gift_card_total * (1 + taxRate),
total_tax_amount: -gift_card_total * taxRate,
}) })
} }
@@ -134,20 +139,19 @@ class KlarnaProviderService extends PaymentService {
} }
} }
// TODO: Check if country matches ISO const hasCountry =
if ( !_.isEmpty(cart.shipping_address) && cart.shipping_address.country_code
!_.isEmpty(cart.shipping_address) &&
cart.shipping_address.country_code if (hasCountry) {
) { order.purchase_country = cart.shipping_address.country_code.toUpperCase()
order.purchase_country = cart.shipping_address.country_code
} else { } else {
// Defaults to Sweden // Defaults to Sweden
order.purchase_country = "SE" order.purchase_country = "SE"
} }
order.order_amount = (await this.totalsService_.getTotal(cart)) * 100
order.order_tax_amount = (await this.totalsService_.getTaxTotal(cart)) * 100 order.order_amount = total
// TODO: Check if currency matches ISO order.order_tax_amount = tax_total
order.purchase_currency = currency_code order.purchase_currency = region.currency_code.toUpperCase()
order.merchant_urls = { order.merchant_urls = {
terms: this.options_.merchant_urls.terms, terms: this.options_.merchant_urls.terms,
@@ -159,20 +163,24 @@ class KlarnaProviderService extends PaymentService {
} }
if (cart.shipping_address && cart.shipping_address.first_name) { if (cart.shipping_address && cart.shipping_address.first_name) {
const shippingOptions = await this.shippingProfileService_.fetchCartOptions( let shippingOptions = await this.shippingProfileService_.fetchCartOptions(
cart cart
) )
shippingOptions = shippingOptions.filter(
(so) => !so.data?.require_drop_point
)
// If the cart does not have shipping methods yet, preselect one from // If the cart does not have shipping methods yet, preselect one from
// shipping_options and set the selected shipping method // shipping_options and set the selected shipping method
if (cart.shipping_methods.length) { if (cart.shipping_methods.length) {
const shipping_method = cart.shipping_methods[0] const shipping_method = cart.shipping_methods[0]
order.selected_shipping_option = { order.selected_shipping_option = {
id: shipping_method._id, id: shipping_method.shipping_option.id,
name: shipping_method.name, name: shipping_method.shipping_option.name,
price: shipping_method.price * (1 + tax_rate) * 100, price: shipping_method.price * (1 + taxRate),
tax_amount: shipping_method.price * tax_rate * 100, tax_amount: shipping_method.price * taxRate,
tax_rate: tax_rate * 10000, tax_rate: taxRate * 10000,
} }
} }
@@ -185,20 +193,25 @@ class KlarnaProviderService extends PaymentService {
return acc return acc
}, {}) }, {})
let f = (a, b) => // Helper function that calculates the cartesian product of multiple arrays
[].concat(...a.map((a) => b.map((b) => [].concat(a, b)))) // Don't touch :D
let cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a) // 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 methods = Object.keys(partitioned).map((k) => partitioned[k])
const combinations = cartesian(...methods) const combinations = cartesian(...methods)
// Use the cartesian product of shipping methods to generate correct
// format for the Klarna Widget
order.shipping_options = combinations.map((combination) => { order.shipping_options = combinations.map((combination) => {
combination = Array.isArray(combination) ? combination : [combination] combination = Array.isArray(combination) ? combination : [combination]
const details = combination.reduce( const details = combination.reduce(
(acc, next) => { (acc, next) => {
acc.id = [...acc.id, next._id] acc.id = [...acc.id, next.id]
acc.name = [...acc.name, next.name] acc.name = [...acc.name, next.name]
acc.price += next.price acc.price += next.amount
return acc return acc
}, },
{ id: [], name: [], price: 0 } { id: [], name: [], price: 0 }
@@ -207,10 +220,9 @@ class KlarnaProviderService extends PaymentService {
return { return {
id: details.id.join("."), id: details.id.join("."),
name: details.name.join(" + "), name: details.name.join(" + "),
price: details.price * (1 + tax_rate) * 100, price: details.price * (1 + taxRate),
tax_amount: details.price * tax_rate * 100, tax_amount: details.price * taxRate,
tax_rate: tax_rate * 10000, tax_rate: taxRate * 10000,
preselected: combinations.length === 1,
} }
}) })
} }
@@ -224,20 +236,18 @@ class KlarnaProviderService extends PaymentService {
* @returns {string} the status of the Klarna order * @returns {string} the status of the Klarna order
*/ */
async getStatus(paymentData) { async getStatus(paymentData) {
try { const { order_id } = paymentData
const { order_id } = paymentData const { data: order } = await this.klarna_.get(
const { data: order } = await this.klarna_.get( `${this.klarnaOrderUrl_}/${order_id}`
`${this.klarnaOrderUrl_}/${order_id}` )
)
let status = "initial" let status = "pending"
if (order.status === "checkout_complete") {
status = "authorized" if (order.status === "checkout_complete") {
} status = "authorized"
return status
} catch (error) {
throw error
} }
return status
} }
/** /**
@@ -249,9 +259,12 @@ class KlarnaProviderService extends PaymentService {
async createPayment(cart) { async createPayment(cart) {
try { try {
const order = await this.cartToKlarnaOrder(cart) const order = await this.cartToKlarnaOrder(cart)
return this.klarna_
const klarnaPayment = await this.klarna_
.post(this.klarnaOrderUrl_, order) .post(this.klarnaOrderUrl_, order)
.then(({ data }) => data) .then(({ data }) => data)
return klarnaPayment
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -272,6 +285,21 @@ class KlarnaProviderService extends PaymentService {
} }
} }
/**
* 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. * Retrieves completed Klarna Order.
* @param {string} klarnaOrderId - id of the order to retrieve * @param {string} klarnaOrderId - id of the order to retrieve
@@ -287,6 +315,23 @@ class KlarnaProviderService extends PaymentService {
} }
} }
/**
* 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 * Acknowledges a Klarna order as part of the order completion process
* @param {string} klarnaOrderId - id of the order to acknowledge * @param {string} klarnaOrderId - id of the order to acknowledge
@@ -332,6 +377,14 @@ class KlarnaProviderService extends PaymentService {
} }
} }
async updatePaymentData(sessionData, update) {
try {
return { ...sessionData, ...update }
} catch (error) {
throw error
}
}
/** /**
* Updates Klarna order. * Updates Klarna order.
* @param {string} order - the order to update * @param {string} order - the order to update
@@ -339,14 +392,14 @@ class KlarnaProviderService extends PaymentService {
* @returns {Object} updated order * @returns {Object} updated order
*/ */
async updatePayment(paymentData, cart) { async updatePayment(paymentData, cart) {
try { if (cart.total !== paymentData.order_amount) {
const order = await this.cartToKlarnaOrder(cart, true) const order = await this.cartToKlarnaOrder(cart)
return this.klarna_ return this.klarna_
.post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order) .post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order)
.then(({ data }) => data) .then(({ data }) => data)
} catch (error) {
throw error
} }
return paymentData
} }
/** /**
@@ -354,9 +407,9 @@ class KlarnaProviderService extends PaymentService {
* @param {Object} paymentData - payment method data from cart * @param {Object} paymentData - payment method data from cart
* @returns {string} id of captured order * @returns {string} id of captured order
*/ */
async capturePayment(paymentData) { async capturePayment(payment) {
const { order_id } = payment.data
try { try {
const { order_id } = paymentData
const { data: order } = await this.klarna_.get( const { data: order } = await this.klarna_.get(
`${this.klarnaOrderManagementUrl_}/${order_id}` `${this.klarnaOrderManagementUrl_}/${order_id}`
) )
@@ -368,7 +421,8 @@ class KlarnaProviderService extends PaymentService {
captured_amount: order_amount, captured_amount: order_amount,
} }
) )
return order_id
return this.retrieveCompletedOrder(order_id)
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -379,16 +433,17 @@ class KlarnaProviderService extends PaymentService {
* @param {Object} paymentData - payment method data from cart * @param {Object} paymentData - payment method data from cart
* @returns {string} id of refunded order * @returns {string} id of refunded order
*/ */
async refundPayment(paymentData, amount) { async refundPayment(payment, amountToRefund) {
const { order_id } = payment.data
try { try {
const { order_id } = paymentData
await this.klarna_.post( await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${order_id}/refunds`, `${this.klarnaOrderManagementUrl_}/${order_id}/refunds`,
{ {
refunded_amount: amount * 100, refunded_amount: amountToRefund,
} }
) )
return order_id
return this.retrieveCompletedOrder(order_id)
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -399,17 +454,22 @@ class KlarnaProviderService extends PaymentService {
* @param {Object} paymentData - payment method data from cart * @param {Object} paymentData - payment method data from cart
* @returns {string} id of cancelled order * @returns {string} id of cancelled order
*/ */
async cancelPayment(paymentData) { async cancelPayment(payment) {
const { order_id } = payment.data
try { try {
const { order_id } = paymentData
await this.klarna_.post( await this.klarna_.post(
`${this.klarnaOrderManagementUrl_}/${order_id}/cancel` `${this.klarnaOrderManagementUrl_}/${order_id}/cancel`
) )
return order_id
return this.retrieveCompletedOrder(order_id)
} catch (error) { } catch (error) {
throw error throw error
} }
} }
async deletePayment(_) {
return Promise.resolve()
}
} }
export default KlarnaProviderService export default KlarnaProviderService
+18 -10
View File
@@ -3595,7 +3595,7 @@ joi-objectid@^3.0.1:
resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668" resolved "https://registry.yarnpkg.com/joi-objectid/-/joi-objectid-3.0.1.tgz#63ace7860f8e1a993a28d40c40ffd8eff01a3668"
integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ== integrity sha512-V/3hbTlGpvJ03Me6DJbdBI08hBTasFOmipsauOsxOSnsF1blxV537WTl1zPwbfcKle4AK0Ma4OPnzMH4LlvTpQ==
joi@^17.2.1: joi@^17.2.1, joi@^17.3.0:
version "17.3.0" version "17.3.0"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2" resolved "https://registry.yarnpkg.com/joi/-/joi-17.3.0.tgz#f1be4a6ce29bc1716665819ac361dfa139fff5d2"
integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg== integrity sha512-Qh5gdU6niuYbUIUV5ejbsMiiFmBdw8Kcp8Buj2JntszCkCfxJ9Cz76OtHxOZMPXrt5810iDIXs+n1nNVoquHgg==
@@ -3857,21 +3857,29 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
medusa-core-utils@^1.0.10: medusa-core-utils@^1.0.11:
version "1.0.10" version "1.0.11"
resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.10.tgz#6175f1e428318205742621f78d387254fc85b8a1" resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.11.tgz#c77685c4b637f6aebe808e82dc4f7739c2f9ff79"
integrity sha512-z12ITzPl5UpDJTILNXcMGD4yujSRkuvVtxkrvqCmA44IEyRj1hWZ0dGbVzuHj2wIahDHFFg66oq91IQH5QVsyg== integrity sha512-Nnp7RCWsR4MJNrslC0YB/wMO1Wf2QJH0aras+tjRdymXfR/ySn2/0+v8pyXxhY8XXYzOZknnjdCB8KI9+PHlsA==
dependencies: dependencies:
joi "^17.2.1" joi "^17.2.1"
joi-objectid "^3.0.1" joi-objectid "^3.0.1"
medusa-test-utils@^1.0.11: medusa-core-utils@^1.0.12-alpha.787+0646bd3:
version "1.0.11" version "1.0.12-alpha.801"
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.11.tgz#bae901efa90426fb64818de700dc6e163820160f" resolved "https://registry.yarnpkg.com/medusa-core-utils/-/medusa-core-utils-1.0.12-alpha.801.tgz#0a7d0b5d4b2290d9a2f7afb35ab1f0bec666dbb0"
integrity sha512-CSNb70sXOfKTndzxWtPMYq+KeYaFkSLAPUyhuwDNVvkbne0faOYF5OMCV8r3aLoQ3D1Tvjrb/XB9Qt17ycjuPw== integrity sha512-1ppxgl8noZfO3JLLHeLj4eEMOwTI/LcCxWwwD7AUp5uP1brPmctthDTM/pOMrEuytKWIqaNXCRohvAlgDr0vYA==
dependencies:
joi "^17.3.0"
joi-objectid "^3.0.1"
medusa-test-utils@^1.0.12-alpha.176+0646bd3:
version "1.0.13"
resolved "https://registry.yarnpkg.com/medusa-test-utils/-/medusa-test-utils-1.0.13.tgz#f728b8dacb1ba64d85529e3f2e9dba6c7e315416"
integrity sha512-EBbjGLHFEG/Fnqneire/bNCPeseo1ZFF3n3yMUfp0JDmBAVgE8L52wJa8Zeu36TL/rbQsQAsTAS6La46A8IJgg==
dependencies: dependencies:
"@babel/plugin-transform-classes" "^7.9.5" "@babel/plugin-transform-classes" "^7.9.5"
medusa-core-utils "^1.0.10" medusa-core-utils "^1.0.11"
mongoose "^5.8.0" mongoose "^5.8.0"
memory-pager@^1.0.2: memory-pager@^1.0.2:
+1
View File
@@ -1,5 +1,6 @@
{ {
"plugins": [ "plugins": [
"@babel/plugin-proposal-optional-chaining",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-instanceof", "@babel/plugin-transform-instanceof",
"@babel/plugin-transform-classes" "@babel/plugin-transform-classes"
@@ -13,4 +13,3 @@ yarn.lock
/models /models
/subscribers /subscribers
/__mocks__ /__mocks__
+5 -1
View File
@@ -5,5 +5,9 @@ node_modules
/*.js /*.js
!index.js !index.js
yarn.lock yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc
@@ -7,13 +7,17 @@ exports["default"] = exports.CartServiceMock = exports.carts = void 0;
var _medusaTestUtils = require("medusa-test-utils"); var _medusaTestUtils = require("medusa-test-utils");
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var carts = { var carts = {
emptyCart: { emptyCart: {
_id: _medusaTestUtils.IdMap.getId("emptyCart"), id: _medusaTestUtils.IdMap.getId("emptyCart"),
items: [], items: [],
region_id: _medusaTestUtils.IdMap.getId("testRegion"), region_id: _medusaTestUtils.IdMap.getId("testRegion"),
customer_id: "test-customer",
payment_sessions: [],
shipping_options: [{ shipping_options: [{
_id: _medusaTestUtils.IdMap.getId("freeShipping"), id: _medusaTestUtils.IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
data: { data: {
some_data: "yes" some_data: "yes"
@@ -21,58 +25,53 @@ var carts = {
}] }]
}, },
frCart: { frCart: {
_id: _medusaTestUtils.IdMap.getId("fr-cart"), id: _medusaTestUtils.IdMap.getId("fr-cart"),
email: "lebron@james.com", email: "lebron@james.com",
title: "test", title: "test",
region_id: _medusaTestUtils.IdMap.getId("region-france"), region_id: _medusaTestUtils.IdMap.getId("region-france"),
items: [{ items: [{
_id: _medusaTestUtils.IdMap.getId("line"), id: _medusaTestUtils.IdMap.getId("line"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: [{ unit_price: 8,
unit_price: 8, variant: {
variant: { id: _medusaTestUtils.IdMap.getId("eur-8-us-10")
_id: _medusaTestUtils.IdMap.getId("eur-8-us-10")
},
product: {
_id: _medusaTestUtils.IdMap.getId("product")
},
quantity: 1
}, {
unit_price: 10,
variant: {
_id: _medusaTestUtils.IdMap.getId("eur-10-us-12")
},
product: {
_id: _medusaTestUtils.IdMap.getId("product")
},
quantity: 1
}],
quantity: 10
}, {
_id: _medusaTestUtils.IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 10,
variant: {
_id: _medusaTestUtils.IdMap.getId("eur-10-us-12")
},
product: {
_id: _medusaTestUtils.IdMap.getId("product")
},
quantity: 1
}, },
product: {
id: _medusaTestUtils.IdMap.getId("product")
},
// {
// unit_price: 10,
// variant: {
// id: IdMap.getId("eur-10-us-12"),
// },
// product: {
// id: IdMap.getId("product"),
// },
// quantity: 1,
// },
quantity: 10 quantity: 10
}], }, _defineProperty({
id: _medusaTestUtils.IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
unit_price: 10,
variant: {
id: _medusaTestUtils.IdMap.getId("eur-10-us-12")
},
product: {
id: _medusaTestUtils.IdMap.getId("product")
},
quantity: 1
}, "quantity", 10)],
shipping_methods: [{ shipping_methods: [{
_id: _medusaTestUtils.IdMap.getId("freeShipping"), id: _medusaTestUtils.IdMap.getId("freeShipping"),
profile_id: "default_profile" profile_id: "default_profile"
}], }],
shipping_options: [{ shipping_options: [{
_id: _medusaTestUtils.IdMap.getId("freeShipping"), id: _medusaTestUtils.IdMap.getId("freeShipping"),
profile_id: "default_profile" profile_id: "default_profile"
}], }],
payment_sessions: [{ payment_sessions: [{
@@ -95,57 +94,57 @@ var carts = {
customer_id: _medusaTestUtils.IdMap.getId("lebron") customer_id: _medusaTestUtils.IdMap.getId("lebron")
}, },
frCartNoStripeCustomer: { frCartNoStripeCustomer: {
_id: _medusaTestUtils.IdMap.getId("fr-cart-no-customer"), id: _medusaTestUtils.IdMap.getId("fr-cart-no-customer"),
title: "test", title: "test",
region_id: _medusaTestUtils.IdMap.getId("region-france"), region_id: _medusaTestUtils.IdMap.getId("region-france"),
items: [{ items: [{
_id: _medusaTestUtils.IdMap.getId("line"), id: _medusaTestUtils.IdMap.getId("line"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: [{ content: [{
unit_price: 8, unit_price: 8,
variant: { variant: {
_id: _medusaTestUtils.IdMap.getId("eur-8-us-10") id: _medusaTestUtils.IdMap.getId("eur-8-us-10")
}, },
product: { product: {
_id: _medusaTestUtils.IdMap.getId("product") id: _medusaTestUtils.IdMap.getId("product")
}, },
quantity: 1 quantity: 1
}, { }, {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: _medusaTestUtils.IdMap.getId("eur-10-us-12") id: _medusaTestUtils.IdMap.getId("eur-10-us-12")
}, },
product: { product: {
_id: _medusaTestUtils.IdMap.getId("product") id: _medusaTestUtils.IdMap.getId("product")
}, },
quantity: 1 quantity: 1
}], }],
quantity: 10 quantity: 10
}, { }, {
_id: _medusaTestUtils.IdMap.getId("existingLine"), id: _medusaTestUtils.IdMap.getId("existingLine"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: { content: {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: _medusaTestUtils.IdMap.getId("eur-10-us-12") id: _medusaTestUtils.IdMap.getId("eur-10-us-12")
}, },
product: { product: {
_id: _medusaTestUtils.IdMap.getId("product") id: _medusaTestUtils.IdMap.getId("product")
}, },
quantity: 1 quantity: 1
}, },
quantity: 10 quantity: 10
}], }],
shipping_methods: [{ shipping_methods: [{
_id: _medusaTestUtils.IdMap.getId("freeShipping"), id: _medusaTestUtils.IdMap.getId("freeShipping"),
profile_id: "default_profile" profile_id: "default_profile"
}], }],
shipping_options: [{ shipping_options: [{
_id: _medusaTestUtils.IdMap.getId("freeShipping"), id: _medusaTestUtils.IdMap.getId("freeShipping"),
profile_id: "default_profile" profile_id: "default_profile"
}], }],
payment_sessions: [{ payment_sessions: [{
@@ -175,6 +174,10 @@ var CartServiceMock = {
return Promise.resolve(carts.frCart); return Promise.resolve(carts.frCart);
} }
if (cartId === _medusaTestUtils.IdMap.getId("fr-cart-no-customer")) {
return Promise.resolve(carts.frCartNoStripeCustomer);
}
if (cartId === _medusaTestUtils.IdMap.getId("emptyCart")) { if (cartId === _medusaTestUtils.IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart); return Promise.resolve(carts.emptyCart);
} }
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-payment-stripe", "name": "medusa-payment-stripe",
"version": "1.0.15", "version": "1.0.14-alpha.176+0646bd3",
"description": "Stripe Payment provider for Meduas Commerce", "description": "Stripe Payment provider for Meduas Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -15,6 +15,7 @@
"@babel/core": "^7.7.5", "@babel/core": "^7.7.5",
"@babel/node": "^7.7.4", "@babel/node": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^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-classes": "^7.9.5",
"@babel/plugin-transform-instanceof": "^7.8.3", "@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6", "@babel/plugin-transform-runtime": "^7.7.6",
@@ -25,7 +26,7 @@
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jest": "^25.5.2", "jest": "^25.5.2",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"scripts": { "scripts": {
"build": "babel src -d . --ignore **/__tests__", "build": "babel src -d . --ignore **/__tests__",
@@ -39,8 +40,8 @@
"dependencies": { "dependencies": {
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"stripe": "^8.50.0" "stripe": "^8.50.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -2,12 +2,14 @@ import { IdMap } from "medusa-test-utils"
export const carts = { export const carts = {
emptyCart: { emptyCart: {
_id: IdMap.getId("emptyCart"), id: IdMap.getId("emptyCart"),
items: [], items: [],
region_id: IdMap.getId("testRegion"), region_id: IdMap.getId("testRegion"),
customer_id: "test-customer",
payment_sessions: [],
shipping_options: [ shipping_options: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
data: { data: {
some_data: "yes", some_data: "yes",
@@ -16,67 +18,61 @@ export const carts = {
], ],
}, },
frCart: { frCart: {
_id: IdMap.getId("fr-cart"), id: IdMap.getId("fr-cart"),
email: "lebron@james.com", email: "lebron@james.com",
title: "test", title: "test",
region_id: IdMap.getId("region-france"), region_id: IdMap.getId("region-france"),
items: [ items: [
{ {
_id: IdMap.getId("line"), id: IdMap.getId("line"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: [
{ unit_price: 8,
unit_price: 8, variant: {
variant: { id: IdMap.getId("eur-8-us-10"),
_id: IdMap.getId("eur-8-us-10"), },
}, product: {
product: { id: IdMap.getId("product"),
_id: IdMap.getId("product"), },
}, // {
quantity: 1, // unit_price: 10,
}, // variant: {
{ // id: IdMap.getId("eur-10-us-12"),
unit_price: 10, // },
variant: { // product: {
_id: IdMap.getId("eur-10-us-12"), // id: IdMap.getId("product"),
}, // },
product: { // quantity: 1,
_id: IdMap.getId("product"), // },
},
quantity: 1,
},
],
quantity: 10, quantity: 10,
}, },
{ {
_id: IdMap.getId("existingLine"), id: IdMap.getId("existingLine"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: { unit_price: 10,
unit_price: 10, variant: {
variant: { id: IdMap.getId("eur-10-us-12"),
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
}, },
product: {
id: IdMap.getId("product"),
},
quantity: 1,
quantity: 10, quantity: 10,
}, },
], ],
shipping_methods: [ shipping_methods: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
shipping_options: [ shipping_options: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
@@ -102,12 +98,12 @@ export const carts = {
customer_id: IdMap.getId("lebron"), customer_id: IdMap.getId("lebron"),
}, },
frCartNoStripeCustomer: { frCartNoStripeCustomer: {
_id: IdMap.getId("fr-cart-no-customer"), id: IdMap.getId("fr-cart-no-customer"),
title: "test", title: "test",
region_id: IdMap.getId("region-france"), region_id: IdMap.getId("region-france"),
items: [ items: [
{ {
_id: IdMap.getId("line"), id: IdMap.getId("line"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
@@ -115,20 +111,20 @@ export const carts = {
{ {
unit_price: 8, unit_price: 8,
variant: { variant: {
_id: IdMap.getId("eur-8-us-10"), id: IdMap.getId("eur-8-us-10"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
{ {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: IdMap.getId("eur-10-us-12"), id: IdMap.getId("eur-10-us-12"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
@@ -136,17 +132,17 @@ export const carts = {
quantity: 10, quantity: 10,
}, },
{ {
_id: IdMap.getId("existingLine"), id: IdMap.getId("existingLine"),
title: "merge line", title: "merge line",
description: "This is a new line", description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb", thumbnail: "test-img-yeah.com/thumb",
content: { content: {
unit_price: 10, unit_price: 10,
variant: { variant: {
_id: IdMap.getId("eur-10-us-12"), id: IdMap.getId("eur-10-us-12"),
}, },
product: { product: {
_id: IdMap.getId("product"), id: IdMap.getId("product"),
}, },
quantity: 1, quantity: 1,
}, },
@@ -155,13 +151,13 @@ export const carts = {
], ],
shipping_methods: [ shipping_methods: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
shipping_options: [ shipping_options: [
{ {
_id: IdMap.getId("freeShipping"), id: IdMap.getId("freeShipping"),
profile_id: "default_profile", profile_id: "default_profile",
}, },
], ],
@@ -193,6 +189,9 @@ export const CartServiceMock = {
if (cartId === IdMap.getId("fr-cart")) { if (cartId === IdMap.getId("fr-cart")) {
return Promise.resolve(carts.frCart) return Promise.resolve(carts.frCart)
} }
if (cartId === IdMap.getId("fr-cart-no-customer")) {
return Promise.resolve(carts.frCartNoStripeCustomer)
}
if (cartId === IdMap.getId("emptyCart")) { if (cartId === IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart) return Promise.resolve(carts.emptyCart)
} }
@@ -24,7 +24,7 @@ export default async (req, res) => {
switch (event.type) { switch (event.type) {
case "payment_intent.succeeded": case "payment_intent.succeeded":
if (order) { if (order) {
await orderService.update(order._id, { await orderService.update(order.id, {
payment_status: "captured", payment_status: "captured",
}) })
} }
@@ -10,7 +10,7 @@ export const StripeProviderServiceMock = {
} }
if (payData.id === "pi_no") { if (payData.id === "pi_no") {
return Promise.resolve({ return Promise.resolve({
id: "pi", id: "pi_no",
}) })
} }
return Promise.resolve(undefined) return Promise.resolve(undefined)
@@ -129,6 +129,7 @@ describe("StripeProviderService", () => {
result = await stripeProviderService.updatePayment( result = await stripeProviderService.updatePayment(
{ {
id: "pi_lebron", id: "pi_lebron",
amount: 800,
}, },
{ {
total: 1000, total: 1000,
@@ -136,7 +137,7 @@ describe("StripeProviderService", () => {
) )
}) })
it("returns cancelled stripe payment intent", () => { it("returns updated stripe payment intent", () => {
expect(result).toEqual({ expect(result).toEqual({
id: "pi_lebron", id: "pi_lebron",
customer: "cus_lebron", customer: "cus_lebron",
@@ -186,7 +187,13 @@ describe("StripeProviderService", () => {
} }
) )
result = await stripeProviderService.capturePayment("pi_lebron") result = await stripeProviderService.capturePayment({
data: {
id: "pi_lebron",
customer: "cus_lebron",
amount: 1000,
},
})
}) })
it("returns captured stripe payment intent", () => { it("returns captured stripe payment intent", () => {
@@ -210,7 +217,17 @@ describe("StripeProviderService", () => {
} }
) )
result = await stripeProviderService.refundPayment("pi_lebron", 1000) result = await stripeProviderService.refundPayment(
{
data: {
id: "re_123",
payment_intent: "pi_lebron",
amount: 1000,
status: "succeeded",
},
},
1000
)
}) })
it("returns refunded stripe payment intent", () => { it("returns refunded stripe payment intent", () => {
@@ -234,7 +251,13 @@ describe("StripeProviderService", () => {
} }
) )
result = await stripeProviderService.cancelPayment("pi_lebron") result = await stripeProviderService.cancelPayment({
data: {
id: "pi_lebron",
customer: "cus_lebron",
status: "cancelled",
},
})
}) })
it("returns cancelled stripe payment intent", () => { it("returns cancelled stripe payment intent", () => {
@@ -8,48 +8,78 @@ class StripeProviderService extends PaymentService {
constructor({ customerService, totalsService, regionService }, options) { constructor({ customerService, totalsService, regionService }, options) {
super() super()
/**
* Required Stripe options:
* {
* api_key: "stripe_secret_key", REQUIRED
* webhook_secret: "stripe_webhook_secret", REQUIRED
* // Use this flag to capture payment immediately (default is false)
* capture: true
* }
*/
this.options_ = options this.options_ = options
/** @private @const {Stripe} */
this.stripe_ = Stripe(options.api_key) this.stripe_ = Stripe(options.api_key)
/** @private @const {CustomerService} */
this.customerService_ = customerService this.customerService_ = customerService
/** @private @const {RegionService} */
this.regionService_ = regionService this.regionService_ = regionService
/** @private @const {TotalsService} */
this.totalsService_ = totalsService this.totalsService_ = totalsService
} }
/** /**
* Status for Stripe PaymentIntent. * Fetches Stripe payment intent. Check its status and returns the
* @param {Object} paymentData - payment method data from cart * corresponding Medusa status.
* @param {object} paymentData - payment method data from cart
* @returns {string} the status of the payment intent * @returns {string} the status of the payment intent
*/ */
async getStatus(paymentData) { async getStatus(paymentData) {
const { id } = paymentData const { id } = paymentData
const paymentIntent = await this.stripe_.paymentIntents.retrieve(id) const paymentIntent = await this.stripe_.paymentIntents.retrieve(id)
let status = "initial" let status = "pending"
if (paymentIntent.status === "requires_payment_method") { if (paymentIntent.status === "requires_payment_method") {
return status return status
} }
if (paymentIntent.status === "requires_confirmation") {
return status
}
if (paymentIntent.status === "processing") {
return status
}
if (paymentIntent.status === "requires_action") {
status = "requires_more"
}
if (paymentIntent.status === "canceled") {
status = "canceled"
}
if (paymentIntent.status === "requires_capture") { if (paymentIntent.status === "requires_capture") {
status = "authorized" status = "authorized"
} }
if (paymentIntent.status === "succeeded") { if (paymentIntent.status === "succeeded") {
status = "succeeded" status = "authorized"
}
if (paymentIntent.status === "canceled") {
status = "canceled"
} }
return status return status
} }
/**
* Fetches a customers saved payment methods if registered in Stripe.
* @param {object} customer - customer to fetch saved cards for
* @returns {Promise<Array<object>>} saved payments methods
*/
async retrieveSavedMethods(customer) { async retrieveSavedMethods(customer) {
if (customer.metadata && customer.metadata.stripe_id) { if (customer.metadata && customer.metadata.stripe_id) {
const methods = await this.stripe_.paymentMethods.list({ const methods = await this.stripe_.paymentMethods.list({
@@ -63,6 +93,11 @@ class StripeProviderService extends PaymentService {
return Promise.resolve([]) return Promise.resolve([])
} }
/**
* Fetches a Stripe customer
* @param {string} customerId - Stripe customer id
* @returns {Promise<object>} Stripe customer
*/
async retrieveCustomer(customerId) { async retrieveCustomer(customerId) {
if (!customerId) { if (!customerId) {
return Promise.resolve() return Promise.resolve()
@@ -70,17 +105,23 @@ class StripeProviderService extends PaymentService {
return this.stripe_.customers.retrieve(customerId) return this.stripe_.customers.retrieve(customerId)
} }
// customer metadata /**
* Creates a Stripe customer using a Medusa customer.
* @param {object} customer - Customer data from Medusa
* @returns {Promise<object>} Stripe customer
*/
async createCustomer(customer) { async createCustomer(customer) {
try { try {
const stripeCustomer = await this.stripe_.customers.create({ const stripeCustomer = await this.stripe_.customers.create({
email: customer.email, email: customer.email,
}) })
await this.customerService_.setMetadata(
customer._id, if (customer.id) {
"stripe_id", await this.customerService_.update(customer.id, {
stripeCustomer.id metadata: { stripe_id: stripeCustomer.id },
) })
}
return stripeCustomer return stripeCustomer
} catch (error) { } catch (error) {
throw error throw error
@@ -88,50 +129,57 @@ class StripeProviderService extends PaymentService {
} }
/** /**
* Creates Stripe PaymentIntent. * Creates a Stripe payment intent.
* @param {string} cart - the cart to create a payment for * If customer is not registered in Stripe, we do so.
* @param {number} amount - the amount to create a payment for * @param {object} cart - cart to create a payment for
* @returns {string} id of payment intent * @returns {object} Stripe payment intent
*/ */
async createPayment(cart) { async createPayment(cart) {
const { customer_id, region_id } = cart const { customer_id, region_id, email } = cart
const { currency_code } = await this.regionService_.retrieve(region_id) const { currency_code } = await this.regionService_.retrieve(region_id)
let stripeCustomerId
if (!customer_id) {
const { id } = await this.stripe_.customers.create({
email: cart.email,
})
stripeCustomerId = id
} else {
const customer = await this.customerService_.retrieve(customer_id)
if (!(customer.metadata && customer.metadata.stripe_id)) {
const { id } = await this.stripe_.customers.create({
email: customer.email,
})
await this.customerService_.setMetadata(customer._id, "stripe_id", id)
} else {
stripeCustomerId = customer.metadata.stripe_id
}
}
const amount = await this.totalsService_.getTotal(cart) const amount = await this.totalsService_.getTotal(cart)
const paymentIntent = await this.stripe_.paymentIntents.create({
customer: stripeCustomerId, const intentRequest = {
amount: parseInt(amount * 100), // Stripe amount is in cents amount: amount,
currency: currency_code, currency: currency_code,
setup_future_usage: "on_session", setup_future_usage: "on_session",
capture_method: "manual", capture_method: this.options_.capture ? "automatic" : "manual",
metadata: { cart_id: `${cart._id}` }, metadata: { cart_id: `${cart.id}` },
}) }
if (customer_id) {
const customer = await this.customerService_.retrieve(customer_id)
if (customer.metadata?.stripe_id) {
intentRequest.customer = customer.metadata.stripe_id
} else {
const stripeCustomer = await this.createCustomer({
email,
id: customer_id,
})
intentRequest.customer = stripeCustomer.id
}
} else {
const stripeCustomer = await this.createCustomer({
email,
})
intentRequest.customer = stripeCustomer.id
}
const paymentIntent = await this.stripe_.paymentIntents.create(
intentRequest
)
return paymentIntent return paymentIntent
} }
/** /**
* Retrieves Stripe PaymentIntent. * Retrieves Stripe payment intent.
* @param {object} data - the data of the payment to retrieve * @param {object} data - the data of the payment to retrieve
* @returns {Object} Stripe PaymentIntent * @returns {Promise<object>} Stripe payment intent
*/ */
async retrievePayment(data) { async retrievePayment(data) {
try { try {
@@ -142,26 +190,74 @@ class StripeProviderService extends PaymentService {
} }
/** /**
* Updates Stripe PaymentIntent. * Gets a Stripe payment intent and returns it.
* @param {object} data - The payment session data. * @param {object} sessionData - the data of the payment to retrieve
* @param {Object} cart - the current cart value * @returns {Promise<object>} Stripe payment intent
* @returns {Object} Stripe PaymentIntent
*/ */
async updatePayment(data, cart) { async getPaymentData(sessionData) {
try { try {
const { id } = data return this.stripe_.paymentIntents.retrieve(sessionData.data.id)
const amount = await this.totalsService_.getTotal(cart) } catch (error) {
return this.stripe_.paymentIntents.update(id, { throw error
amount: parseInt(amount * 100), }
}
/**
* Authorizes Stripe payment intent by simply returning
* the status for the payment intent 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 = {}) {
const stat = await this.getStatus(sessionData.data)
try {
return { data: sessionData.data, status: stat }
} catch (error) {
throw error
}
}
async updatePaymentData(sessionData, update) {
try {
return this.stripe_.paymentIntents.update(sessionData.id, {
...update.data,
}) })
} catch (error) { } catch (error) {
throw error throw error
} }
} }
async deletePayment(data) { /**
* Updates Stripe payment intent.
* @param {object} sessionData - payment session data.
* @param {object} update - objec to update intent with
* @returns {object} Stripe payment intent
*/
async updatePayment(sessionData, cart) {
try { try {
const { id } = data const stripeId = cart.customer?.metadata?.stripe_id || undefined
if (stripeId !== sessionData.customer) {
return this.createPayment(cart)
} else {
if (cart.total && sessionData.amount === cart.total) {
return sessionData
}
return this.stripe_.paymentIntents.update(sessionData.id, {
amount: cart.total,
})
}
} catch (error) {
throw error
}
}
async deletePayment(payment) {
try {
const { id } = payment.data
return this.stripe_.paymentIntents.cancel(id).catch((err) => { return this.stripe_.paymentIntents.cancel(id).catch((err) => {
if (err.statusCode === 400) { if (err.statusCode === 400) {
return return
@@ -174,15 +270,15 @@ class StripeProviderService extends PaymentService {
} }
/** /**
* Updates customer of Stripe PaymentIntent. * Updates customer of Stripe payment intent.
* @param {string} cart - the cart to update payment intent for * @param {string} paymentIntentId - id of payment intent to update
* @param {Object} data - the update object for the payment intent * @param {string} customerId - id of new Stripe customer
* @returns {Object} Stripe PaymentIntent * @returns {object} Stripe payment intent
*/ */
async updatePaymentIntentCustomer(paymentIntent, id) { async updatePaymentIntentCustomer(paymentIntentId, customerId) {
try { try {
return this.stripe_.paymentIntents.update(paymentIntent, { return this.stripe_.paymentIntents.update(paymentIntentId, {
customer: id, customer: customerId,
}) })
} catch (error) { } catch (error) {
throw error throw error
@@ -190,12 +286,12 @@ class StripeProviderService extends PaymentService {
} }
/** /**
* Captures payment for Stripe PaymentIntent. * Captures payment for Stripe payment intent.
* @param {Object} paymentData - payment method data from cart * @param {object} paymentData - payment method data from cart
* @returns {Object} Stripe PaymentIntent * @returns {object} Stripe payment intent
*/ */
async capturePayment(paymentData) { async capturePayment(payment) {
const { id } = paymentData const { id } = payment.data
try { try {
return this.stripe_.paymentIntents.capture(id) return this.stripe_.paymentIntents.capture(id)
} catch (error) { } catch (error) {
@@ -204,32 +300,34 @@ class StripeProviderService extends PaymentService {
} }
/** /**
* Refunds payment for Stripe PaymentIntent. * Refunds payment for Stripe payment intent.
* @param {Object} paymentData - payment method data from cart * @param {object} paymentData - payment method data from cart
* @returns {string} id of payment intent * @param {number} amountToRefund - amount to refund
* @returns {string} refunded payment intent
*/ */
async refundPayment(paymentData, amount) { async refundPayment(payment, amountToRefund) {
const { id } = paymentData const { id } = payment.data
try { try {
return this.stripe_.refunds.create({ await this.stripe_.refunds.create({
amount: parseInt(amount * 100), amount: amountToRefund,
payment_intent: id, payment_intent: id,
}) })
return payment.data
} catch (error) { } catch (error) {
throw error throw error
} }
} }
/** /**
* Cancels payment for Stripe PaymentIntent. * Cancels payment for Stripe payment intent.
* @param {Object} paymentData - payment method data from cart * @param {object} paymentData - payment method data from cart
* @returns {string} id of payment intent * @returns {object} canceled payment intent
*/ */
async cancelPayment(paymentData) { async cancelPayment(payment) {
const { id } = paymentData const { id } = payment.data
try { try {
const result = await this.stripe_.paymentIntents.cancel(id) return this.stripe_.paymentIntents.cancel(id)
return result
} catch (error) { } catch (error) {
if (error.payment_intent.status === "canceled") { if (error.payment_intent.status === "canceled") {
return error.payment_intent return error.payment_intent
@@ -241,10 +339,10 @@ class StripeProviderService extends PaymentService {
/** /**
* Constructs Stripe Webhook event * Constructs Stripe Webhook event
* @param {Object} data - the data of the webhook request: req.body * @param {object} data - the data of the webhook request: req.body
* @param {Object} signature - the Stripe signature on the event, that * @param {object} signature - the Stripe signature on the event, that
* ensures integrity of the webhook event * ensures integrity of the webhook event
* @returns {Object} Stripe Webhook event * @returns {object} Stripe Webhook event
*/ */
constructWebhookEvent(data, signature) { constructWebhookEvent(data, signature) {
return this.stripe_.webhooks.constructEvent( return this.stripe_.webhooks.constructEvent(
@@ -1,93 +0,0 @@
import { IdMap } from "medusa-test-utils"
import { carts, CartServiceMock } from "../../__mocks__/cart"
import { CustomerServiceMock } from "../../__mocks__/customer"
import { StripeProviderServiceMock } from "../../services/__mocks__/stripe-provider"
import { EventBusServiceMock } from "../../__mocks__/eventbus"
import CartSubscriber from "../cart"
describe("CartSubscriber", () => {
describe("onCustomerUpdated", () => {
let cartSubcriber = new CartSubscriber({
eventBusService: EventBusServiceMock,
cartService: CartServiceMock,
stripeProviderService: StripeProviderServiceMock,
customerService: CustomerServiceMock,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("resolves on non-existing payment data", async () => {
await cartSubcriber.onCustomerUpdated(carts.emptyCart)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(0)
})
it("cancels old and creates new payment intent with the updated existing customer", async () => {
await cartSubcriber.onCustomerUpdated(carts.frCart)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("lebron")
)
expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledWith(
carts.frCart.payment_sessions[0].data
)
expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledWith({
id: "pi",
customer: "cus_123456789",
})
expect(StripeProviderServiceMock.createPayment).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.createPayment).toHaveBeenCalledWith(
carts.frCart
)
expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledTimes(1)
expect(CartServiceMock.updatePaymentSession).toHaveBeenCalledWith(
IdMap.getId("fr-cart"),
"stripe",
{
id: "pi_new",
customer: "cus_123456789_new",
}
)
})
it("cancels old and creates new payment intent and creates new stripe customer", async () => {
await cartSubcriber.onCustomerUpdated(carts.frCartNoStripeCustomer)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("vvd")
)
expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.retrievePayment).toHaveBeenCalledWith(
carts.frCartNoStripeCustomer.payment_sessions[0].data
)
expect(StripeProviderServiceMock.createCustomer).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.createCustomer).toHaveBeenCalledWith({
_id: IdMap.getId("vvd"),
first_name: "Virgil",
last_name: "Van Dijk",
email: "virg@vvd.com",
password_hash: "1234",
metadata: {},
})
expect(
StripeProviderServiceMock.updatePaymentIntentCustomer
).toHaveBeenCalledTimes(1)
expect(
StripeProviderServiceMock.updatePaymentIntentCustomer
).toHaveBeenCalledWith("cus_123456789_new_vvd")
})
})
})
@@ -2,12 +2,12 @@ class CartSubscriber {
constructor({ constructor({
cartService, cartService,
customerService, customerService,
stripeProviderService, paymentProviderService,
eventBusService, eventBusService,
}) { }) {
this.cartService_ = cartService this.cartService_ = cartService
this.customerService_ = customerService this.customerService_ = customerService
this.stripeProviderService_ = stripeProviderService this.paymentProviderService_ = paymentProviderService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
this.eventBus_.subscribe("cart.customer_updated", async (cart) => { this.eventBus_.subscribe("cart.customer_updated", async (cart) => {
@@ -15,64 +15,37 @@ class CartSubscriber {
}) })
} }
async onCustomerUpdated(cart) { async onCustomerUpdated(cartId) {
const { customer_id, payment_sessions } = cart const cart = await this.cartService_.retrieve(cartId, {
select: [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"total",
],
relations: [
"items",
"billing_address",
"shipping_address",
"region",
"region.payment_providers",
"payment_sessions",
"customer",
],
})
if (!payment_sessions) { if (!cart.payment_sessions?.length) {
return Promise.resolve() return Promise.resolve()
} }
const customer = await this.customerService_.retrieve(customer_id) const session = cart.payment_sessions.find(
(ps) => ps.provider_id === "stripe"
const stripeSession = payment_sessions.find(
(s) => s.provider_id === "stripe"
) )
if (!stripeSession) { if (session) {
return Promise.resolve() return this.paymentProviderService_.updateSession(session, cart)
} }
const paymentIntent = await this.stripeProviderService_.retrievePayment(
stripeSession.data
)
let stripeCustomer
if (customer.metadata && customer.metadata.stripe_id) {
stripeCustomer = await this.stripeProviderService_.retrieveCustomer(
customer.metadata.stripe_id
)
}
if (!stripeCustomer) {
stripeCustomer = await this.stripeProviderService_.createCustomer(
customer
)
}
if (stripeCustomer.id === paymentIntent.customer) {
return Promise.resolve()
}
if (!paymentIntent.customer) {
return this.stripeProviderService_.updatePaymentIntentCustomer(
stripeCustomer.id
)
}
if (stripeCustomer.id !== paymentIntent.customer) {
await this.stripeProviderService_.cancelPayment(paymentIntent)
const newPaymentIntent = await this.stripeProviderService_.createPayment(
cart
)
await this.cartService_.updatePaymentSession(
cart._id,
"stripe",
newPaymentIntent
)
}
return Promise.resolve()
} }
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-add-ons", "name": "medusa-plugin-add-ons",
"version": "1.0.8", "version": "1.0.7-alpha.176+0646bd3",
"description": "Add-on plugin for Medusa Commerce", "description": "Add-on plugin for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -25,7 +25,7 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jest": "^25.5.2", "jest": "^25.5.2",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"scripts": { "scripts": {
"build": "babel src -d . --ignore **/__tests__", "build": "babel src -d . --ignore **/__tests__",
@@ -37,8 +37,8 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"redis": "^3.0.2" "redis": "^3.0.2"
}, },
"gitHead": "3bd91f65304ed1d31c41b85d5c87123450e0542e" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -39,16 +39,7 @@ class AddOnService extends BaseService {
* @return {string} the validated id * @return {string} the validated id
*/ */
validateId_(rawId) { validateId_(rawId) {
const schema = Validator.objectId() return rawId
const { value, error } = schema.validate(rawId.toString())
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"The addOnId could not be casted to an ObjectId"
)
}
return value
} }
/** /**
@@ -187,7 +178,7 @@ class AddOnService extends BaseService {
}) })
// Return the price if we found a suitable match // Return the price if we found a suitable match
if (price) { if (price !== undefined) {
return price return price
} }
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-brightpearl", "name": "medusa-plugin-brightpearl",
"version": "1.0.26", "version": "1.0.21-alpha.176+0646bd3",
"description": "Brightpearl plugin for Medusa Commerce", "description": "Brightpearl plugin for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -27,7 +27,7 @@
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"eslint": "^6.8.0", "eslint": "^6.8.0",
"jest": "^25.5.2", "jest": "^25.5.2",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"prettier": "^2.0.5" "prettier": "^2.0.5"
}, },
"scripts": { "scripts": {
@@ -43,8 +43,8 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"axios-rate-limit": "^1.2.1", "axios-rate-limit": "^1.2.1",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"randomatic": "^3.1.1" "randomatic": "^3.1.1"
}, },
"gitHead": "3bd91f65304ed1d31c41b85d5c87123450e0542e" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -1,25 +0,0 @@
const REFRESH_CRON = process.env.BP_REFRESH_CRON || "5 4 * * */6"
const refreshToken = async (container) => {
const logger = container.resolve("logger")
const oauthService = container.resolve("oauthService")
const eventBus = container.resolve("eventBusService")
try {
logger.info("registering refresh cron job BP")
eventBus.createCronJob("refresh-token-bp", {}, REFRESH_CRON, async () => {
const appData = await oauthService.retrieveByName("brightpearl")
const data = appData.data
if (data && data.refresh_token) {
return oauthService.refreshToken("brightpearl", data.refresh_token)
}
})
} catch (err) {
if (err.name === "not_allowed") {
return
}
throw err
}
}
export default refreshToken
@@ -6,8 +6,52 @@ import MockAdapter from "axios-mock-adapter"
jest.mock("../../utils/brightpearl") jest.mock("../../utils/brightpearl")
const order = {
region: {
tax_code: "1234",
},
items: [
{
title: "Test",
variant: {
sku: "TEST",
},
unit_price: 1100,
quantity: 2,
},
],
shipping_total: 12399,
shipping_methods: [
{
name: "standard",
price: 12399,
},
],
payment_method: {
id: "123",
},
tax_rate: 23.1,
currency_code: "DKK",
display_id: "1234",
id: "12355",
discounts: [],
shipping_address: {
first_name: "Test",
last_name: "Testson",
address_1: "Test",
address_2: "TEst",
postal_code: "1234",
country_code: "DK",
phone: "12345678",
},
email: "test@example.com",
}
const OrderService = { const OrderService = {
setMetadata: () => { retrieve: () => {
return Promise.resolve(order)
},
update: () => {
return Promise.resolve() return Promise.resolve()
}, },
} }
@@ -20,7 +64,7 @@ const TotalsService = {
return Promise.resolve([]) return Promise.resolve([])
}, },
getShippingTotal: () => { getShippingTotal: () => {
return 123.9999929393293 return 12399
}, },
rounded: (value) => { rounded: (value) => {
const decimalPlaces = 4 const decimalPlaces = 4
@@ -91,45 +135,6 @@ describe("BrightpearlService", () => {
}) })
describe("createSalesOrder", () => { describe("createSalesOrder", () => {
const order = {
items: [
{
title: "Test",
content: {
variant: {
sku: "TEST",
},
unit_price: 11,
},
quantity: 2,
},
],
shipping_methods: [
{
name: "standard",
price: 123.9999929393293,
},
],
payment_method: {
_id: "123",
},
tax_rate: 0.231,
currency_code: "DKK",
display_id: "1234",
_id: "12355",
discounts: [],
shipping_address: {
first_name: "Test",
last_name: "Testson",
address_1: "Test",
address_2: "TEst",
postal_code: "1234",
country_code: "DK",
phone: "12345678",
},
email: "test@example.com",
}
const bpService = new BrightpearlService( const bpService = new BrightpearlService(
{ {
orderService: OrderService, orderService: OrderService,
@@ -187,8 +192,8 @@ describe("BrightpearlService", () => {
{ {
name: "Shipping: standard", name: "Shipping: standard",
quantity: 1, quantity: 1,
net: 124, net: 123.99,
tax: 28.644, tax: 28.6417,
taxCode: "1234", taxCode: "1234",
nominalCode: "4040", nominalCode: "4040",
}, },
@@ -160,7 +160,7 @@ class BrightpearlService extends BaseService {
// Only update if the inventory levels have changed // Only update if the inventory levels have changed
if (parseInt(v.inventory_quantity) !== parseInt(onHand)) { if (parseInt(v.inventory_quantity) !== parseInt(onHand)) {
return this.productVariantService_.update(v._id, { return this.productVariantService_.update(v.id, {
inventory_quantity: onHand, inventory_quantity: onHand,
}) })
} }
@@ -181,10 +181,13 @@ class BrightpearlService extends BaseService {
const onHand = availability[productId].total.onHand const onHand = availability[productId].total.onHand
const sku = brightpearlProduct.identity.sku const sku = brightpearlProduct.identity.sku
const [variant] = await this.productVariantService_.list({ sku }) if (!sku) return
const variant = await this.productVariantService_
.retrieveBySKU(sku)
.catch((_) => undefined)
if (variant && variant.manage_inventory) { if (variant && variant.manage_inventory) {
await this.productVariantService_.update(variant._id, { await this.productVariantService_.update(variant.id, {
inventory_quantity: onHand, inventory_quantity: onHand,
}) })
} }
@@ -249,7 +252,7 @@ class BrightpearlService extends BaseService {
} }
async createRefundCredit(fromOrder, fromRefund) { async createRefundCredit(fromOrder, fromRefund) {
const region = await this.regionService_.retrieve(fromOrder.region_id) const region = fromOrder.region
const client = await this.getClient() const client = await this.getClient()
const authData = await this.getAuthData() const authData = await this.getAuthData()
const orderId = fromOrder.metadata.brightpearl_sales_order_id const orderId = fromOrder.metadata.brightpearl_sales_order_id
@@ -266,7 +269,7 @@ class BrightpearlService extends BaseService {
const order = { const order = {
currency: parentSo.currency, currency: parentSo.currency,
ref: parentSo.ref, ref: parentSo.ref,
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, externalRef: `${parentSo.externalRef}.${fromRefund.id}`,
channelId: this.options.channel_id || `1`, channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id, installedIntegrationInstanceId: authData.installation_instance_id,
customer: parentSo.customer, customer: parentSo.customer,
@@ -277,11 +280,12 @@ class BrightpearlService extends BaseService {
name: `${fromRefund.reason}: ${fromRefund.note}`, name: `${fromRefund.reason}: ${fromRefund.note}`,
quantity: 1, quantity: 1,
taxCode: region.tax_code, taxCode: region.tax_code,
net: this.totalsService_.rounded( net: this.bpnum_(
fromRefund.amount / (1 + fromOrder.tax_rate) fromRefund.amount,
10000 / (100 + fromOrder.tax_rate)
), ),
tax: this.totalsService_.rounded( tax: this.bpnum_(
fromRefund.amount - fromRefund.amount / (1 + fromOrder.tax_rate) fromRefund.amount * (1 - 100 / (100 + fromOrder.tax_rate))
), ),
nominalCode: accountingCode, nominalCode: accountingCode,
}, },
@@ -291,15 +295,15 @@ class BrightpearlService extends BaseService {
return client.orders return client.orders
.createCredit(order) .createCredit(order)
.then(async (creditId) => { .then(async (creditId) => {
const paymentMethod = fromOrder.payment_method const paymentMethod = fromOrder.payments[0]
const paymentType = "PAYMENT" const paymentType = "PAYMENT"
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, transactionRef: `${paymentMethod.id}.${fromRefund.id}`,
transactionCode: fromOrder._id, transactionCode: fromOrder.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: creditId, orderId: creditId,
currencyIsoCode: fromOrder.currency_code, currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: fromRefund.amount, amountPaid: this.bpnum_(fromRefund.amount),
paymentDate: new Date(), paymentDate: new Date(),
paymentType, paymentType,
} }
@@ -309,18 +313,21 @@ class BrightpearlService extends BaseService {
await client.payments.create(payment) await client.payments.create(payment)
return this.orderService_.setMetadata( return this.orderService_.update(fromOrder.id, {
fromOrder._id, metadata: {
"brightpearl_credit_ids", brightpearl_credit_ids: newIds,
newIds },
) })
})
.catch((err) => {
console.log(err)
console.log(err.response.data.errors)
}) })
.catch((err) => console.log(err.response.data.errors))
} }
} }
async createSalesCredit(fromOrder, fromReturn) { async createSalesCredit(fromOrder, fromReturn) {
const region = await this.regionService_.retrieve(fromOrder.region_id) const region = fromOrder.region
const client = await this.getClient() const client = await this.getClient()
const authData = await this.getAuthData() const authData = await this.getAuthData()
const orderId = fromOrder.metadata.brightpearl_sales_order_id const orderId = fromOrder.metadata.brightpearl_sales_order_id
@@ -329,7 +336,7 @@ class BrightpearlService extends BaseService {
const order = { const order = {
currency: parentSo.currency, currency: parentSo.currency,
ref: parentSo.ref, ref: parentSo.ref,
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`, externalRef: `${parentSo.externalRef}.${fromReturn.id}`,
channelId: this.options.channel_id || `1`, channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id, installedIntegrationInstanceId: authData.installation_instance_id,
customer: parentSo.customer, customer: parentSo.customer,
@@ -359,18 +366,14 @@ class BrightpearlService extends BaseService {
return acc + next.net + next.tax return acc + next.net + next.tax
}, 0) }, 0)
const difference = fromReturn.refund_amount - total const difference = (fromReturn.refund_amount / 100 - total) * 100
if (difference) { if (difference) {
order.rows.push({ order.rows.push({
name: "Difference", name: "Difference",
quantity: 1, quantity: 1,
taxCode: region.tax_code, taxCode: region.tax_code,
net: this.totalsService_.rounded( net: this.bpnum_(difference, 10000 / (100 + fromOrder.tax_rate)),
difference / (1 + fromOrder.tax_rate) tax: this.bpnum_(difference * (1 - 100 / (100 + fromOrder.tax_rate))),
),
tax: this.totalsService_.rounded(
difference - difference / (1 + fromOrder.tax_rate)
),
nominalCode: this.options.sales_account_code || "4000", nominalCode: this.options.sales_account_code || "4000",
}) })
} }
@@ -378,15 +381,15 @@ class BrightpearlService extends BaseService {
return client.orders return client.orders
.createCredit(order) .createCredit(order)
.then(async (creditId) => { .then(async (creditId) => {
const paymentMethod = fromOrder.payment_method const paymentMethod = fromOrder.payments[0]
const paymentType = "PAYMENT" const paymentType = "PAYMENT"
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`, transactionRef: `${paymentMethod.id}.${fromReturn.id}`,
transactionCode: fromOrder._id, transactionCode: fromOrder.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: creditId, orderId: creditId,
currencyIsoCode: fromOrder.currency_code, currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: fromReturn.refund_amount, amountPaid: this.bpnum_(fromReturn.refund_amount),
paymentDate: new Date(), paymentDate: new Date(),
paymentType, paymentType,
} }
@@ -396,17 +399,34 @@ class BrightpearlService extends BaseService {
await client.payments.create(payment) await client.payments.create(payment)
return this.orderService_.setMetadata( return this.orderService_.update(fromOrder.id, {
fromOrder._id, metadata: {
"brightpearl_credit_ids", brightpearl_credit_ids: newIds,
newIds },
) })
}) })
.catch((err) => console.log(err.response.data.errors)) .catch((err) => console.log(err))
} }
} }
async createSalesOrder(fromOrder) { async createSalesOrder(fromOrderId) {
const fromOrder = await this.orderService_.retrieve(fromOrderId, {
select: [
"total",
"subtotal",
"shipping_total",
"gift_card_total",
"discount_total",
],
relations: [
"region",
"shipping_address",
"billing_address",
"shipping_methods",
"payments",
],
})
const client = await this.getClient() const client = await this.getClient()
let customer = await this.retrieveCustomerByEmail(fromOrder.email) let customer = await this.retrieveCustomerByEmail(fromOrder.email)
@@ -420,10 +440,10 @@ class BrightpearlService extends BaseService {
const { shipping_address } = fromOrder const { shipping_address } = fromOrder
const order = { const order = {
currency: { currency: {
code: fromOrder.currency_code, code: fromOrder.currency_code.toUpperCase(),
}, },
ref: fromOrder.display_id, ref: fromOrder.display_id,
externalRef: fromOrder._id, externalRef: fromOrder.id,
channelId: this.options.channel_id || `1`, channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id, installedIntegrationInstanceId: authData.installation_instance_id,
statusId: this.options.default_status_id || `3`, statusId: this.options.default_status_id || `3`,
@@ -464,33 +484,34 @@ class BrightpearlService extends BaseService {
return salesOrderId return salesOrderId
}) })
.then((salesOrderId) => { .then((salesOrderId) => {
return this.orderService_.setMetadata( return this.orderService_.update(fromOrder.id, {
fromOrder._id, metadata: {
"brightpearl_sales_order_id", brightpearl_sales_order_id: salesOrderId,
salesOrderId },
) })
}) })
} }
async createSwapPayment(fromSwap) { async createSwapPayment(fromSwapId) {
const fromSwap = await this.swapService_.retrieve(fromSwapId)
const client = await this.getClient() const client = await this.getClient()
const soId = const soId =
fromSwap.metadata && fromSwap.metadata.brightpearl_sales_order_id fromSwap.metadata && fromSwap.metadata.brightpearl_sales_order_id
if (!soId || fromSwap.amount_paid <= 0) { if (!soId || fromSwap.difference_due <= 0) {
return return
} }
const paymentType = "RECEIPT" const paymentType = "RECEIPT"
const paymentMethod = fromSwap.payment_method
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref transactionRef: `${fromSwap.id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
transactionCode: fromSwap._id, transactionCode: fromSwap.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: soId, orderId: soId,
paymentDate: new Date(), paymentDate: new Date(),
currencyIsoCode: fromSwap.currency_code, currencyIsoCode: fromSwap.order.currency_code.toUpperCase(),
amountPaid: fromSwap.amount_paid, amountPaid: fromSwap.difference_due,
paymentType, paymentType,
} }
@@ -502,20 +523,23 @@ class BrightpearlService extends BaseService {
let customer = await this.retrieveCustomerByEmail(fromOrder.email) let customer = await this.retrieveCustomerByEmail(fromOrder.email)
if (!customer) { if (!customer) {
customer = await this.createCustomer(fromOrder) customer = await this.createCustomer({
...fromOrder,
...fromSwap,
})
} }
const authData = await this.getAuthData() const authData = await this.getAuthData()
const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s)) const sIndex = fromOrder.swaps.findIndex((s) => fromSwap.id === s.id)
const { shipping_address } = fromSwap const { shipping_address } = fromSwap
const order = { const order = {
currency: { currency: {
code: fromOrder.currency_code, code: fromOrder.currency_code.toUpperCase(),
}, },
ref: `${fromOrder.display_id}-S${sIndex + 1}`, ref: `${fromOrder.display_id}-S${sIndex + 1}`,
externalRef: `${fromOrder._id}.${fromSwap._id}`, externalRef: `${fromOrder.id}.${fromSwap.id}`,
channelId: this.options.channel_id || `1`, channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id, installedIntegrationInstanceId: authData.installation_instance_id,
statusId: statusId:
@@ -545,7 +569,7 @@ class BrightpearlService extends BaseService {
}, },
}, },
rows: await this.getBrightpearlRows({ rows: await this.getBrightpearlRows({
region_id: fromOrder.region_id, region: fromOrder.region,
discounts: fromOrder.discounts, discounts: fromOrder.discounts,
tax_rate: fromOrder.tax_rate, tax_rate: fromOrder.tax_rate,
items: fromSwap.additional_items, items: fromSwap.additional_items,
@@ -562,14 +586,14 @@ class BrightpearlService extends BaseService {
return acc + parseFloat(next.net) + parseFloat(next.tax) return acc + parseFloat(next.net) + parseFloat(next.tax)
}, 0) }, 0)
const paymentMethod = fromOrder.payment_method const paymentMethod = fromOrder.payments[0]
const paymentType = "RECEIPT" const paymentType = "RECEIPT"
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`, transactionRef: `${paymentMethod.id}.${paymentType}-${fromSwap.id}`,
transactionCode: fromOrder._id, transactionCode: fromOrder.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: salesOrderId, orderId: salesOrderId,
currencyIsoCode: fromOrder.currency_code, currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: total, amountPaid: total,
paymentDate: new Date(), paymentDate: new Date(),
paymentType, paymentType,
@@ -577,33 +601,33 @@ class BrightpearlService extends BaseService {
await client.payments.create(payment) await client.payments.create(payment)
return this.swapService_.setMetadata( return this.swapService_.update(fromSwap.id, {
fromSwap._id, metadata: {
"brightpearl_sales_order_id", brightpearl_sales_order_id: salesOrderId,
salesOrderId },
) })
}) })
} }
async createSwapCredit(fromOrder, fromSwap) { async createSwapCredit(fromOrder, fromSwap) {
const region = await this.regionService_.retrieve(fromOrder.region_id) const region = fromOrder.region
const client = await this.getClient() const client = await this.getClient()
const authData = await this.getAuthData() const authData = await this.getAuthData()
const orderId = fromOrder.metadata.brightpearl_sales_order_id const orderId = fromOrder.metadata.brightpearl_sales_order_id
const sIndex = fromOrder.swaps.findIndex((s) => fromSwap._id.equals(s)) const sIndex = fromOrder.swaps.findIndex((s) => fromSwap.id === s.id)
if (orderId) { if (orderId) {
const parentSo = await client.orders.retrieve(orderId) const parentSo = await client.orders.retrieve(orderId)
const order = { const order = {
currency: parentSo.currency, currency: parentSo.currency,
ref: `${parentSo.ref}-S${sIndex + 1}`, ref: `${parentSo.ref}-S${sIndex + 1}`,
externalRef: `${parentSo.externalRef}.${fromSwap._id}`, externalRef: `${parentSo.externalRef}.${fromSwap.id}`,
channelId: this.options.channel_id || `1`, channelId: this.options.channel_id || `1`,
installedIntegrationInstanceId: authData.installation_instance_id, installedIntegrationInstanceId: authData.installation_instance_id,
customer: parentSo.customer, customer: parentSo.customer,
delivery: parentSo.delivery, delivery: parentSo.delivery,
parentId: orderId, parentId: orderId,
rows: fromSwap.return.items.map((i) => { rows: fromSwap.return_order.items.map((i) => {
const parentRow = parentSo.rows.find((row) => { const parentRow = parentSo.rows.find((row) => {
return row.externalRef === i.item_id return row.externalRef === i.item_id
}) })
@@ -623,13 +647,18 @@ class BrightpearlService extends BaseService {
}), }),
} }
if (fromSwap.return_shipping && fromSwap.return_shipping.price) { if (
fromSwap.return_order.shipping_method &&
fromSwap.return_order.shipping_method.price
) {
order.rows.push({ order.rows.push({
name: "Return Shipping", name: "Return Shipping",
quantity: 1, quantity: 1,
taxCode: region.tax_code, taxCode: region.tax_code,
net: -1 * fromSwap.return_shipping.price, net: (-1 * fromSwap.return_order.shipping_method.price) / 100,
tax: -1 * fromSwap.return_shipping.price * fromOrder.tax_rate, tax:
((-1 * fromSwap.return_order.shipping_method.price) / 100) *
(fromOrder.tax_rate / 100),
nominalCode: this.options.shipping_account_code || "4040", nominalCode: this.options.shipping_account_code || "4040",
}) })
} }
@@ -641,14 +670,14 @@ class BrightpearlService extends BaseService {
return client.orders return client.orders
.createCredit(order) .createCredit(order)
.then(async (creditId) => { .then(async (creditId) => {
const paymentMethod = fromOrder.payment_method const paymentMethod = fromOrder.payments[0]
const paymentType = "PAYMENT" const paymentType = "PAYMENT"
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${paymentType}-${fromSwap._id}`, transactionRef: `${paymentMethod.id}.${paymentType}-${fromSwap.id}`,
transactionCode: fromSwap._id, transactionCode: fromSwap.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: creditId, orderId: creditId,
currencyIsoCode: fromSwap.currency_code, currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: total, amountPaid: total,
paymentDate: new Date(), paymentDate: new Date(),
paymentType, paymentType,
@@ -660,7 +689,12 @@ class BrightpearlService extends BaseService {
} }
} }
async createPayment(fromOrder) { async createPayment(fromOrderId) {
const fromOrder = await this.orderService_.retrieve(fromOrderId, {
select: ["total"],
relations: ["payments"],
})
const client = await this.getClient() const client = await this.getClient()
const soId = const soId =
fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id
@@ -669,38 +703,35 @@ class BrightpearlService extends BaseService {
} }
const paymentType = "RECEIPT" const paymentType = "RECEIPT"
const paymentMethod = fromOrder.payment_method
const payment = { const payment = {
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref transactionRef: `${fromOrder.id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
transactionCode: fromOrder._id, transactionCode: fromOrder.id,
paymentMethodCode: this.options.payment_method_code || "1220", paymentMethodCode: this.options.payment_method_code || "1220",
orderId: soId, orderId: soId,
paymentDate: new Date(), paymentDate: new Date(),
currencyIsoCode: fromOrder.currency_code, currencyIsoCode: fromOrder.currency_code.toUpperCase(),
amountPaid: await this.totalsService_.getTotal(fromOrder), amountPaid: this.bpnum_(fromOrder.total),
paymentType, paymentType,
} }
await client.payments.create(payment) return client.payments.create(payment)
} }
async getBrightpearlRows(fromOrder) { async getBrightpearlRows(fromOrder) {
const region = await this.regionService_.retrieve(fromOrder.region_id) const { region } = fromOrder
const discount = fromOrder.discounts.find( const discount = fromOrder.discounts.find(
({ discount_rule }) => discount_rule.type !== "free_shipping" ({ rule }) => rule.type !== "free_shipping"
) )
let lineDiscounts = [] let lineDiscounts = []
if (discount && !discount.is_giftcard) { if (discount) {
lineDiscounts = this.totalsService_.getLineDiscounts(fromOrder, discount) lineDiscounts = this.totalsService_.getLineDiscounts(fromOrder, discount)
} }
const lines = await Promise.all( const lines = await Promise.all(
fromOrder.items.map(async (item) => { fromOrder.items.map(async (item) => {
const bpProduct = await this.retrieveProductBySKU( const bpProduct = await this.retrieveProductBySKU(item.variant.sku)
item.content.variant.sku
)
const ld = lineDiscounts.find((l) => item._id === l.item._id) || { const ld = lineDiscounts.find((l) => item.id === l.item.id) || {
amount: 0, amount: 0,
} }
@@ -710,13 +741,14 @@ class BrightpearlService extends BaseService {
} else { } else {
row.name = item.title row.name = item.title
} }
row.net = this.totalsService_.rounded( row.net = this.bpnum_(item.unit_price * item.quantity - ld.amount)
item.content.unit_price * item.quantity - ld.amount row.tax = this.bpnum_(
item.unit_price * item.quantity - ld.amount,
fromOrder.tax_rate
) )
row.tax = this.totalsService_.rounded(row.net * fromOrder.tax_rate)
row.quantity = item.quantity row.quantity = item.quantity
row.taxCode = region.tax_code row.taxCode = region.tax_code
row.externalRef = item._id row.externalRef = item.id
row.nominalCode = this.options.sales_account_code || "4000" row.nominalCode = this.options.sales_account_code || "4000"
if (item.is_giftcard) { if (item.is_giftcard) {
@@ -731,30 +763,28 @@ class BrightpearlService extends BaseService {
// correspondingly. This reduces the amount payable, while debiting the // correspondingly. This reduces the amount payable, while debiting the
// gift card account that was previously credited, when the gift card was // gift card account that was previously credited, when the gift card was
// purchased. // purchased.
if (discount && discount.is_giftcard) { const gcTotal = fromOrder.gift_card_total
const discountTotal = await this.totalsService_.getDiscountTotal( if (gcTotal) {
fromOrder
)
lines.push({ lines.push({
name: `Gift Card: ${discount.code}`, name: `Gift Card`,
net: -1 * discountTotal, net: this.bpnum_(-1 * gcTotal),
tax: this.totalsService_.rounded( tax: this.bpnum_(-1 * gcTotal, fromOrder.tax_rate),
-1 * discountTotal * fromOrder.tax_rate
),
quantity: 1, quantity: 1,
taxCode: region.tax_code, taxCode: region.tax_code,
nominalCode: this.options.gift_card_account_code || "4000", nominalCode: this.options.gift_card_account_code || "4000",
}) })
} }
const shippingTotal = this.totalsService_.getShippingTotal(fromOrder) const shippingTotal =
fromOrder.shipping_total ||
this.totalsService_.getShippingTotal(fromOrder)
const shippingMethods = fromOrder.shipping_methods const shippingMethods = fromOrder.shipping_methods
if (shippingMethods.length > 0) { if (shippingMethods.length > 0) {
lines.push({ lines.push({
name: `Shipping: ${shippingMethods.map((m) => m.name).join(" + ")}`, name: `Shipping: ${shippingMethods.map((m) => m.name).join(" + ")}`,
quantity: 1, quantity: 1,
net: this.totalsService_.rounded(shippingTotal), net: this.bpnum_(shippingTotal),
tax: this.totalsService_.rounded(shippingTotal * fromOrder.tax_rate), tax: this.bpnum_(shippingTotal, fromOrder.tax_rate),
taxCode: region.tax_code, taxCode: region.tax_code,
nominalCode: this.options.shipping_account_code || "4040", nominalCode: this.options.shipping_account_code || "4040",
}) })
@@ -821,11 +851,10 @@ class BrightpearlService extends BaseService {
.filter((i) => !!i) .filter((i) => !!i)
// Orders with a concatenated externalReference are swap orders // Orders with a concatenated externalReference are swap orders
const [orderId, swapId] = order.externalRef.split(".") const [_, swapId] = order.externalRef.split(".")
if (swapId) { if (swapId) {
const order = await this.orderService_.retrieve(orderId) return this.swapService_.createFulfillment(swapId, {
return this.swapService_.createFulfillment(order, swapId, {
goods_out_note: id, goods_out_note: id,
}) })
} }
@@ -866,6 +895,11 @@ class BrightpearlService extends BaseService {
return { contactId: customer } return { contactId: customer }
} }
bpnum_(number, taxRate = 100) {
const bpNumber = number / 100
return this.totalsService_.rounded(bpNumber * (taxRate / 100))
}
} }
export default BrightpearlService export default BrightpearlService
@@ -3,18 +3,24 @@ class OrderSubscriber {
eventBusService, eventBusService,
orderService, orderService,
swapService, swapService,
returnService,
paymentProviderService,
brightpearlService, brightpearlService,
fulfillmentService,
}) { }) {
this.orderService_ = orderService this.orderService_ = orderService
this.brightpearlService_ = brightpearlService this.brightpearlService_ = brightpearlService
this.swapService_ = swapService this.swapService_ = swapService
this.returnService_ = returnService
this.paymentProviderService_ = paymentProviderService
this.fulfillmentService_ = fulfillmentService
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
eventBusService.subscribe("order.refund_created", this.registerRefund) eventBusService.subscribe("order.refund_created", this.registerRefund)
eventBusService.subscribe("order.items_returned", this.registerReturn) eventBusService.subscribe("order.items_returned", this.registerReturn)
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
eventBusService.subscribe( eventBusService.subscribe(
"order.payment_captured", "order.payment_captured",
this.registerCapturedPayment this.registerCapturedPayment
@@ -24,50 +30,65 @@ class OrderSubscriber {
eventBusService.subscribe("swap.shipment_created", this.registerShipment) eventBusService.subscribe("swap.shipment_created", this.registerShipment)
// Before we initiate a swap we wait for the payment and the return // Before we initiate a swap we wait for the payment and the return
eventBusService.subscribe("swap.payment_completed", this.registerSwap) eventBusService.subscribe(
"swap.payment_completed",
this.registerSwapPayment
)
eventBusService.subscribe("order.swap_received", this.registerSwap) eventBusService.subscribe("order.swap_received", this.registerSwap)
} }
sendToBrightpearl = (order) => { sendToBrightpearl = (data) => {
return this.brightpearlService_.createSalesOrder(order) return this.brightpearlService_.createSalesOrder(data.id)
} }
registerCapturedPayment = (order) => { registerCapturedPayment = ({ id }) => {
return this.brightpearlService_.createPayment(order) return this.brightpearlService_.createPayment(id)
}
registerSwapPayment = async (data) => {
return this.registerSwap({ id: data.id, swap_id: data.id })
} }
registerSwap = async (data) => { registerSwap = async (data) => {
const { order, swap, swap_id } = data const { id, swap_id } = data
if (!order && !swap) { if (!id && !swap_id) {
return return
} }
let fromOrder = order const fromSwap = await this.swapService_.retrieve(swap_id, {
if (!fromOrder) { relations: [
fromOrder = await this.orderService_.retrieve(swap.order_id) "order",
} "order.payments",
"order.region",
"order.swaps",
"order.discounts",
"return_order",
"return_order.items",
"return_order.shipping_method",
"additional_items",
"shipping_address",
"shipping_methods",
],
})
let fromOrder = fromSwap.order
let fromSwap if (
if (swap) { !(
fromSwap = await this.swapService_.retrieve(swap._id) fromSwap.confirmed_at !== null &&
} else { fromSwap.return_order.status === "received"
fromSwap = await this.swapService_.retrieve(swap_id) )
} ) {
if (!(fromSwap.is_paid && fromSwap.return.status === "received")) {
return return
} }
await this.brightpearlService_.createSwapCredit(fromOrder, fromSwap) await this.brightpearlService_.createSwapCredit(fromOrder, fromSwap)
await this.brightpearlService_.createSwapOrder(fromOrder, fromSwap) await this.brightpearlService_.createSwapOrder(fromOrder, fromSwap)
const paySwap = await this.swapService_.retrieve(fromSwap._id)
await this.brightpearlService_.createSwapPayment(paySwap)
} }
registerShipment = async (data) => { registerShipment = async (data) => {
const { shipment } = data const { fulfillment_id } = data
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
const noteId = shipment.metadata.goods_out_note const noteId = shipment.metadata.goods_out_note
if (noteId) { if (noteId) {
await this.brightpearlService_.registerGoodsOutTrackingNumber( await this.brightpearlService_.registerGoodsOutTrackingNumber(
@@ -78,15 +99,28 @@ class OrderSubscriber {
} }
} }
registerReturn = (data) => { registerReturn = async (data) => {
const { order, return: fromReturn } = data const { id, return_id } = data
const order = await this.orderService_.retrieve(id, {
relations: ["region", "payments"],
})
const fromReturn = await this.returnService_.retrieve(return_id, {
relations: ["items"],
})
return this.brightpearlService_ return this.brightpearlService_
.createSalesCredit(order, fromReturn) .createSalesCredit(order, fromReturn)
.catch((err) => console.log(err)) .catch((err) => console.log(err))
} }
registerRefund = (data) => { registerRefund = async (data) => {
const { order, refund } = data const { id, refund_id } = data
const order = await this.orderService_.retrieve(id, {
relations: ["region", "payments"],
})
const refund = await this.paymentProviderService_.retrieveRefund(refund_id)
return this.brightpearlService_ return this.brightpearlService_
.createRefundCredit(order, refund) .createRefundCredit(order, refund)
.catch((err) => console.log(err)) .catch((err) => console.log(err))
+5 -1
View File
@@ -5,5 +5,9 @@ node_modules
/*.js /*.js
!index.js !index.js
yarn.lock yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-contentful", "name": "medusa-plugin-contentful",
"version": "1.0.15", "version": "1.0.12-alpha.176+0646bd3",
"description": "Contentful plugin for Medusa Commerce", "description": "Contentful plugin for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -39,9 +39,9 @@
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"contentful-management": "^5.27.1", "contentful-management": "^5.27.1",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"redis": "^3.0.2" "redis": "^3.0.2"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -46,21 +46,16 @@ class ContentfulService extends BaseService {
} }
} }
async getVariantEntries_(productId) { async getVariantEntries_(variants) {
try { try {
const productVariants = await this.productService_.retrieveVariants(
productId
)
const contentfulVariants = await Promise.all( const contentfulVariants = await Promise.all(
productVariants.map((variant) => variants.map((variant) =>
this.updateProductVariantInContentful(variant) this.updateProductVariantInContentful(variant)
) )
) )
return contentfulVariants return contentfulVariants
} catch (error) { } catch (error) {
console.log(error)
throw error throw error
} }
} }
@@ -118,79 +113,33 @@ class ContentfulService extends BaseService {
async createProductInContentful(product) { async createProductInContentful(product) {
try { try {
const p = await this.productService_.retrieve(product.id, {
relations: ["variants", "options"],
})
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
const variantEntries = await this.getVariantEntries_(product._id) const variantEntries = await this.getVariantEntries_(p.variants)
const variantLinks = this.getVariantLinks_(variantEntries) const variantLinks = this.getVariantLinks_(variantEntries)
const fieldsObject = { const result = await environment.createEntryWithId("product", p.id, {
title: { fields: {
"en-US": product.title, title: {
}, "en-US": p.title,
variants: {
"en-US": variantLinks,
},
options: {
"en-US": product.options,
},
objectId: {
"en-US": product._id,
},
}
if (product.images.length > 0) {
const imageLinks = await this.createImageAssets(product)
const thumbnailAsset = await environment.createAsset({
fields: {
title: {
"en-US": `${product.title}`,
},
description: {
"en-US": "",
},
file: {
"en-US": {
contentType: "image/xyz",
fileName: product.thumbnail,
upload: product.thumbnail,
},
},
}, },
}) variants: {
"en-US": variantLinks,
await thumbnailAsset.processForAllLocales()
const thumbnailLink = {
sys: {
type: "Link",
linkType: "Asset",
id: thumbnailAsset.sys.id,
}, },
} options: {
"en-US": p.options,
fieldsObject.thumbnail = {
"en-US": thumbnailLink,
}
if (imageLinks) {
fieldsObject.images = {
"en-US": imageLinks,
}
}
}
const result = await environment.createEntryWithId(
"product",
product._id,
{
fields: {
...fieldsObject,
}, },
} objectId: {
) "en-US": p.id,
},
},
})
const ignoreIds = (await this.getIgnoreIds_("product")) || [] const ignoreIds = (await this.getIgnoreIds_("product")) || []
ignoreIds.push(product._id) ignoreIds.push(product.id)
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds)) this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
return result return result
} catch (error) { } catch (error) {
@@ -200,33 +149,37 @@ class ContentfulService extends BaseService {
async createProductVariantInContentful(variant) { async createProductVariantInContentful(variant) {
try { try {
const v = await this.productVariantService_.retrieve(variant.id, {
relations: ["prices", "options"],
})
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
const result = await environment.createEntryWithId( const result = await environment.createEntryWithId(
"productVariant", "productVariant",
variant._id, v.id,
{ {
fields: { fields: {
title: { title: {
"en-US": variant.title, "en-US": v.title,
}, },
sku: { sku: {
"en-US": variant.sku, "en-US": v.sku,
}, },
prices: { prices: {
"en-US": variant.prices, "en-US": v.prices,
}, },
options: { options: {
"en-US": variant.options, "en-US": v.options,
}, },
objectId: { objectId: {
"en-US": variant._id, "en-US": v.id,
}, },
}, },
} }
) )
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
ignoreIds.push(variant._id) ignoreIds.push(v.id)
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds)) this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
return result return result
} catch (error) { } catch (error) {
@@ -238,12 +191,12 @@ class ContentfulService extends BaseService {
try { try {
const ignoreIds = (await this.getIgnoreIds_("product")) || [] const ignoreIds = (await this.getIgnoreIds_("product")) || []
if (ignoreIds.includes(product._id)) { if (ignoreIds.includes(product.id)) {
const newIgnoreIds = ignoreIds.filter((id) => id !== product._id) const newIgnoreIds = ignoreIds.filter((id) => id !== product.id)
this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds)) this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds))
return return
} else { } else {
ignoreIds.push(product._id) ignoreIds.push(product.id)
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds)) this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
} }
@@ -251,27 +204,31 @@ class ContentfulService extends BaseService {
// check if product exists // check if product exists
let productEntry = undefined let productEntry = undefined
try { try {
productEntry = await environment.getEntry(product._id) productEntry = await environment.getEntry(product.id)
} catch (error) { } catch (error) {
return this.createProductInContentful(product) return this.createProductInContentful(product)
} }
const variantEntries = await this.getVariantEntries_(product._id) const p = await this.productService_.retrieve(product.id, {
relations: ["options", "variants"],
})
const variantEntries = await this.getVariantEntries_(p.variants)
const variantLinks = this.getVariantLinks_(variantEntries) const variantLinks = this.getVariantLinks_(variantEntries)
const productEntryFields = { const productEntryFields = {
...productEntry.fields, ...productEntry.fields,
title: { title: {
"en-US": product.title, "en-US": p.title,
}, },
options: { options: {
"en-US": product.options, "en-US": p.options,
}, },
variants: { variants: {
"en-US": variantLinks, "en-US": variantLinks,
}, },
objectId: { objectId: {
"en-US": product._id, "en-US": p.id,
}, },
} }
@@ -290,15 +247,15 @@ class ContentfulService extends BaseService {
try { try {
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || [] const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
if (ignoreIds.includes(variant._id)) { if (ignoreIds.includes(variant.id)) {
const newIgnoreIds = ignoreIds.filter((id) => id !== variant._id) const newIgnoreIds = ignoreIds.filter((id) => id !== variant.id)
this.redis_.set( this.redis_.set(
"product_variant_ignore_ids", "product_variant_ignore_ids",
JSON.stringify(newIgnoreIds) JSON.stringify(newIgnoreIds)
) )
return return
} else { } else {
ignoreIds.push(variant._id) ignoreIds.push(variant.id)
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds)) this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
} }
@@ -307,32 +264,31 @@ class ContentfulService extends BaseService {
let variantEntry = undefined let variantEntry = undefined
// if not, we create a new one // if not, we create a new one
try { try {
variantEntry = await environment.getEntry(variant._id) variantEntry = await environment.getEntry(variant.id)
} catch (error) { } catch (error) {
return this.createProductVariantInContentful(variant) return this.createProductVariantInContentful(variant)
} }
const cleanPrices = variant.prices.map((price) => ({ const v = await this.productVariantService_.retrieve(variant.id, {
...price, relations: ["prices", "options"],
sale_amount: price.sale_amount || undefined, })
}))
const variantEntryFields = { const variantEntryFields = {
...variantEntry.fields, ...variantEntry.fields,
title: { title: {
"en-US": variant.title, "en-US": v.title,
}, },
sku: { sku: {
"en-US": variant.sku, "en-US": v.sku,
}, },
options: { options: {
"en-US": variant.options, "en-US": v.options,
}, },
prices: { prices: {
"en-US": cleanPrices, "en-US": v.prices,
}, },
objectId: { objectId: {
"en-US": variant._id, "en-US": v.id,
}, },
} }
@@ -1,5 +1,12 @@
class ContentfulSubscriber { class ContentfulSubscriber {
constructor({ contentfulService, eventBusService }) { constructor({
contentfulService,
productVariantService,
productService,
eventBusService,
}) {
this.productVariantService_ = productVariantService
this.productService_ = productService
this.contentfulService_ = contentfulService this.contentfulService_ = contentfulService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-discount-generator", "name": "medusa-plugin-discount-generator",
"version": "1.0.1", "version": "1.0.2-alpha.787+0646bd3",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"author": "Sebastian Rindom", "author": "Sebastian Rindom",
@@ -40,5 +40,5 @@
"medusa-core-utils": "^0.1.27", "medusa-core-utils": "^0.1.27",
"randomatic": "^3.1.1" "randomatic": "^3.1.1"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-economic", "name": "medusa-plugin-economic",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "E-conomic financial reporting", "description": "E-conomic financial reporting",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -39,9 +39,9 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"moment": "^2.27.0" "moment": "^2.27.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -71,7 +71,7 @@ class EconomicService extends BaseService {
let order_lines = [] let order_lines = []
// Find the discount, that is not free shipping // Find the discount, that is not free shipping
const discount = order.discounts.find( const discount = order.discounts.find(
({ discount_rule }) => discount_rule.type !== "free_shipping" ({ rule }) => rule.type !== "free_shipping"
) )
// If the discount has an item specific allocation method, // If the discount has an item specific allocation method,
// we need to fetch the discount for each item // we need to fetch the discount for each item
+5 -1
View File
@@ -5,5 +5,9 @@ node_modules
/*.js /*.js
!index.js !index.js
yarn.lock yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-ip-lookup", "name": "medusa-plugin-ip-lookup",
"version": "1.0.11", "version": "1.0.12-alpha.787+0646bd3",
"description": "IP lookup middleware for Medusa core", "description": "IP lookup middleware for Medusa core",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -35,5 +35,5 @@
"axios": "^0.20.0", "axios": "^0.20.0",
"mongoose": "^5.8.0" "mongoose": "^5.8.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -7,7 +7,8 @@ export default {
} }
const ipLookupService = req.scope.resolve("ipLookupService") const ipLookupService = req.scope.resolve("ipLookupService")
const regionService = req.scope.resolve("regionService") const manager = req.scope.resolve("manager")
const countryRepository = req.scope.resolve("countryRepository")
const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress const ip = req.headers["x-forwarded-for"] || req.connection.remoteAddress
@@ -18,15 +19,15 @@ export default {
return return
} }
// Find region using the country code from ip lookup const countryRepo = manager.getCustomRepository(countryRepository)
const regions = await regionService.list({ const country = await countryRepo.findOne({
countries: data.country_code, where: { iso_2: data.country_code.toLowerCase() },
}) })
// If this region exists, add it to the body of the cart creation request // If country exists, add it to the body of the cart creation request
if (regions[0]) { if (country) {
req.body.region_id = regions[0]._id.toString() req.body.region_id = country.region_id
req.body.country_code = data.country_code req.body.country_code = country.iso_2
} }
next() next()
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-mailchimp", "name": "medusa-plugin-mailchimp",
"version": "1.0.15", "version": "1.0.14-alpha.176+0646bd3",
"description": "Mailchimp newsletter subscriptions", "description": "Mailchimp newsletter subscriptions",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -40,7 +40,8 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"mailchimp-api-v3": "^1.14.0", "mailchimp-api-v3": "^1.14.0",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
} },
"gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-permissions", "name": "medusa-plugin-permissions",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Role permission for Medusa core", "description": "Role permission for Medusa core",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -32,9 +32,9 @@
"medusa-interfaces": "1.x" "medusa-interfaces": "1.x"
}, },
"dependencies": { "dependencies": {
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"mongoose": "^5.8.0" "mongoose": "^5.8.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-segment", "name": "medusa-plugin-segment",
"version": "1.0.16", "version": "1.0.14-alpha.176+0646bd3",
"description": "Segment Analytics", "description": "Segment Analytics",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -39,8 +39,8 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -13,8 +13,8 @@ class SegmentService extends BaseService {
constructor({ totalsService }, options) { constructor({ totalsService }, options) {
super() super()
this.options_ = options
this.totalsService_ = totalsService this.totalsService_ = totalsService
this.options_ = options
this.analytics_ = new Analytics(options.write_key) this.analytics_ = new Analytics(options.write_key)
} }
@@ -30,14 +30,17 @@ class SegmentService extends BaseService {
return this.analytics_.track(data) return this.analytics_.track(data)
} }
async getReportingValue(fromCurrency, value) { async getReportingValue(rawCurrency, value) {
const fromCurrency = rawCurrency.toUpperCase()
const date = "latest" const date = "latest"
const toCurrency = const toCurrency =
(this.options_.reporting_currency && (this.options_.reporting_currency &&
this.options_.reporting_currency.toUpperCase()) || this.options_.reporting_currency.toUpperCase()) ||
"EUR" "EUR"
if (fromCurrency === toCurrency) return value.toFixed(2) if (fromCurrency === toCurrency) {
return this.totalsService_.rounded(value)
}
const exchangeRate = await axios const exchangeRate = await axios
.get( .get(
@@ -47,28 +50,28 @@ class SegmentService extends BaseService {
return data.rates[fromCurrency] return data.rates[fromCurrency]
}) })
return (value / exchangeRate).toFixed(2) return this.totalsService_.rounded(value / exchangeRate)
} }
async buildOrder(order) { async buildOrder(order) {
const subtotal = await this.totalsService_.getSubtotal(order) const subtotal = order.subtotal / 100
const total = await this.totalsService_.getTotal(order) const total = order.total / 100
const tax = await this.totalsService_.getTaxTotal(order) const tax = order.tax_total / 100
const discount = await this.totalsService_.getDiscountTotal(order) const discount = order.discount_total / 100
const shipping = await this.totalsService_.getShippingTotal(order) const shipping = order.shipping_total / 100
const revenue = total - tax const revenue = total - tax
let coupon let coupon
if (order.discounts && order.discounts.length) { if (order.discounts && order.discounts.length) {
coupon = order.discounts[0].code coupon = order.discounts[0] && order.discounts[0].code
} }
const orderData = { const orderData = {
checkout_id: order.cart_id, checkout_id: order.cart_id,
order_id: order._id, order_id: order.id,
email: order.email, email: order.email,
region_id: order.region_id, region_id: order.region_id,
payment_provider: order.payment_method.provider_id, payment_provider: order.payments.map((p) => p.provider_id).join(","),
shipping_methods: order.shipping_methods, shipping_methods: order.shipping_methods,
shipping_country: order.shipping_address.country_code, shipping_country: order.shipping_address.country_code,
shipping_city: order.shipping_address.city, shipping_city: order.shipping_address.city,
@@ -102,8 +105,8 @@ class SegmentService extends BaseService {
order.items.map(async (item) => { order.items.map(async (item) => {
let name = item.title let name = item.title
const unit_price = item.content.unit_price const unit_price = item.unit_price
const line_total = unit_price * item.content.quantity * item.quantity const line_total = (unit_price * item.quantity) / 100
const revenue = await this.getReportingValue( const revenue = await this.getReportingValue(
order.currency_code, order.currency_code,
line_total line_total
@@ -114,11 +117,11 @@ class SegmentService extends BaseService {
return { return {
name, name,
variant: item.content.variant.sku, variant,
price: unit_price, price: unit_price / 100,
reporting_revenue: revenue, reporting_revenue: revenue,
product_id: `${item.content.product._id}`, product_id: item.variant.product_id,
sku: skuParts.join("-"), sku: item.variant.sku,
quantity: item.quantity, quantity: item.quantity,
} }
}) })
@@ -1,13 +1,55 @@
class OrderSubscriber { class OrderSubscriber {
constructor({ segmentService, eventBusService }) { constructor({
segmentService,
eventBusService,
orderService,
returnService,
}) {
this.orderService_ = orderService
this.returnService_ = returnService
eventBusService.subscribe( eventBusService.subscribe(
"order.items_returned", "order.items_returned",
async ({ order, return: ret }) => { async ({ id, return_id }) => {
const order = await this.orderService_.retrieve(id, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"payments",
"fulfillments",
"returns",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const ret = await this.returnService_.retrieve(return_id)
const shipping = [] const shipping = []
if (ret.shipping_method && ret.shipping_method.price) { if (ret.shipping_method && ret.shipping_method.price) {
shipping.push({ shipping.push({
...ret.shipping_method, ...ret.shipping_method,
price: -1 * ret.shipping_method.price, price: -1 * (ret.shipping_method.price / 100),
}) })
} }
@@ -15,7 +57,7 @@ class OrderSubscriber {
...order, ...order,
shipping_methods: shipping, shipping_methods: shipping,
items: ret.items.map((i) => items: ret.items.map((i) =>
order.items.find((l) => l._id === i.item_id) order.items.find((l) => l.id === i.item_id)
), ),
} }
@@ -31,7 +73,19 @@ class OrderSubscriber {
} }
) )
eventBusService.subscribe("order.canceled", async (order) => { eventBusService.subscribe("order.canceled", async ({ id }) => {
const order = await this.orderService_.retrieve(id, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
})
const date = new Date() const date = new Date()
const orderData = await segmentService.buildOrder(order) const orderData = await segmentService.buildOrder(order)
const orderEvent = { const orderEvent = {
@@ -44,7 +98,38 @@ class OrderSubscriber {
segmentService.track(orderEvent) segmentService.track(orderEvent)
}) })
eventBusService.subscribe("order.placed", async (order) => { eventBusService.subscribe("order.placed", async ({ id }) => {
const order = await this.orderService_.retrieve(id, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"payments",
"fulfillments",
"returns",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const date = new Date(parseInt(order.created)) const date = new Date(parseInt(order.created))
const orderData = await segmentService.buildOrder(order) const orderData = await segmentService.buildOrder(order)
const orderEvent = { const orderEvent = {
+5 -1
View File
@@ -5,5 +5,9 @@ node_modules
/*.js /*.js
!index.js !index.js
yarn.lock yarn.lock
src
.gitignore
.eslintrc
.babelrc
.prettierrc
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-sendgrid", "name": "medusa-plugin-sendgrid",
"version": "1.0.14", "version": "1.0.13-alpha.176+0646bd3",
"description": "SendGrid transactional emails", "description": "SendGrid transactional emails",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -39,8 +39,8 @@
"@sendgrid/mail": "^7.1.1", "@sendgrid/mail": "^7.1.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -37,8 +37,7 @@ class SendGridService extends BaseService {
templateId = this.options_.gift_card_created_template templateId = this.options_.gift_card_created_template
data = { data = {
...data, ...data,
display_value: display_value: data.giftcard.rule.value * (1 + data.tax_rate),
data.giftcard.discount_rule.value * (1 + data.tax_rate),
} }
break break
case "order.placed": case "order.placed":
@@ -4,16 +4,52 @@ class OrderSubscriber {
orderService, orderService,
sendgridService, sendgridService,
eventBusService, eventBusService,
fulfillmentService,
}) { }) {
this.orderService_ = orderService this.orderService_ = orderService
this.totalsService_ = totalsService this.totalsService_ = totalsService
this.sendgridService_ = sendgridService this.sendgridService_ = sendgridService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
this.fulfillmentService_ = fulfillmentService
this.eventBus_.subscribe( this.eventBus_.subscribe(
"order.shipment_created", "order.shipment_created",
async ({ order_id, shipment }) => { async ({ id, fulfillment_id }) => {
const order = await this.orderService_.retrieve(order_id) const order = await this.orderService_.retrieve(id, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
"refundable_amount",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"shipping_methods.shipping_option",
"payments",
"fulfillments",
"returns",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id)
const data = { const data = {
...order, ...order,
tracking_number: shipment.tracking_numbers.join(", "), tracking_number: shipment.tracking_numbers.join(", "),
@@ -33,50 +69,108 @@ class OrderSubscriber {
) )
}) })
this.eventBus_.subscribe("order.placed", async (order) => { this.eventBus_.subscribe("order.placed", async (orderObj) => {
const subtotal = await this.totalsService_.getSubtotal(order) try {
const tax_total = await this.totalsService_.getTaxTotal(order) const order = await this.orderService_.retrieve(orderObj.id, {
const discount_total = await this.totalsService_.getDiscountTotal(order) select: [
const shipping_total = await this.totalsService_.getShippingTotal(order) "shipping_total",
const total = await this.totalsService_.getTotal(order) "discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"shipping_methods.shipping_option",
"payments",
"fulfillments",
"returns",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const date = new Date(parseInt(order.created)) const {
const data = { subtotal,
...order, tax_total,
date: date.toDateString(), discount_total,
items: order.items.map((i) => { shipping_total,
total,
} = order
const taxRate = order.tax_rate / 100
const currencyCode = order.currency_code.toUpperCase()
const items = order.items.map((i) => {
return { return {
...i, ...i,
price: `${(i.content.unit_price * (1 + order.tax_rate)).toFixed( price: `${((i.unit_price / 100) * (1 + taxRate)).toFixed(
2 2
)} ${order.currency_code}`, )} ${currencyCode}`,
} }
}), })
discounts: order.discounts.map((discount) => {
return {
is_giftcard: discount.is_giftcard,
code: discount.code,
descriptor: `${discount.discount_rule.value}${
discount.discount_rule.type === "percentage"
? "%"
: ` ${order.currency_code}`
}`,
}
}),
subtotal: `${(subtotal * (1 + order.tax_rate)).toFixed(2)} ${
order.currency_code
}`,
tax_total: `${tax_total.toFixed(2)} ${order.currency_code}`,
discount_total: `${(discount_total * (1 + order.tax_rate)).toFixed(
2
)} ${order.currency_code}`,
shipping_total: `${(shipping_total * (1 + order.tax_rate)).toFixed(
2
)} ${order.currency_code}`,
total: `${total.toFixed(2)} ${order.currency_code}`,
}
await this.sendgridService_.transactionalEmail("order.placed", data) let discounts = []
if (order.discounts) {
discounts = order.discounts.map((discount) => {
return {
is_giftcard: false,
code: discount.code,
descriptor: `${discount.rule.value}${
discount.rule.type === "percentage" ? "%" : ` ${currencyCode}`
}`,
}
})
}
let giftCards = []
if (order.gift_cards) {
giftCards = order.gift_cards.map((gc) => {
return {
is_giftcard: true,
code: gc.code,
descriptor: `${gc.value} ${currencyCode}`,
}
})
discounts.concat(giftCards)
}
const data = {
...order,
date: order.created_at.toDateString(),
items,
discounts,
subtotal: `${((subtotal / 100) * (1 + taxRate)).toFixed(
2
)} ${currencyCode}`,
tax_total: `${(tax_total / 100).toFixed(2)} ${currencyCode}`,
discount_total: `${((discount_total / 100) * (1 + taxRate)).toFixed(
2
)} ${currencyCode}`,
shipping_total: `${((shipping_total / 100) * (1 + taxRate)).toFixed(
2
)} ${currencyCode}`,
total: `${(total / 100).toFixed(2)} ${currencyCode}`,
}
await this.sendgridService_.transactionalEmail("order.placed", data)
} catch (error) {
console.log(error)
}
}) })
this.eventBus_.subscribe("order.cancelled", async (order) => { this.eventBus_.subscribe("order.cancelled", async (order) => {
@@ -5,5 +5,7 @@ node_modules
/*.js /*.js
!index.js !index.js
yarn.lock yarn.lock
src
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-slack-notification", "name": "medusa-plugin-slack-notification",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Slack notifications", "description": "Slack notifications",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -39,9 +39,9 @@
"axios": "^0.19.2", "axios": "^0.19.2",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"moment": "^2.27.0" "moment": "^2.27.0"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
@@ -1,5 +1,4 @@
import axios from "axios" import axios from "axios"
import moment from "moment"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
class SlackService extends BaseService { class SlackService extends BaseService {
@@ -23,20 +22,48 @@ class SlackService extends BaseService {
} }
async orderNotification(orderId) { async orderNotification(orderId) {
const order = await this.orderService_.retrieve(orderId) const order = await this.orderService_.retrieve(orderId, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"payments",
"fulfillments",
"returns",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const subtotal = await this.totalsService_.getSubtotal(order) const { subtotal, tax_total, discount_total, shipping_total, total } = order
const shippingTotal = await this.totalsService_.getShippingTotal(order)
const taxTotal = await this.totalsService_.getTaxTotal(order) const currencyCode = order.currency_code.toUpperCase()
const discountTotal = await this.totalsService_.getDiscountTotal(order) const taxRate = order.tax_rate / 100
const total = await this.totalsService_.getTotal(order)
let blocks = [ let blocks = [
{ {
type: "section", type: "section",
text: { text: {
type: "mrkdwn", type: "mrkdwn",
text: `Order *<${this.options_.admin_orders_url}/${order._id}|#${order.display_id}>* has been processed.`, text: `Order *<${this.options_.admin_orders_url}/${order.id}|#${order.display_id}>* has been processed.`,
}, },
}, },
{ {
@@ -56,15 +83,17 @@ class SlackService extends BaseService {
type: "section", type: "section",
text: { text: {
type: "mrkdwn", type: "mrkdwn",
text: `*Subtotal*\t${subtotal.toFixed(2)} ${ text: `*Subtotal*\t${(subtotal / 100).toFixed(
order.currency_code 2
}\n*Shipping*\t${shippingTotal.toFixed(2)} ${ )} ${currencyCode}\n*Shipping*\t${(shipping_total / 100).toFixed(
order.currency_code 2
}\n*Discount Total*\t${discountTotal.toFixed(2)} ${ )} ${currencyCode}\n*Discount Total*\t${(
order.currency_code discount_total / 100
}\n*Tax*\t${taxTotal.toFixed(2)} ${ ).toFixed(2)} ${currencyCode}\n*Tax*\t${(tax_total / 100).toFixed(
order.currency_code 2
}\n*Total*\t${total.toFixed(2)} ${order.currency_code}`, )} ${currencyCode}\n*Total*\t${(total / 100).toFixed(
2
)} ${currencyCode}`,
}, },
}, },
] ]
@@ -74,7 +103,9 @@ class SlackService extends BaseService {
type: "section", type: "section",
text: { text: {
type: "mrkdwn", type: "mrkdwn",
text: `*Promo Code*\t${d.code} ${d.discount_rule.value}${d.discount_rule.type === "percentage" ? "%" : ` ${order.currency_code}`}`, text: `*Promo Code*\t${d.code} ${d.rule.value}${
d.rule.type === "percentage" ? "%" : ` ${currencyCode}`
}`,
}, },
}) })
}) })
@@ -88,12 +119,13 @@ class SlackService extends BaseService {
type: "section", type: "section",
text: { text: {
type: "mrkdwn", type: "mrkdwn",
text: `*${lineItem.title}*\n${lineItem.quantity} x ${ text: `*${lineItem.title}*\n${lineItem.quantity} x ${(
!Array.isArray(lineItem.content) && (lineItem.unit_price / 100) *
(lineItem.content.unit_price * (1 + order.tax_rate)).toFixed(2) (1 + taxRate)
} ${order.currency_code}`, ).toFixed(2)} ${currencyCode}`,
}, },
} }
if (lineItem.thumbnail) { if (lineItem.thumbnail) {
let url = lineItem.thumbnail let url = lineItem.thumbnail
if ( if (
@@ -106,9 +138,8 @@ class SlackService extends BaseService {
line.accessory = { line.accessory = {
type: "image", type: "image",
alt_text: "Item", alt_text: "Item",
image_url: url image_url: url,
} }
} }
blocks.push(line) blocks.push(line)
@@ -4,8 +4,8 @@ class OrderSubscriber {
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
this.eventBus_.subscribe("order.placed", async (order) => { this.eventBus_.subscribe("order.placed", async ({ id }) => {
await this.slackService_.orderNotification(order._id) await this.slackService_.orderNotification(id)
}) })
} }
} }
@@ -1,30 +0,0 @@
export default [
"BE",
"BG",
"CZ",
"DK",
"DE",
"EE",
"IE",
"EL",
"ES",
"FR",
"HR",
"IT",
"CY",
"LV",
"LT",
"LU",
"HU",
"MT",
"NL",
"AT",
"PL",
"PT",
"RO",
"SI",
"SK",
"FI",
"SE",
"UK",
]
@@ -1,8 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _default = ["BE", "BG", "CZ", "DK", "DE", "EE", "IE", "EL", "ES", "FR", "HR", "IT", "CY", "LV", "LT", "LU", "HU", "MT", "NL", "AT", "PL", "PT", "RO", "SI", "SK", "FI", "SE", "UK"];
exports["default"] = _default;
@@ -1,7 +1,6 @@
{ {
"name": "medusa-plugin-twilio-sms", "name": "medusa-plugin-twilio-sms",
"version": "1.0.12", "version": "1.0.11-alpha.176+0646bd3",
"description": "Twilio SMS service",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -36,9 +35,9 @@
"dependencies": { "dependencies": {
"@babel/plugin-transform-classes": "^7.9.5", "@babel/plugin-transform-classes": "^7.9.5",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13", "medusa-test-utils": "^1.0.12-alpha.176+0646bd3",
"twilio": "^3.49.1" "twilio": "^3.49.1"
}, },
"gitHead": "e1c465d244b20e68dd88bcb12db0a061f25581ec" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-wishlist", "name": "medusa-plugin-wishlist",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Provides /customers/:id/wishlist to add items to a customr's wishlist", "description": "Provides /customers/:id/wishlist to add items to a customr's wishlist",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -37,8 +37,8 @@
"dependencies": { "dependencies": {
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"express": "^4.17.1", "express": "^4.17.1",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"medusa-test-utils": "^1.0.13" "medusa-test-utils": "^1.0.12-alpha.176+0646bd3"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-test-utils", "name": "medusa-test-utils",
"version": "1.0.13", "version": "1.0.12-alpha.176+0646bd3",
"description": "Test utils for Medusa", "description": "Test utils for Medusa",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -29,8 +29,8 @@
}, },
"dependencies": { "dependencies": {
"@babel/plugin-transform-classes": "^7.9.5", "@babel/plugin-transform-classes": "^7.9.5",
"medusa-core-utils": "^1.0.11", "medusa-core-utils": "^1.0.12-alpha.787+0646bd3",
"mongoose": "^5.8.0" "randomatic": "^3.1.1"
}, },
"gitHead": "3cc7cbe5124cbcbb75f6e1435db4dcfaa2a60408" "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd"
} }
+9 -13
View File
@@ -1,23 +1,19 @@
import mongoose from "mongoose" import randomize from "randomatic";
String.prototype.equals = function(that) {
return this === that
}
class IdMap { class IdMap {
ids = {} ids = {};
getId(key, backend=false) { getId(key, prefix = "", length = 10) {
if (this.ids[key]) { if (this.ids[key]) {
return this.ids[key] return this.ids[key];
} }
const mongooseId = `${mongoose.Types.ObjectId()}` const id = `${prefix && prefix + "_"}${randomize("Aa0", length)}`;
this.ids[key] = mongooseId this.ids[key] = id;
return mongooseId return id;
} }
} }
const instance = new IdMap() const instance = new IdMap();
export default instance export default instance;
+3 -1
View File
@@ -1 +1,3 @@
export { default as IdMap } from "./id-map" export { default as IdMap } from "./id-map";
export { default as MockRepository } from "./mock-repository";
export { default as MockManager } from "./mock-manager";
@@ -0,0 +1,13 @@
export default {
getCustomRepository: function (repo) {
return repo;
},
transaction: function (isolationOrCb, cb) {
if (typeof isolationOrCb === "string") {
return cb(this);
} else {
return isolationOrCb(this);
}
},
};
@@ -0,0 +1,67 @@
export default ({
create,
update,
remove,
softRemove,
find,
findOne,
findOneOrFail,
save,
} = {}) => {
return {
create: jest.fn().mockImplementation((...args) => {
if (create) {
return create(...args);
}
return {};
}),
softRemove: jest.fn().mockImplementation((...args) => {
if (softRemove) {
return softRemove(...args);
}
return {};
}),
remove: jest.fn().mockImplementation((...args) => {
if (remove) {
return remove(...args);
}
return {};
}),
update: jest.fn().mockImplementation((...args) => {
if (update) {
return update(...args);
}
}),
findOneOrFail: jest.fn().mockImplementation((...args) => {
if (findOneOrFail) {
return findOneOrFail(...args);
}
}),
findOne: jest.fn().mockImplementation((...args) => {
if (findOne) {
return findOne(...args);
}
}),
findOneOrFail: jest.fn().mockImplementation((...args) => {
if (findOneOrFail) {
return findOneOrFail(...args);
}
}),
find: jest.fn().mockImplementation((...args) => {
if (find) {
return find(...args);
}
}),
softRemove: jest.fn().mockImplementation((...args) => {
if (softRemove) {
return softRemove(...args);
}
}),
save: jest.fn().mockImplementation((...args) => {
if (save) {
return save(...args);
}
return Promise.resolve(...args);
}),
};
};
Loaded 100 of 499 files, more files were not shown because too many files have changed in this diff. Show more