@@ -3,5 +3,8 @@
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"lerna": "^3.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"@hapi/joi": "^16.1.8",
|
||||
"chalk": "^4.0.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
"core-js": "^3.6.5",
|
||||
"fs-exists-cached": "^1.0.0",
|
||||
"joi-objectid": "^3.0.1",
|
||||
@@ -46,4 +45,4 @@
|
||||
"yargs": "^15.3.1"
|
||||
},
|
||||
"gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc"
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,7 @@ const resolveCwd = require(`resolve-cwd`)
|
||||
const yargs = require(`yargs`)
|
||||
const { getLocalMedusaVersion } = require(`./util/version`)
|
||||
const { didYouMean } = require(`./did-you-mean`)
|
||||
const envinfo = require(`envinfo`)
|
||||
const existsSync = require(`fs-exists-cached`).sync
|
||||
const clipboardy = require(`clipboardy`)
|
||||
|
||||
const handlerP = fn => (...args) => {
|
||||
Promise.resolve(fn(...args)).then(
|
||||
|
||||
@@ -1247,11 +1247,6 @@ anymatch@^3.0.3:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arch@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.1.tgz#8f5c2731aa35a30929221bb0640eed65175ec84e"
|
||||
integrity sha512-BLM56aPo9vLLFVa8+/+pJLnrZ7QGGTVHWsCwieAWT9o9K8UeGaQbzZbGoabWLOo2ksBCztoXdqBZBplqLDDCSg==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@@ -1616,15 +1611,6 @@ cli-width@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
|
||||
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
|
||||
|
||||
clipboardy@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
|
||||
integrity sha512-mKhiIL2DrQIsuXMgBgnfEHOZOryC7kY7YO//TN6c63wlEm3NG5tz+YgY5rVi29KCmq/QQjKYvM7a19+MDOTHOQ==
|
||||
dependencies:
|
||||
arch "^2.1.1"
|
||||
execa "^1.0.0"
|
||||
is-wsl "^2.1.1"
|
||||
|
||||
cliui@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
|
||||
|
||||
@@ -7,12 +7,13 @@ Joi.address = () => {
|
||||
first_name: Joi.string().required(),
|
||||
last_name: Joi.string().required(),
|
||||
address_1: Joi.string().required(),
|
||||
address_2: Joi.string(),
|
||||
address_2: Joi.string().allow(""),
|
||||
city: Joi.string().required(),
|
||||
country_code: Joi.string().required(),
|
||||
province: Joi.string(),
|
||||
province: Joi.string().allow(""),
|
||||
postal_code: Joi.string().required(),
|
||||
metadata: Joi.object()
|
||||
phone: Joi.string().optional(),
|
||||
metadata: Joi.object(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
13
packages/medusa-file-spaces/.babelrc
Normal file
13
packages/medusa-file-spaces/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-file-spaces/.eslintrc
Normal file
9
packages/medusa-file-spaces/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
16
packages/medusa-file-spaces/.gitignore
vendored
Normal file
16
packages/medusa-file-spaces/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
9
packages/medusa-file-spaces/.npmignore
Normal file
9
packages/medusa-file-spaces/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
7
packages/medusa-file-spaces/.prettierrc
Normal file
7
packages/medusa-file-spaces/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
1
packages/medusa-file-spaces/index.js
Normal file
1
packages/medusa-file-spaces/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
46
packages/medusa-file-spaces/package.json
Normal file
46
packages/medusa-file-spaces/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "medusa-file-spaces",
|
||||
"version": "0.3.0",
|
||||
"description": "Digital Ocean Spaces file connector for Medusa",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-file-spaces"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d .",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"aws-sdk": "^2.710.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.3.0",
|
||||
"medusa-test-utils": "^0.3.0",
|
||||
"stripe": "^8.50.0"
|
||||
},
|
||||
"gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc"
|
||||
}
|
||||
73
packages/medusa-file-spaces/src/services/digital-ocean.js
Normal file
73
packages/medusa-file-spaces/src/services/digital-ocean.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import fs from "fs"
|
||||
import aws from "aws-sdk"
|
||||
import { FileService } from "medusa-interfaces"
|
||||
|
||||
class DigitalOceanService extends FileService {
|
||||
constructor({}, options) {
|
||||
super()
|
||||
|
||||
this.bucket_ = options.bucket
|
||||
this.spacesUrl_ = options.spaces_url
|
||||
this.accessKeyId_ = options.access_key_id
|
||||
this.secretAccessKey_ = options.secret_access_key
|
||||
this.region_ = options.region
|
||||
this.endpoint_ = options.endpoint
|
||||
}
|
||||
|
||||
upload(file) {
|
||||
aws.config.setPromisesDependency()
|
||||
aws.config.update({
|
||||
accessKeyId: this.accessKeyId_,
|
||||
secretAccessKey: this.secretAccessKey_,
|
||||
region: this.region_,
|
||||
endpoint: this.endpoint_,
|
||||
})
|
||||
|
||||
const s3 = new aws.S3()
|
||||
var params = {
|
||||
ACL: "public-read",
|
||||
Bucket: this.bucket_,
|
||||
Body: fs.createReadStream(file.path),
|
||||
Key: `${file.originalname}`,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.upload(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
|
||||
resolve({ url: data.Location })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
delete(file) {
|
||||
aws.config.setPromisesDependency()
|
||||
aws.config.update({
|
||||
accessKeyId: this.accessKeyId_,
|
||||
secretAccessKey: this.secretAccessKey_,
|
||||
region: this.region_,
|
||||
endpoint: this.endpoint_,
|
||||
})
|
||||
|
||||
const s3 = new aws.S3()
|
||||
var params = {
|
||||
Bucket: this.bucket_,
|
||||
Key: `${file}`,
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
s3.deleteObject(params, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default DigitalOceanService
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof"
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
@@ -9,4 +10,4 @@
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-fulfillment-manual/.eslintrc
Normal file
9
packages/medusa-fulfillment-manual/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
7
packages/medusa-fulfillment-manual/.prettierrc
Normal file
7
packages/medusa-fulfillment-manual/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
88
packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js
vendored
Normal file
88
packages/medusa-fulfillment-manual/dist/services/manual-fulfillment.js
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports["default"] = void 0;
|
||||
|
||||
var _medusaInterfaces = require("medusa-interfaces");
|
||||
|
||||
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
|
||||
|
||||
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
|
||||
|
||||
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }
|
||||
|
||||
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
|
||||
|
||||
function _createSuper(Derived) { return function () { var Super = _getPrototypeOf(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
|
||||
|
||||
function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }
|
||||
|
||||
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
|
||||
|
||||
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }
|
||||
|
||||
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
|
||||
|
||||
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 ManualFulfillmentService = /*#__PURE__*/function (_FulfillmentService) {
|
||||
_inherits(ManualFulfillmentService, _FulfillmentService);
|
||||
|
||||
var _super = _createSuper(ManualFulfillmentService);
|
||||
|
||||
function ManualFulfillmentService() {
|
||||
_classCallCheck(this, ManualFulfillmentService);
|
||||
|
||||
return _super.call(this);
|
||||
}
|
||||
|
||||
_createClass(ManualFulfillmentService, [{
|
||||
key: "getFulfillmentOptions",
|
||||
value: function getFulfillmentOptions() {
|
||||
return [{
|
||||
id: "manual-fulfillment"
|
||||
}];
|
||||
}
|
||||
}, {
|
||||
key: "validateFulfillmentData",
|
||||
value: function validateFulfillmentData(data, cart) {
|
||||
return data;
|
||||
}
|
||||
}, {
|
||||
key: "validateOption",
|
||||
value: function validateOption(data) {
|
||||
return true;
|
||||
}
|
||||
}, {
|
||||
key: "canCalculate",
|
||||
value: function canCalculate() {
|
||||
return false;
|
||||
}
|
||||
}, {
|
||||
key: "calculatePrice",
|
||||
value: function calculatePrice() {
|
||||
throw Error("Manual Fulfillment service cannot calculatePrice");
|
||||
}
|
||||
}, {
|
||||
key: "createOrder",
|
||||
value: function createOrder() {
|
||||
// No data is being sent anywhere
|
||||
return;
|
||||
}
|
||||
}]);
|
||||
|
||||
return ManualFulfillmentService;
|
||||
}(_medusaInterfaces.FulfillmentService);
|
||||
|
||||
_defineProperty(ManualFulfillmentService, "identifier", "manual");
|
||||
|
||||
var _default = ManualFulfillmentService;
|
||||
exports["default"] = _default;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "medusa-fulfillment-manual",
|
||||
"version": "0.1.27-alpha.0",
|
||||
"version": "1.0.0",
|
||||
"description": "A manual fulfillment provider for Medusa",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"jest": "^25.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --out-dir . --ignore **/__tests__",
|
||||
"build": "babel src -d dist",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__"
|
||||
},
|
||||
@@ -32,7 +32,6 @@
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.1.27-alpha.0"
|
||||
"medusa-core-utils": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FulfillmentService } from "medusa-interfaces"
|
||||
|
||||
class ManualFulfillmentService extends FulfillmentService {
|
||||
class ManualFulfillmentService extends FulfillmentService {
|
||||
static identifier = "manual"
|
||||
|
||||
constructor() {
|
||||
@@ -8,9 +8,11 @@ class ManualFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
|
||||
getFulfillmentOptions() {
|
||||
return [{
|
||||
id: "manual-fulfillment"
|
||||
}]
|
||||
return [
|
||||
{
|
||||
id: "manual-fulfillment",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
validateFulfillmentData(data, cart) {
|
||||
@@ -18,11 +20,7 @@ class ManualFulfillmentService extends FulfillmentService {
|
||||
}
|
||||
|
||||
validateOption(data) {
|
||||
if (data.id === "manual-fulfillment") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
canCalculate() {
|
||||
@@ -35,7 +33,7 @@ class ManualFulfillmentService extends FulfillmentService {
|
||||
|
||||
createOrder() {
|
||||
// No data is being sent anywhere
|
||||
return
|
||||
return Promise.resolve({})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ class BaseModel {
|
||||
return mongoose.model(this.getModelName(), this.getSchema())
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
startSession() {
|
||||
return this.mongooseModel_.startSession()
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the mongoose model via the mongoose's findOne.
|
||||
* @param query {object} a mongoose selector query
|
||||
@@ -51,7 +57,7 @@ class BaseModel {
|
||||
* @return {?mongoose.Document} the retreived mongoose document or null.
|
||||
*/
|
||||
findOne(query, options = {}) {
|
||||
return this.mongooseModel_.findOne(query, options)
|
||||
return this.mongooseModel_.findOne(query, options).lean()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,7 +68,7 @@ class BaseModel {
|
||||
* an empty array
|
||||
*/
|
||||
find(query, options) {
|
||||
return this.mongooseModel_.find(query, options)
|
||||
return this.mongooseModel_.find(query, options).lean()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +80,7 @@ class BaseModel {
|
||||
*/
|
||||
updateOne(query, update, options = {}) {
|
||||
options.new = true
|
||||
return this.mongooseModel_.findOneAndUpdate(query, update, options)
|
||||
return this.mongooseModel_.findOneAndUpdate(query, update, options).lean()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
21
packages/medusa-interfaces/src/file-service.js
Normal file
21
packages/medusa-interfaces/src/file-service.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import BaseService from "./base-service"
|
||||
|
||||
/**
|
||||
* Interface for file connectors
|
||||
* @interface
|
||||
*/
|
||||
class BaseFileService extends BaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
upload() {
|
||||
throw Error("upload must be overridden by the child class")
|
||||
}
|
||||
|
||||
delete() {
|
||||
throw Error("delete must be overridden by the child class")
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseFileService
|
||||
@@ -11,9 +11,12 @@ class BaseFulfillmentService extends BaseService {
|
||||
super()
|
||||
}
|
||||
|
||||
getFulfillmentOptions() {
|
||||
getIdentifier() {
|
||||
return this.constructor.identifier
|
||||
}
|
||||
|
||||
getFulfillmentOptions() {}
|
||||
|
||||
validateFulfillmentData(data, cart) {
|
||||
throw Error("validateFulfillmentData must be overridden by the child class")
|
||||
}
|
||||
|
||||
@@ -2,3 +2,5 @@ export { default as BaseService } from "./base-service"
|
||||
export { default as BaseModel } from "./base-model"
|
||||
export { default as PaymentService } from "./payment-service"
|
||||
export { default as FulfillmentService } from "./fulfillment-service"
|
||||
export { default as FileService } from "./file-service"
|
||||
export { default as OauthService } from "./oauth-service"
|
||||
|
||||
25
packages/medusa-interfaces/src/oauth-service.js
Normal file
25
packages/medusa-interfaces/src/oauth-service.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import BaseService from "./base-service"
|
||||
|
||||
/**
|
||||
* Interface for file connectors
|
||||
* @interface
|
||||
*/
|
||||
class BaseOauthService extends BaseService {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
generateToken() {
|
||||
throw Error("generateToken must be overridden by the child class")
|
||||
}
|
||||
|
||||
refreshToken() {
|
||||
throw Error("refreshToken must be overridden by the child class")
|
||||
}
|
||||
|
||||
destroyToken() {
|
||||
throw Error("destroyToken must be overridden by the child class")
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseOauthService
|
||||
@@ -11,6 +11,10 @@ class BasePaymentService extends BaseService {
|
||||
super()
|
||||
}
|
||||
|
||||
getIdentifier() {
|
||||
return this.constructor.identifier
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to create a payment to be processed with the service's payment gateway.
|
||||
* @param cart {object} - the cart that the payment should cover.
|
||||
@@ -61,6 +65,14 @@ class BasePaymentService extends BaseService {
|
||||
deletePayment() {
|
||||
throw Error("deletePayment must be overridden by the child class")
|
||||
}
|
||||
|
||||
/**
|
||||
* If the payment provider can save a payment method this function will
|
||||
* retrieve them.
|
||||
*/
|
||||
retrieveSavedMethods(customer) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
}
|
||||
|
||||
export default BasePaymentService
|
||||
|
||||
13
packages/medusa-payment-adyen/.babelrc
Normal file
13
packages/medusa-payment-adyen/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-payment-adyen/.eslintrc
Normal file
9
packages/medusa-payment-adyen/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
16
packages/medusa-payment-adyen/.gitignore
vendored
Normal file
16
packages/medusa-payment-adyen/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
9
packages/medusa-payment-adyen/.npmignore
Normal file
9
packages/medusa-payment-adyen/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
7
packages/medusa-payment-adyen/.prettierrc
Normal file
7
packages/medusa-payment-adyen/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
1
packages/medusa-payment-adyen/index.js
Normal file
1
packages/medusa-payment-adyen/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
47
packages/medusa-payment-adyen/package.json
Normal file
47
packages/medusa-payment-adyen/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "medusa-payment-adyen",
|
||||
"version": "0.3.0",
|
||||
"description": "Adyen Payment provider for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-payment-adyen"
|
||||
},
|
||||
"author": "Oliver Juhl",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d .",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@adyen/api-library": "^5.0.1",
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"axios": "^0.19.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.1.27",
|
||||
"medusa-interfaces": "^0.3.0",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc"
|
||||
}
|
||||
10
packages/medusa-payment-adyen/src/api/index.js
Normal file
10
packages/medusa-payment-adyen/src/api/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express"
|
||||
import store from "./routes/store"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default (rootDirectory) => {
|
||||
const app = Router()
|
||||
store(app, rootDirectory)
|
||||
hooks(app, rootDirectory)
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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]")
|
||||
}
|
||||
30
packages/medusa-payment-adyen/src/api/routes/hooks/index.js
Normal file
30
packages/medusa-payment-adyen/src/api/routes/hooks/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from "express"
|
||||
import cors from "cors"
|
||||
import bodyParser from "body-parser"
|
||||
import middlewares from "../../middlewares"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app, rootDirectory) => {
|
||||
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||
const config = (configModule && configModule.projectConfig) || {}
|
||||
|
||||
const storeCors = config.store_cors || ""
|
||||
route.use(
|
||||
cors({
|
||||
origin: storeCors.split(","),
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
|
||||
app.use("/adyen-hooks", route)
|
||||
|
||||
route.post(
|
||||
"/capture",
|
||||
bodyParser.json(),
|
||||
middlewares.wrap(require("./capture-hook").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
54
packages/medusa-payment-adyen/src/api/routes/store/index.js
Normal file
54
packages/medusa-payment-adyen/src/api/routes/store/index.js
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Router } from "express"
|
||||
import cors from "cors"
|
||||
import bodyParser from "body-parser"
|
||||
import middlewares from "../../middlewares"
|
||||
import { getConfigFile } from "medusa-core-utils"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app, rootDirectory) => {
|
||||
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
|
||||
const config = (configModule && configModule.projectConfig) || {}
|
||||
|
||||
const storeCors = config.store_cors || ""
|
||||
route.use(
|
||||
cors({
|
||||
origin: storeCors.split(","),
|
||||
credentials: true,
|
||||
})
|
||||
)
|
||||
|
||||
app.use("/adyen", route)
|
||||
|
||||
route.post(
|
||||
"/payment-methods",
|
||||
bodyParser.json(),
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
cart_id: Validator.string().required(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const adyenService = req.scope.resolve("adyenService")
|
||||
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 region = await regionService.retrieve(cart.region_id)
|
||||
const total = await totalsService.getTotal(cart)
|
||||
|
||||
const allowedMethods = cart.payment_sessions.map(
|
||||
(ps) => ps.provider_id.split("Adyen")[0]
|
||||
)
|
||||
|
||||
if (allowedMethods.length === 0) {
|
||||
res.status(200).json({ paymentMethods: {} })
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = await adyenService.retrievePaymentMethods(
|
||||
cart,
|
||||
allowedMethods,
|
||||
total,
|
||||
region.currency_code
|
||||
)
|
||||
|
||||
res.status(200).json({ paymentMethods: data })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
219
packages/medusa-payment-adyen/src/services/adyen.js
Normal file
219
packages/medusa-payment-adyen/src/services/adyen.js
Normal file
@@ -0,0 +1,219 @@
|
||||
import axios from "axios"
|
||||
import _ from "lodash"
|
||||
import { hmacValidator } from "@adyen/api-library"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
class AdyenService extends BaseService {
|
||||
constructor({ regionService, cartService, totalsService }, options) {
|
||||
super()
|
||||
|
||||
this.regionService_ = regionService
|
||||
|
||||
this.cartService_ = cartService
|
||||
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.adyenCheckoutApi = axios.create({
|
||||
baseURL: "https://checkout-test.adyen.com/v53",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-API-key": this.options_.api_key,
|
||||
},
|
||||
})
|
||||
|
||||
this.adyenPaymentApi = axios.create({
|
||||
baseURL: "https://pal-test.adyen.com/pal/servlet/Payment/v53",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-API-key": this.options_.api_key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.options_
|
||||
}
|
||||
|
||||
validateNotification(event) {
|
||||
const validator = new hmacValidator()
|
||||
const validated = validator.validateHMAC(
|
||||
event,
|
||||
this.options_.notification_hmac
|
||||
)
|
||||
return validated
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payment methods from Ayden using country as filter.
|
||||
* @param {string} countryCode - country code of cart
|
||||
* @param {string} shopperLocale - locale used on website
|
||||
* @returns {string} the status of the payment
|
||||
*/
|
||||
async retrievePaymentMethods(cart, allowedMethods, total, currency) {
|
||||
let request = {
|
||||
allowedPaymentMethods: allowedMethods,
|
||||
amount: {
|
||||
value: total * 100,
|
||||
currency: currency,
|
||||
},
|
||||
merchantAccount: this.options_.merchant_account,
|
||||
channel: this.options_.channel,
|
||||
}
|
||||
|
||||
if (cart.customer_id) {
|
||||
request.shopperReference = cart.customer_id
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.adyenCheckoutApi.post("/paymentMethods", request)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Status for Adyen payment.
|
||||
* @param {Object} paymentData - payment method data from cart
|
||||
* @returns {string} the status of the payment
|
||||
*/
|
||||
async getStatus(_) {
|
||||
let status = "initial"
|
||||
return status
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Adyen payment object.
|
||||
* @param {any} _ - placeholder object
|
||||
* @returns {Object} empty payment data
|
||||
*/
|
||||
async createPayment(_) {
|
||||
return {}
|
||||
}
|
||||
|
||||
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
|
||||
* @returns {Object} payment data result
|
||||
*/
|
||||
async authorizePayment(cart, paymentMethod) {
|
||||
const region = await this.regionService_.retrieve(cart.region_id)
|
||||
const total = await this.totalsService_.getTotal(cart)
|
||||
|
||||
let request = {
|
||||
amount: {
|
||||
currency: region.currency_code,
|
||||
value: total * 100,
|
||||
},
|
||||
shopperReference: cart.customer_id,
|
||||
paymentMethod,
|
||||
reference: cart._id,
|
||||
merchantAccount: this.options_.merchant_account,
|
||||
returnUrl: this.options_.return_url,
|
||||
metadata: {
|
||||
cart_id: cart._id,
|
||||
},
|
||||
}
|
||||
|
||||
if (paymentMethod.storedPaymentMethodId) {
|
||||
request.shopperInteraction = "Ecommerce"
|
||||
request.recurringProcessingModel = "CardOnFile"
|
||||
}
|
||||
|
||||
return await this.adyenCheckoutApi.post("/payments", request)
|
||||
}
|
||||
|
||||
async checkPaymentResult(paymentData, payload) {
|
||||
const request = {
|
||||
paymentData,
|
||||
details: {
|
||||
payload,
|
||||
},
|
||||
}
|
||||
return this.adyenCheckoutApi.post("/payments/details", request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures an Ayden payment
|
||||
* @param {Object} data - payment data to capture
|
||||
* @returns {Object} payment data result of capture
|
||||
*/
|
||||
async capturePayment(data) {
|
||||
const { pspReference, amount } = data
|
||||
|
||||
try {
|
||||
const captured = this.adyenPaymentApi.post("/capture", {
|
||||
originalReference: pspReference,
|
||||
modificationAmount: amount,
|
||||
merchantAccount: this.options_.merchant_account,
|
||||
})
|
||||
|
||||
if (
|
||||
captured.data.pspReference &&
|
||||
captured.data.response !== "[capture-received]"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Could not process capture"
|
||||
)
|
||||
}
|
||||
|
||||
return captured
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds an Ayden payment
|
||||
* @param {Object} paymentData - payment data to refund
|
||||
* @param {number} amountToRefund - amount to refund
|
||||
* @returns {Object} payment data result of refund
|
||||
*/
|
||||
async refundPayment(data) {
|
||||
const { pspReference, amount } = data
|
||||
|
||||
try {
|
||||
return this.adyenPaymentApi.post("/capture", {
|
||||
originalReference: pspReference,
|
||||
merchantAccount: this.options_.merchant_account,
|
||||
modificationAmount: amount,
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels an Ayden payment
|
||||
* @param {Object} paymentData - payment data to cancel
|
||||
* @returns {Object} payment data result of cancel
|
||||
*/
|
||||
async cancelPayment(paymentData) {
|
||||
const { pspReference } = paymentData
|
||||
|
||||
try {
|
||||
return this.adyenPaymentApi.post("/capture", {
|
||||
originalReference: pspReference,
|
||||
merchantAccount: this.options_.merchant_account,
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AdyenService
|
||||
74
packages/medusa-payment-adyen/src/services/applepay.js
Normal file
74
packages/medusa-payment-adyen/src/services/applepay.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
74
packages/medusa-payment-adyen/src/services/card.js
Normal file
74
packages/medusa-payment-adyen/src/services/card.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
74
packages/medusa-payment-adyen/src/services/googlepay.js
Normal file
74
packages/medusa-payment-adyen/src/services/googlepay.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
74
packages/medusa-payment-adyen/src/services/ideal.js
Normal file
74
packages/medusa-payment-adyen/src/services/ideal.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
74
packages/medusa-payment-adyen/src/services/mobilepay.js
Normal file
74
packages/medusa-payment-adyen/src/services/mobilepay.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
74
packages/medusa-payment-adyen/src/services/paypal.js
Normal file
74
packages/medusa-payment-adyen/src/services/paypal.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
13
packages/medusa-payment-klarna/.babelrc
Normal file
13
packages/medusa-payment-klarna/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-payment-klarna/.eslintrc
Normal file
9
packages/medusa-payment-klarna/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
16
packages/medusa-payment-klarna/.gitignore
vendored
Normal file
16
packages/medusa-payment-klarna/.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
!jest.config.js
|
||||
|
||||
/dist
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
|
||||
9
packages/medusa-payment-klarna/.npmignore
Normal file
9
packages/medusa-payment-klarna/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
7
packages/medusa-payment-klarna/.prettierrc
Normal file
7
packages/medusa-payment-klarna/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
1
packages/medusa-payment-klarna/index.js
Normal file
1
packages/medusa-payment-klarna/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
3
packages/medusa-payment-klarna/jest.config.js
Normal file
3
packages/medusa-payment-klarna/jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
}
|
||||
44
packages/medusa-payment-klarna/package.json
Normal file
44
packages/medusa-payment-klarna/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "medusa-payment-klarna",
|
||||
"version": "1.0.0",
|
||||
"description": "Klarna Payment provider for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-payment-klarna"
|
||||
},
|
||||
"author": "Oliver Juhl",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d dist",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"axios": "^0.19.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.3.0",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc"
|
||||
}
|
||||
5
packages/medusa-payment-klarna/src/__mocks__/axios.js
Normal file
5
packages/medusa-payment-klarna/src/__mocks__/axios.js
Normal file
@@ -0,0 +1,5 @@
|
||||
const mockAxios = jest.genMockFromModule("axios")
|
||||
|
||||
mockAxios.create = jest.fn(() => mockAxios)
|
||||
|
||||
export default mockAxios
|
||||
119
packages/medusa-payment-klarna/src/__mocks__/cart.js
Normal file
119
packages/medusa-payment-klarna/src/__mocks__/cart.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const carts = {
|
||||
frCart: {
|
||||
_id: IdMap.getId("fr-cart"),
|
||||
email: "lebron@james.com",
|
||||
title: "test",
|
||||
region_id: IdMap.getId("region-france"),
|
||||
items: [
|
||||
{
|
||||
_id: IdMap.getId("line"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: [
|
||||
{
|
||||
unit_price: 8,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-8-us-10"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
{
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
quantity: 10,
|
||||
},
|
||||
{
|
||||
_id: IdMap.getId("existingLine"),
|
||||
title: "merge line",
|
||||
description: "This is a new line",
|
||||
thumbnail: "test-img-yeah.com/thumb",
|
||||
content: {
|
||||
unit_price: 10,
|
||||
variant: {
|
||||
_id: IdMap.getId("eur-10-us-12"),
|
||||
},
|
||||
product: {
|
||||
_id: IdMap.getId("product"),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
quantity: 10,
|
||||
},
|
||||
],
|
||||
shipping_methods: [
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
shipping_options: [
|
||||
{
|
||||
_id: IdMap.getId("freeShipping"),
|
||||
profile_id: "default_profile",
|
||||
},
|
||||
],
|
||||
payment_sessions: [
|
||||
{
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_123456789",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
],
|
||||
payment_method: {
|
||||
provider_id: "stripe",
|
||||
data: {
|
||||
id: "pi_123456789",
|
||||
customer: IdMap.getId("not-lebron"),
|
||||
},
|
||||
},
|
||||
shipping_address: {},
|
||||
billing_address: {},
|
||||
discounts: [
|
||||
{
|
||||
code: "MEDUSA_FREE",
|
||||
discount_rule: {
|
||||
type: "percent",
|
||||
value: 20,
|
||||
allocation: "item",
|
||||
},
|
||||
},
|
||||
],
|
||||
customer_id: IdMap.getId("lebron"),
|
||||
},
|
||||
}
|
||||
|
||||
export const CartServiceMock = {
|
||||
retrieve: jest.fn().mockImplementation((cartId) => {
|
||||
if (cartId === IdMap.getId("fr-cart")) {
|
||||
return Promise.resolve(carts.frCart)
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
updatePaymentSession: jest
|
||||
.fn()
|
||||
.mockImplementation((cartId, stripe, paymentIntent) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return CartServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
21
packages/medusa-payment-klarna/src/__mocks__/region.js
Normal file
21
packages/medusa-payment-klarna/src/__mocks__/region.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const RegionServiceMock = {
|
||||
retrieve: jest.fn().mockImplementation((regionId) => {
|
||||
return Promise.resolve({
|
||||
_id: IdMap.getId("testRegion"),
|
||||
name: "Test Region",
|
||||
countries: ["DK", "US", "DE"],
|
||||
tax_rate: 0.25,
|
||||
payment_providers: ["default_provider", "unregistered"],
|
||||
fulfillment_providers: ["test_shipper"],
|
||||
currency_code: "usd",
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return RegionServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
11
packages/medusa-payment-klarna/src/__mocks__/totals.js
Normal file
11
packages/medusa-payment-klarna/src/__mocks__/totals.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const TotalsServiceMock = {
|
||||
getTotal: jest.fn(),
|
||||
getTaxTotal: jest.fn(),
|
||||
getAllocationItemDiscounts: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return TotalsServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
10
packages/medusa-payment-klarna/src/api/index.js
Normal file
10
packages/medusa-payment-klarna/src/api/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from "express"
|
||||
import hooks from "./routes/hooks"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
|
||||
hooks(app)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export default async (req, res) => {
|
||||
// In Medusa, we store the cart id in merchant_data
|
||||
const { shipping_address, merchant_data } = req.body
|
||||
|
||||
try {
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const klarnaProviderService = req.scope.resolve("pp_klarna")
|
||||
const shippingProfileService = req.scope.resolve("shippingProfileService")
|
||||
|
||||
const cart = await cartService.retrieve(merchant_data)
|
||||
|
||||
if (shipping_address) {
|
||||
const updatedAddress = {
|
||||
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.toUpperCase(),
|
||||
postal_code: shipping_address.postal_code,
|
||||
phone: shipping_address.phone
|
||||
}
|
||||
|
||||
await cartService.updateShippingAddress(cart._id, updatedAddress)
|
||||
await cartService.updateBillingAddress(cart._id, updatedAddress)
|
||||
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
|
||||
} else {
|
||||
res.sendStatus(400)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
16
packages/medusa-payment-klarna/src/api/routes/hooks/index.js
Normal file
16
packages/medusa-payment-klarna/src/api/routes/hooks/index.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
import middlewares from "../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/klarna", route)
|
||||
|
||||
route.use(bodyParser.json())
|
||||
route.post("/address", middlewares.wrap(require("./address").default))
|
||||
route.post("/shipping", middlewares.wrap(require("./shipping").default))
|
||||
route.post("/push", middlewares.wrap(require("./push").default))
|
||||
|
||||
return app
|
||||
}
|
||||
32
packages/medusa-payment-klarna/src/api/routes/hooks/push.js
Normal file
32
packages/medusa-payment-klarna/src/api/routes/hooks/push.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { klarna_order_id } = req.query
|
||||
|
||||
try {
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const klarnaProviderService = req.scope.resolve("pp_klarna")
|
||||
|
||||
const klarnaOrder = await klarnaProviderService.retrieveCompletedOrder(
|
||||
klarna_order_id
|
||||
).then(({ data }) => data)
|
||||
|
||||
const cartId = klarnaOrder.merchant_data
|
||||
try {
|
||||
const order = await orderService.retrieveByCartId(cartId)
|
||||
await klarnaProviderService.acknowledgeOrder(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)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export default async (req, res) => {
|
||||
// In Medusa, we store the cart id in merchant_data
|
||||
const { merchant_data, selected_shipping_option } = req.body
|
||||
|
||||
try {
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const klarnaProviderService = req.scope.resolve("pp_klarna")
|
||||
const shippingProfileService = req.scope.resolve("shippingProfileService")
|
||||
|
||||
const cart = await cartService.retrieve(merchant_data)
|
||||
const shippingOptions = await shippingProfileService.fetchCartOptions(cart)
|
||||
|
||||
const ids = selected_shipping_option.id.split(".")
|
||||
await Promise.all(ids.map(async id => {
|
||||
const option = shippingOptions.find(({ _id }) => _id.equals(id))
|
||||
|
||||
if (option) {
|
||||
await cartService.addShippingMethod(cart._id, option._id, option.data)
|
||||
}
|
||||
}))
|
||||
|
||||
const newCart = await cartService.retrieve(cart._id)
|
||||
const order = await klarnaProviderService.cartToKlarnaOrder(newCart)
|
||||
res.json(order)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const KlarnaProviderServiceMock = {
|
||||
retrievePayment: jest.fn().mockImplementation((cart) => {
|
||||
if (cart._id === IdMap.getId("fr-cart")) {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
cancelPayment: jest.fn().mockImplementation((cart) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
updatePayment: jest.fn().mockImplementation((cart) => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
capturePayment: jest.fn().mockImplementation((cart) => {
|
||||
if (cart._id === IdMap.getId("fr-cart")) {
|
||||
return Promise.resolve({
|
||||
id: "123456789",
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
createPayment: jest.fn().mockImplementation((cart) => {
|
||||
if (cart._id === IdMap.getId("fr-cart")) {
|
||||
return Promise.resolve({
|
||||
id: "123456789",
|
||||
order_amount: 100,
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return KlarnaProviderServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -0,0 +1,327 @@
|
||||
import KlarnaProviderService from "../klarna-provider"
|
||||
import mockAxios from "../../__mocks__/axios"
|
||||
import { carts } from "../../__mocks__/cart"
|
||||
import { TotalsServiceMock } from "../../__mocks__/totals"
|
||||
import { RegionServiceMock } from "../../__mocks__/region"
|
||||
|
||||
describe("KlarnaProviderService", () => {
|
||||
beforeEach(() => {
|
||||
mockAxios.mockClear()
|
||||
})
|
||||
|
||||
describe("createPayment", () => {
|
||||
let result
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
regionService: RegionServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
merchant_urls: {
|
||||
terms: "terms",
|
||||
checkout: "checkout",
|
||||
confirmation: "confirmation",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("creates Klarna order", async () => {
|
||||
mockAxios.post.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
order_amount: 100,
|
||||
})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.createPayment(carts.frCart)
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
order_amount: 100,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrievePayment", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns Klarna order", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.retrievePayment({
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveCompletedOrder", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns completed Klarna order", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.retrieveCompletedOrder({
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("updatePayment", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns updated Klarna order", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
order_id: "123456789",
|
||||
order_amount: 1000,
|
||||
})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.updatePayment(
|
||||
{
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "123456789",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
order_amount: 1000,
|
||||
}
|
||||
)
|
||||
|
||||
expect(result).toEqual({
|
||||
order_id: "123456789",
|
||||
order_amount: 1000,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelPayment", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.cancelPayment({ id: "123456789" })
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
})
|
||||
})
|
||||
|
||||
describe("acknowledgeOrder", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve({})
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.acknowledgeOrder("123456789")
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
})
|
||||
})
|
||||
|
||||
describe("addOrderToKlarnaOrder", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.addOrderToKlarnaOrder(
|
||||
"123456789",
|
||||
"order123456789"
|
||||
)
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
})
|
||||
})
|
||||
|
||||
describe("capturePayment", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.get.mockImplementation((data) => {
|
||||
return Promise.resolve({
|
||||
order: { order_amount: 1000 },
|
||||
})
|
||||
})
|
||||
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.capturePayment({
|
||||
id: "123456789",
|
||||
})
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
})
|
||||
})
|
||||
|
||||
describe("refundPayment", () => {
|
||||
let result
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
const klarnaProviderService = new KlarnaProviderService(
|
||||
{
|
||||
totalsService: TotalsServiceMock,
|
||||
},
|
||||
{
|
||||
url: "medusajs/tests",
|
||||
user: "lebronjames",
|
||||
password: "123456789",
|
||||
}
|
||||
)
|
||||
|
||||
it("returns order id", async () => {
|
||||
mockAxios.post.mockImplementation((data) => {
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
result = await klarnaProviderService.capturePayment(
|
||||
{
|
||||
id: "123456789",
|
||||
},
|
||||
1000
|
||||
)
|
||||
|
||||
expect(result).toEqual("123456789")
|
||||
})
|
||||
})
|
||||
})
|
||||
406
packages/medusa-payment-klarna/src/services/klarna-provider.js
Normal file
406
packages/medusa-payment-klarna/src/services/klarna-provider.js
Normal file
@@ -0,0 +1,406 @@
|
||||
import _ from "lodash"
|
||||
import axios from "axios"
|
||||
import { PaymentService } from "medusa-interfaces"
|
||||
|
||||
class KlarnaProviderService extends PaymentService {
|
||||
static identifier = "klarna"
|
||||
|
||||
constructor(
|
||||
{ shippingProfileService, totalsService, regionService },
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
|
||||
this.klarna_ = axios.create({
|
||||
baseURL: options.url,
|
||||
auth: {
|
||||
username: options.user,
|
||||
password: options.password,
|
||||
},
|
||||
})
|
||||
|
||||
this.klarnaOrderUrl_ = "/checkout/v3/orders"
|
||||
|
||||
this.klarnaOrderManagementUrl_ = "/ordermanagement/v1/orders"
|
||||
|
||||
this.backendUrl_ =
|
||||
process.env.BACKEND_URL || "https://c8e1abe7d8b3.ngrok.io"
|
||||
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
this.regionService_ = regionService
|
||||
|
||||
this.shippingProfileService_ = shippingProfileService
|
||||
}
|
||||
|
||||
async lineItemsToOrderLines_(cart, taxRate) {
|
||||
let order_lines = []
|
||||
|
||||
cart.items.forEach((item) => {
|
||||
// For bundles, we create an order line for each item in the bundle
|
||||
if (Array.isArray(item.content)) {
|
||||
item.content.forEach((c) => {
|
||||
const total_amount = c.unit_price * c.quantity * (taxRate + 1)
|
||||
const total_tax_amount = total_amount * taxRate
|
||||
|
||||
order_lines.push({
|
||||
name: item.title,
|
||||
unit_price: c.unit_price,
|
||||
quantity: c.quantity,
|
||||
tax_rate: taxRate * 10000,
|
||||
total_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) {
|
||||
const { name, price } = cart.shipping_methods.reduce((acc, next) => {
|
||||
acc.name = [...acc.name, next.name]
|
||||
acc.price += next.price
|
||||
return acc
|
||||
}, { name: [], price: 0 })
|
||||
|
||||
order_lines.push({
|
||||
name: name.join(" + "),
|
||||
quantity: 1,
|
||||
type: "shipping_fee",
|
||||
unit_price: price * (1 + taxRate) * 100,
|
||||
tax_rate: taxRate * 10000,
|
||||
total_amount: price * (1 + taxRate) * 100,
|
||||
total_tax_amount: price * taxRate * 100,
|
||||
})
|
||||
}
|
||||
|
||||
return order_lines
|
||||
}
|
||||
|
||||
async cartToKlarnaOrder(cart) {
|
||||
let order = {
|
||||
// Cart id is stored, such that we can use it for hooks
|
||||
merchant_data: cart._id,
|
||||
// TODO: Investigate if other locales are needed
|
||||
locale: "en-US",
|
||||
}
|
||||
|
||||
const { tax_rate, currency_code } = await this.regionService_.retrieve(
|
||||
cart.region_id
|
||||
)
|
||||
|
||||
order.order_lines = await this.lineItemsToOrderLines_(cart, tax_rate)
|
||||
|
||||
const discount = (await this.totalsService_.getDiscountTotal(cart)) * 100
|
||||
if (discount) {
|
||||
order.order_lines.push({
|
||||
name: `Discount`,
|
||||
quantity: 1,
|
||||
type: "discount",
|
||||
unit_price: 0,
|
||||
total_discount_amount: discount * (1 + tax_rate),
|
||||
tax_rate: tax_rate * 10000,
|
||||
total_amount: - discount * (1 + tax_rate),
|
||||
total_tax_amount: - discount * tax_rate
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isEmpty(cart.billing_address)) {
|
||||
order.billing_address = {
|
||||
email: cart.email,
|
||||
street_address: cart.billing_address.address_1,
|
||||
street_address2: cart.billing_address.address_2,
|
||||
postal_code: cart.billing_address.postal_code,
|
||||
city: cart.billing_address.city,
|
||||
country: cart.billing_address.country_code,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Check if country matches ISO
|
||||
if (!_.isEmpty(cart.billing_address) && cart.billing_address.country) {
|
||||
order.purchase_country = cart.billing_address.country
|
||||
} else {
|
||||
// Defaults to Sweden
|
||||
order.purchase_country = "SE"
|
||||
}
|
||||
order.order_amount = (await this.totalsService_.getTotal(cart)) * 100
|
||||
order.order_tax_amount = (await this.totalsService_.getTaxTotal(cart)) * 100
|
||||
// TODO: Check if currency matches ISO
|
||||
order.purchase_currency = currency_code
|
||||
|
||||
order.merchant_urls = {
|
||||
terms: this.options_.merchant_urls.terms,
|
||||
checkout: this.options_.merchant_urls.checkout,
|
||||
confirmation: this.options_.merchant_urls.confirmation,
|
||||
push: `${this.backendUrl_}/klarna/push?klarna_order_id={checkout.order.id}`,
|
||||
shipping_option_update: `${this.backendUrl_}/klarna/shipping`,
|
||||
address_update: `${this.backendUrl_}/klarna/address`,
|
||||
}
|
||||
|
||||
if (cart.shipping_address && cart.shipping_address.first_name) {
|
||||
const shippingOptions = await this.shippingProfileService_.fetchCartOptions(
|
||||
cart
|
||||
)
|
||||
|
||||
// If the cart does not have shipping methods yet, preselect one from
|
||||
// shipping_options and set the selected shipping method
|
||||
if (cart.shipping_methods.length) {
|
||||
const shipping_method = cart.shipping_methods[0]
|
||||
order.selected_shipping_option = {
|
||||
id: shipping_method._id,
|
||||
name: shipping_method.name,
|
||||
price: shipping_method.price * (1 + tax_rate) * 100,
|
||||
tax_amount: shipping_method.price * tax_rate * 100,
|
||||
tax_rate: tax_rate * 10000,
|
||||
}
|
||||
}
|
||||
|
||||
const partitioned = shippingOptions.reduce((acc, next) => {
|
||||
if (acc[next.profile_id]) {
|
||||
acc[next.profile_id] = [...acc[next.profile_id], next]
|
||||
} else {
|
||||
acc[next.profile_id] = [next]
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
let f = (a, b) => [].concat(...a.map(a => b.map(b => [].concat(a, b))))
|
||||
let cartesian = (a, b, ...c) => b ? cartesian(f(a, b), ...c) : a
|
||||
|
||||
const methods = Object.keys(partitioned).map(k => partitioned[k])
|
||||
const combinations = cartesian(...methods)
|
||||
|
||||
|
||||
order.shipping_options = combinations.map((combination) => {
|
||||
combination = Array.isArray(combination) ? combination : [combination]
|
||||
const details = combination.reduce((acc, next) => {
|
||||
acc.id = [...acc.id, next._id]
|
||||
acc.name = [...acc.name, next.name]
|
||||
acc.price += next.price
|
||||
return acc
|
||||
}, { id: [], name: [], price: 0 })
|
||||
|
||||
return {
|
||||
id: details.id.join("."),
|
||||
name: details.name.join(" + "),
|
||||
price: details.price * (1 + tax_rate) * 100,
|
||||
tax_amount: details.price * tax_rate * 100,
|
||||
tax_rate: tax_rate * 10000,
|
||||
preselected: combinations.length === 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
/**
|
||||
* Status for Klarna order.
|
||||
* @param {Object} paymentData - payment method data from cart
|
||||
* @returns {string} the status of the Klarna order
|
||||
*/
|
||||
async getStatus(paymentData) {
|
||||
try {
|
||||
const { order_id } = paymentData
|
||||
const { data: order } = await this.klarna_.get(
|
||||
`${this.klarnaOrderUrl_}/${order_id}`
|
||||
)
|
||||
|
||||
let status = "initial"
|
||||
if (order.status === "checkout_complete") {
|
||||
status = "authorized"
|
||||
}
|
||||
return status
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Stripe PaymentIntent.
|
||||
* @param {string} cart - the cart to create a payment for
|
||||
* @param {number} amount - the amount to create a payment for
|
||||
* @returns {string} id of payment intent
|
||||
*/
|
||||
async createPayment(cart) {
|
||||
try {
|
||||
const order = await this.cartToKlarnaOrder(cart)
|
||||
return this.klarna_
|
||||
.post(this.klarnaOrderUrl_, order)
|
||||
.then(({ data }) => data)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves Klarna Order.
|
||||
* @param {string} cart - the cart to retrieve order for
|
||||
* @returns {Object} Klarna order
|
||||
*/
|
||||
async retrievePayment(paymentData) {
|
||||
try {
|
||||
return this.klarna_.get(`${this.klarnaOrderUrl_}/${paymentData.order_id}`)
|
||||
.then(({ data }) => data)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves completed Klarna Order.
|
||||
* @param {string} klarnaOrderId - id of the order to retrieve
|
||||
* @returns {Object} Klarna order
|
||||
*/
|
||||
async retrieveCompletedOrder(klarnaOrderId) {
|
||||
try {
|
||||
return this.klarna_.get(
|
||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}`
|
||||
)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acknowledges a Klarna order as part of the order completion process
|
||||
* @param {string} klarnaOrderId - id of the order to acknowledge
|
||||
* @returns {string} id of acknowledged order
|
||||
*/
|
||||
async acknowledgeOrder(klarnaOrderId, orderId) {
|
||||
try {
|
||||
await this.klarna_.post(
|
||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/acknowledge`
|
||||
)
|
||||
|
||||
await this.klarna_.patch(
|
||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
|
||||
{
|
||||
merchant_reference1: orderId
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return klarnaOrderId
|
||||
} catch (error) {
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the id of the Medusa order to the Klarna Order to create a relation
|
||||
* @param {string} klarnaOrderId - id of the klarna order
|
||||
* @param {string} orderId - id of the Medusa order
|
||||
* @returns {string} id of updated order
|
||||
*/
|
||||
async addOrderToKlarnaOrder(klarnaOrderId, orderId) {
|
||||
try {
|
||||
await this.klarna_.post(
|
||||
`${this.klarnaOrderManagementUrl_}/${klarnaOrderId}/merchant-references`,
|
||||
{
|
||||
merchant_reference1: orderId,
|
||||
}
|
||||
)
|
||||
|
||||
return klarnaOrderId
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Klarna order.
|
||||
* @param {string} order - the order to update
|
||||
* @param {Object} data - the update object
|
||||
* @returns {Object} updated order
|
||||
*/
|
||||
async updatePayment(paymentData, cart) {
|
||||
try {
|
||||
const order = await this.cartToKlarnaOrder(cart, true)
|
||||
return this.klarna_
|
||||
.post(`${this.klarnaOrderUrl_}/${paymentData.order_id}`, order)
|
||||
.then(({ data }) => data)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures Klarna order.
|
||||
* @param {Object} paymentData - payment method data from cart
|
||||
* @returns {string} id of captured order
|
||||
*/
|
||||
async capturePayment(paymentData) {
|
||||
try {
|
||||
const { order_id } = paymentData
|
||||
const orderData = await this.klarna_.get(
|
||||
`${this.klarnaOrderUrl_}/${order_id}`
|
||||
)
|
||||
const { order_amount } = orderData.order
|
||||
|
||||
await this.klarna_.post(
|
||||
`${this.klarnaOrderManagementUrl_}/${order_id}/captures`,
|
||||
{
|
||||
captured_amount: order_amount,
|
||||
}
|
||||
)
|
||||
return order_id
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refunds payment for Klarna Order.
|
||||
* @param {Object} paymentData - payment method data from cart
|
||||
* @returns {string} id of refunded order
|
||||
*/
|
||||
async refundPayment(paymentData, amount) {
|
||||
try {
|
||||
const { order_id } = paymentData
|
||||
await this.klarna_.post(
|
||||
`${this.klarnaOrderManagementUrl_}/${order_id}/refunds`,
|
||||
{
|
||||
refunded_amount: amount,
|
||||
}
|
||||
)
|
||||
return order_id
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels payment for Klarna Order.
|
||||
* @param {Object} paymentData - payment method data from cart
|
||||
* @returns {string} id of cancelled order
|
||||
*/
|
||||
async cancelPayment(paymentData) {
|
||||
try {
|
||||
const { order_id } = paymentData
|
||||
await this.klarna_.post(`${this.klarnaOrderUrl_}/${order_id}/cancel`)
|
||||
return order_id
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default KlarnaProviderService
|
||||
5709
packages/medusa-payment-klarna/yarn.lock
Normal file
5709
packages/medusa-payment-klarna/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
1
packages/medusa-payment-stripe/.gitignore
vendored
1
packages/medusa-payment-stripe/.gitignore
vendored
@@ -12,4 +12,5 @@ yarn.lock
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
|
||||
@@ -23,10 +23,11 @@
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2"
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d dist",
|
||||
"build": "babel src -d .",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
|
||||
@@ -5,10 +5,10 @@ import middlewares from "../../middlewares"
|
||||
const route = Router()
|
||||
|
||||
export default (app) => {
|
||||
app.use("/hooks", route)
|
||||
app.use("/stripe", route)
|
||||
|
||||
route.post(
|
||||
"/stripe",
|
||||
"/hooks",
|
||||
// stripe constructEvent fails without body-parser
|
||||
bodyParser.raw({ type: "application/json" }),
|
||||
middlewares.wrap(require("./stripe").default)
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async (req, res) => {
|
||||
|
||||
let event
|
||||
try {
|
||||
const stripeProviderService = req.resolve("pp_stripe")
|
||||
const stripeProviderService = req.scope.resolve("pp_stripe")
|
||||
event = stripeProviderService.constructWebhookEvent(req.body, signature)
|
||||
} catch (err) {
|
||||
res.status(400).send(`Webhook Error: ${err.message}`)
|
||||
@@ -12,19 +12,37 @@ export default async (req, res) => {
|
||||
|
||||
const paymentIntent = event.data.object
|
||||
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
const cartId = paymentIntent.metadata.cart_id
|
||||
const order = await orderService.retrieveByCartId(cartId)
|
||||
.catch(() => undefined)
|
||||
|
||||
// handle payment intent events
|
||||
switch (event.type) {
|
||||
case "payment_intent.succeeded":
|
||||
if (order) {
|
||||
await orderService.update(order._id, {
|
||||
payment_status: "captured",
|
||||
})
|
||||
}
|
||||
break
|
||||
case "payment_intent.canceled":
|
||||
break
|
||||
case "payment_intent.created":
|
||||
case "payment_intent.cancelled":
|
||||
if (order) {
|
||||
await orderService.update(order._id, {
|
||||
status: "cancelled",
|
||||
})
|
||||
}
|
||||
break
|
||||
case "payment_intent.payment_failed":
|
||||
// TODO: Not implemented yet
|
||||
break
|
||||
case "payment_intent.amount_capturable_updated":
|
||||
break
|
||||
case "payment_intent.processing":
|
||||
if (!order) {
|
||||
const cart = await cartService.retrieve(cartId)
|
||||
await orderService.createFromCart(cart)
|
||||
}
|
||||
break
|
||||
default:
|
||||
res.status(400)
|
||||
|
||||
@@ -120,14 +120,10 @@ describe("StripeProviderService", () => {
|
||||
|
||||
result = await stripeProviderService.updatePayment(
|
||||
{
|
||||
payment_method: {
|
||||
data: {
|
||||
id: "pi_lebron",
|
||||
},
|
||||
},
|
||||
id: "pi_lebron",
|
||||
},
|
||||
{
|
||||
amount: 1000,
|
||||
total: 1000,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@ import { PaymentService } from "medusa-interfaces"
|
||||
class StripeProviderService extends PaymentService {
|
||||
static identifier = "stripe"
|
||||
|
||||
constructor({ customerService, totalsService }, options) {
|
||||
constructor({ customerService, totalsService, regionService }, options) {
|
||||
super()
|
||||
|
||||
this.options_ = options
|
||||
@@ -14,6 +14,8 @@ class StripeProviderService extends PaymentService {
|
||||
|
||||
this.customerService_ = customerService
|
||||
|
||||
this.regionService_ = regionService
|
||||
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
@@ -33,7 +35,7 @@ class StripeProviderService extends PaymentService {
|
||||
return status
|
||||
}
|
||||
|
||||
if (paymentIntent.status === "requires_action") {
|
||||
if (paymentIntent.status === "requires_capture") {
|
||||
status = "authorized"
|
||||
}
|
||||
|
||||
@@ -48,7 +50,22 @@ class StripeProviderService extends PaymentService {
|
||||
return status
|
||||
}
|
||||
|
||||
async retrieveSavedMethods(customer) {
|
||||
if (customer.metadata && customer.metadata.stripe_id) {
|
||||
const methods = await this.stripe_.paymentMethods.list({
|
||||
customer: customer.metadata.stripe_id, type: "card"
|
||||
})
|
||||
|
||||
return methods.data
|
||||
}
|
||||
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
async retrieveCustomer(customerId) {
|
||||
if (!customerId) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
return this.stripe_.customers.retrieve(customerId)
|
||||
}
|
||||
|
||||
@@ -76,10 +93,12 @@ class StripeProviderService extends PaymentService {
|
||||
* @returns {string} id of payment intent
|
||||
*/
|
||||
async createPayment(cart) {
|
||||
const { customer_id } = cart
|
||||
const { customer_id, region_id } = cart
|
||||
const { currency_code } = await this.regionService_.retrieve(region_id)
|
||||
|
||||
console.log(customer_id)
|
||||
|
||||
let stripeCustomerId
|
||||
|
||||
if (!customer_id) {
|
||||
const { id } = await this.stripe_.customers.create({
|
||||
email: cart.email,
|
||||
@@ -87,7 +106,8 @@ class StripeProviderService extends PaymentService {
|
||||
stripeCustomerId = id
|
||||
} else {
|
||||
const customer = await this.customerService_.retrieve(customer_id)
|
||||
if (!customer.metadata.stripe_id) {
|
||||
console.log(customer)
|
||||
if (!(customer.metadata && customer.metadata.stripe_id)) {
|
||||
const { id } = await this.stripe_.customers.create({
|
||||
email: customer.email,
|
||||
})
|
||||
@@ -97,10 +117,14 @@ class StripeProviderService extends PaymentService {
|
||||
}
|
||||
}
|
||||
|
||||
const amount = this.totalsService_.getTotal(cart)
|
||||
const amount = await this.totalsService_.getTotal(cart)
|
||||
const paymentIntent = await this.stripe_.paymentIntents.create({
|
||||
customer: stripeCustomerId,
|
||||
amount,
|
||||
amount: amount * 100, // Stripe amount is in cents
|
||||
currency: currency_code,
|
||||
setup_future_usage: "on_session",
|
||||
capture_method: "manual",
|
||||
metadata: { cart_id: `${cart._id}` },
|
||||
})
|
||||
|
||||
return paymentIntent
|
||||
@@ -108,12 +132,11 @@ class StripeProviderService extends PaymentService {
|
||||
|
||||
/**
|
||||
* Retrieves Stripe PaymentIntent.
|
||||
* @param {string} cart - the cart to retrieve payment intent for
|
||||
* @param {object} data - the data of the payment to retrieve
|
||||
* @returns {Object} Stripe PaymentIntent
|
||||
*/
|
||||
async retrievePayment(cart) {
|
||||
async retrievePayment(data) {
|
||||
try {
|
||||
const { data } = cart.payment_method
|
||||
return this.stripe_.paymentIntents.retrieve(data.id)
|
||||
} catch (error) {
|
||||
throw error
|
||||
@@ -122,14 +145,32 @@ class StripeProviderService extends PaymentService {
|
||||
|
||||
/**
|
||||
* Updates Stripe PaymentIntent.
|
||||
* @param {string} cart - the cart to update payment intent for
|
||||
* @param {Object} data - the update object for the payment intent
|
||||
* @param {object} data - The payment session data.
|
||||
* @param {Object} cart - the current cart value
|
||||
* @returns {Object} Stripe PaymentIntent
|
||||
*/
|
||||
async updatePayment(cart, update) {
|
||||
async updatePayment(data, cart) {
|
||||
try {
|
||||
const { data } = cart.payment_method
|
||||
return this.stripe_.paymentIntents.update(data.id, update)
|
||||
const { id } = data
|
||||
const amount = this.totalsService_.getTotal(cart)
|
||||
return this.stripe_.paymentIntents.update(id, {
|
||||
amount: amount * 100,
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deletePayment(data) {
|
||||
try {
|
||||
const { id } = data
|
||||
return this.stripe_.paymentIntents.cancel(id)
|
||||
.catch(err => {
|
||||
if (err.statusCode === 400) {
|
||||
return
|
||||
}
|
||||
throw err
|
||||
})
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -174,7 +215,7 @@ class StripeProviderService extends PaymentService {
|
||||
const { id } = paymentData
|
||||
try {
|
||||
return this.stripe_.refunds.create({
|
||||
amount,
|
||||
amount: amount * 100,
|
||||
payment_intent: id,
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,10 +10,6 @@ class CartSubscriber {
|
||||
this.stripeProviderService_ = stripeProviderService
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
this.eventBus_.subscribe("cart.created", (data) => {
|
||||
console.log(data)
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("cart.customer_updated", async (cart) => {
|
||||
await this.onCustomerUpdated(cart)
|
||||
})
|
||||
@@ -28,8 +24,14 @@ class CartSubscriber {
|
||||
|
||||
const customer = await this.customerService_.retrieve(customer_id)
|
||||
|
||||
const stripeSession = payment_sessions.find(s => s.provider_id === "stripe")
|
||||
|
||||
if (!stripeSession) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const paymentIntent = await this.stripeProviderService_.retrievePayment(
|
||||
cart
|
||||
stripeSession.data
|
||||
)
|
||||
|
||||
let stripeCustomer = await this.stripeProviderService_.retrieveCustomer(
|
||||
@@ -53,7 +55,7 @@ class CartSubscriber {
|
||||
}
|
||||
|
||||
if (stripeCustomer.id !== paymentIntent.customer) {
|
||||
await this.stripeProviderService_.cancelPayment(paymentIntent.id)
|
||||
await this.stripeProviderService_.cancelPayment(paymentIntent)
|
||||
const newPaymentIntent = await this.stripeProviderService_.createPayment(
|
||||
cart
|
||||
)
|
||||
|
||||
13
packages/medusa-plugin-brightpearl/.babelrc
Normal file
13
packages/medusa-plugin-brightpearl/.babelrc
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
9
packages/medusa-plugin-brightpearl/.eslintrc
Normal file
9
packages/medusa-plugin-brightpearl/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
15
packages/medusa-plugin-brightpearl/.gitignore
vendored
Normal file
15
packages/medusa-plugin-brightpearl/.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/dist
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
|
||||
9
packages/medusa-plugin-brightpearl/.npmignore
Normal file
9
packages/medusa-plugin-brightpearl/.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/lib
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
|
||||
7
packages/medusa-plugin-brightpearl/.prettierrc
Normal file
7
packages/medusa-plugin-brightpearl/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"endOfLine": "lf",
|
||||
"semi": false,
|
||||
"singleQuote": false,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
1
packages/medusa-plugin-brightpearl/index.js
Normal file
1
packages/medusa-plugin-brightpearl/index.js
Normal file
@@ -0,0 +1 @@
|
||||
// noop
|
||||
61
packages/medusa-plugin-brightpearl/loaders/inventory.js
Normal file
61
packages/medusa-plugin-brightpearl/loaders/inventory.js
Normal file
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports["default"] = void 0;
|
||||
|
||||
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
||||
|
||||
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
||||
|
||||
var inventorySync = /*#__PURE__*/function () {
|
||||
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) {
|
||||
var brightpearlService, eventBus, client, pattern;
|
||||
return regeneratorRuntime.wrap(function _callee$(_context) {
|
||||
while (1) {
|
||||
switch (_context.prev = _context.next) {
|
||||
case 0:
|
||||
brightpearlService = container.resolve("brightpearlService");
|
||||
eventBus = container.resolve("eventBusService");
|
||||
_context.prev = 2;
|
||||
_context.next = 5;
|
||||
return brightpearlService.getClient();
|
||||
|
||||
case 5:
|
||||
client = _context.sent;
|
||||
pattern = "43 4,10,14,20 * * *"; // nice for tests "*/10 * * * * *"
|
||||
|
||||
eventBus.createCronJob("inventory-sync", {}, pattern, brightpearlService.syncInventory());
|
||||
_context.next = 15;
|
||||
break;
|
||||
|
||||
case 10:
|
||||
_context.prev = 10;
|
||||
_context.t0 = _context["catch"](2);
|
||||
|
||||
if (!(_context.t0.name === "not_allowed")) {
|
||||
_context.next = 14;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context.abrupt("return");
|
||||
|
||||
case 14:
|
||||
throw _context.t0;
|
||||
|
||||
case 15:
|
||||
case "end":
|
||||
return _context.stop();
|
||||
}
|
||||
}
|
||||
}, _callee, null, [[2, 10]]);
|
||||
}));
|
||||
|
||||
return function inventorySync(_x) {
|
||||
return _ref.apply(this, arguments);
|
||||
};
|
||||
}();
|
||||
|
||||
var _default = inventorySync;
|
||||
exports["default"] = _default;
|
||||
61
packages/medusa-plugin-brightpearl/loaders/webhooks.js
Normal file
61
packages/medusa-plugin-brightpearl/loaders/webhooks.js
Normal file
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports["default"] = void 0;
|
||||
|
||||
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
|
||||
|
||||
function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
|
||||
|
||||
var webhookLoader = /*#__PURE__*/function () {
|
||||
var _ref = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(container) {
|
||||
var brightpearlService, client;
|
||||
return regeneratorRuntime.wrap(function _callee$(_context) {
|
||||
while (1) {
|
||||
switch (_context.prev = _context.next) {
|
||||
case 0:
|
||||
brightpearlService = container.resolve("brightpearlService");
|
||||
_context.prev = 1;
|
||||
_context.next = 4;
|
||||
return brightpearlService.getClient();
|
||||
|
||||
case 4:
|
||||
client = _context.sent;
|
||||
_context.next = 7;
|
||||
return brightpearlService.verifyWebhooks();
|
||||
|
||||
case 7:
|
||||
_context.next = 14;
|
||||
break;
|
||||
|
||||
case 9:
|
||||
_context.prev = 9;
|
||||
_context.t0 = _context["catch"](1);
|
||||
|
||||
if (!(_context.t0.name === "not_allowed")) {
|
||||
_context.next = 13;
|
||||
break;
|
||||
}
|
||||
|
||||
return _context.abrupt("return");
|
||||
|
||||
case 13:
|
||||
throw _context.t0;
|
||||
|
||||
case 14:
|
||||
case "end":
|
||||
return _context.stop();
|
||||
}
|
||||
}
|
||||
}, _callee, null, [[1, 9]]);
|
||||
}));
|
||||
|
||||
return function webhookLoader(_x) {
|
||||
return _ref.apply(this, arguments);
|
||||
};
|
||||
}();
|
||||
|
||||
var _default = webhookLoader;
|
||||
exports["default"] = _default;
|
||||
44
packages/medusa-plugin-brightpearl/package.json
Normal file
44
packages/medusa-plugin-brightpearl/package.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"name": "medusa-plugin-brightpearl",
|
||||
"version": "1.0.0",
|
||||
"description": "Brightpearl plugin for Medusa Commerce",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/medusa-plugin-brightpearl"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/node": "^7.7.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@babel/plugin-transform-instanceof": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"client-sessions": "^0.8.0",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
"medusa-test-utils": "^0.3.0",
|
||||
"prettier": "^2.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src -d dist",
|
||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||
"watch": "babel -w src --out-dir . --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.19.2",
|
||||
"express": "^4.17.1",
|
||||
"medusa-core-utils": "^0.3.0",
|
||||
"medusa-interfaces": "^0.3.0",
|
||||
"randomatic": "^3.1.1"
|
||||
}
|
||||
}
|
||||
30
packages/medusa-plugin-brightpearl/src/api/index.js
Normal file
30
packages/medusa-plugin-brightpearl/src/api/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Router } from "express"
|
||||
import bodyParser from "body-parser"
|
||||
|
||||
export default (container) => {
|
||||
const app = Router()
|
||||
|
||||
app.post("/brightpearl/goods-out", bodyParser.json(), async (req, res) => {
|
||||
const { id, lifecycle_event } = req.body
|
||||
const brightpearlService = req.scope.resolve("brightpearlService")
|
||||
|
||||
if (lifecycle_event === "created") {
|
||||
await brightpearlService.createFulfillmentFromGoodsOut(id)
|
||||
}
|
||||
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
app.post(
|
||||
"/brightpearl/inventory-update",
|
||||
bodyParser.json(),
|
||||
async (req, res) => {
|
||||
const { id } = req.body
|
||||
const brightpearlService = req.scope.resolve("brightpearlService")
|
||||
await brightpearlService.updateInventory(id)
|
||||
res.sendStatus(200)
|
||||
}
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
22
packages/medusa-plugin-brightpearl/src/loaders/inventory.js
Normal file
22
packages/medusa-plugin-brightpearl/src/loaders/inventory.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const inventorySync = async (container) => {
|
||||
const brightpearlService = container.resolve("brightpearlService")
|
||||
const eventBus = container.resolve("eventBusService")
|
||||
|
||||
try {
|
||||
const client = await brightpearlService.getClient()
|
||||
const pattern = "43 4,10,14,20 * * *" // nice for tests "*/10 * * * * *"
|
||||
eventBus.createCronJob(
|
||||
"inventory-sync",
|
||||
{},
|
||||
pattern,
|
||||
brightpearlService.syncInventory()
|
||||
)
|
||||
} catch (err) {
|
||||
if (err.name === "not_allowed") {
|
||||
return
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export default inventorySync
|
||||
14
packages/medusa-plugin-brightpearl/src/loaders/webhooks.js
Normal file
14
packages/medusa-plugin-brightpearl/src/loaders/webhooks.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const webhookLoader = async (container) => {
|
||||
const brightpearlService = container.resolve("brightpearlService")
|
||||
try {
|
||||
const client = await brightpearlService.getClient()
|
||||
await brightpearlService.verifyWebhooks()
|
||||
} catch (err) {
|
||||
if (err.name === "not_allowed") {
|
||||
return
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
export default webhookLoader
|
||||
607
packages/medusa-plugin-brightpearl/src/services/brightpearl.js
Normal file
607
packages/medusa-plugin-brightpearl/src/services/brightpearl.js
Normal file
@@ -0,0 +1,607 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import Brightpearl from "../utils/brightpearl"
|
||||
|
||||
class BrightpearlService extends BaseService {
|
||||
constructor(
|
||||
{
|
||||
oauthService,
|
||||
totalsService,
|
||||
productVariantService,
|
||||
regionService,
|
||||
orderService,
|
||||
discountService,
|
||||
},
|
||||
options
|
||||
) {
|
||||
super()
|
||||
|
||||
this.options = options
|
||||
this.productVariantService_ = productVariantService
|
||||
this.regionService_ = regionService
|
||||
this.orderService_ = orderService
|
||||
this.totalsService_ = totalsService
|
||||
this.discountService_ = discountService
|
||||
this.oauthService_ = oauthService
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
if (this.brightpearlClient_) {
|
||||
return this.brightpearlClient_
|
||||
}
|
||||
|
||||
const authData = await this.oauthService_.retrieveByName("brightpearl")
|
||||
const { data } = authData
|
||||
|
||||
if (!data || !data.access_token) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You must authenticate the Brightpearl app in settings before continuing"
|
||||
)
|
||||
}
|
||||
|
||||
const client = new Brightpearl({
|
||||
account: this.options.account,
|
||||
url: data.api_domain,
|
||||
auth_type: data.token_type,
|
||||
access_token: data.access_token,
|
||||
})
|
||||
|
||||
this.authData_ = data
|
||||
this.brightpearlClient_ = client
|
||||
return client
|
||||
}
|
||||
|
||||
async getAuthData() {
|
||||
if (this.authData_) {
|
||||
return this.authData_
|
||||
}
|
||||
|
||||
const { data } = await this.oauthService_.retrieveByName("brightpearl")
|
||||
if (!data || !data.access_token) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You must authenticate the Brightpearl app in settings before continuing"
|
||||
)
|
||||
}
|
||||
|
||||
this.authData_ = data
|
||||
return data
|
||||
}
|
||||
|
||||
async verifyWebhooks() {
|
||||
const brightpearl = await this.getClient()
|
||||
const hooks = [
|
||||
{
|
||||
subscribeTo: "goods-out-note.created",
|
||||
httpMethod: "POST",
|
||||
uriTemplate: `${this.options.backend_url}/brightpearl/goods-out`,
|
||||
bodyTemplate:
|
||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
||||
contentType: "application/json",
|
||||
idSetAccepted: false,
|
||||
},
|
||||
{
|
||||
subscribeTo: "product.modified.on-hand-modified",
|
||||
httpMethod: "POST",
|
||||
uriTemplate: `${this.options.backend_url}/brightpearl/inventory-update`,
|
||||
bodyTemplate:
|
||||
'{"account": "${account-code}", "lifecycle_event": "${lifecycle-event}", "resource_type": "${resource-type}", "id": "${resource-id}" }',
|
||||
contentType: "application/json",
|
||||
idSetAccepted: false,
|
||||
},
|
||||
]
|
||||
|
||||
const installedHooks = await brightpearl.webhooks.list().catch(() => [])
|
||||
for (const hook of hooks) {
|
||||
const isInstalled = installedHooks.find(
|
||||
(i) =>
|
||||
i.subscribeTo === hook.subscribeTo &&
|
||||
i.httpMethod === hook.httpMethod &&
|
||||
i.uriTemplate === hook.uriTemplate &&
|
||||
i.bodyTemplate === hook.bodyTemplate &&
|
||||
i.contentType === hook.contentType &&
|
||||
i.idSetAccepted === hook.idSetAccepted
|
||||
)
|
||||
|
||||
if (!isInstalled) {
|
||||
await brightpearl.webhooks.create(hook)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async syncInventory() {
|
||||
const client = await this.getClient()
|
||||
const variants = await this.productVariantService_.list()
|
||||
return Promise.all(
|
||||
variants.map(async (v) => {
|
||||
const brightpearlProduct = await this.retrieveProductBySKU(v.sku)
|
||||
if (!brightpearlProduct) {
|
||||
return
|
||||
}
|
||||
|
||||
const { productId } = brightpearlProduct
|
||||
const availability = await client.products.retrieveAvailability(
|
||||
productId
|
||||
)
|
||||
const onHand = availability[productId].total.onHand
|
||||
|
||||
return this.productVariantService_.update(v._id, {
|
||||
inventory_quantity: onHand,
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async updateInventory(productId) {
|
||||
const client = await this.getClient()
|
||||
const availability = await client.products
|
||||
.retrieveAvailability(productId)
|
||||
.catch(() => null)
|
||||
|
||||
if (availability) {
|
||||
const brightpearlProduct = await client.products.retrieve(productId)
|
||||
const onHand = availability[productId].total.onHand
|
||||
|
||||
const sku = brightpearlProduct.identity.sku
|
||||
const [variant] = await this.productVariantService_.list({ sku })
|
||||
|
||||
if (variant && variant.manage_inventory) {
|
||||
await this.productVariantService_.update(variant._id, {
|
||||
inventory_quantity: onHand,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async createGoodsOutNote(fromOrder, shipment) {
|
||||
const client = await this.getClient()
|
||||
const id =
|
||||
fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id
|
||||
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
|
||||
const order = await client.orders.retrieve(id)
|
||||
const productRows = shipment.item_ids.map((id) => {
|
||||
const row = order.rows.find(({ externalRef }) => externalRef === id)
|
||||
return {
|
||||
productId: row.productId,
|
||||
salesOrderRowId: row.id,
|
||||
quantity: row.quantity,
|
||||
}
|
||||
})
|
||||
|
||||
const goodsOut = {
|
||||
warehouses: [
|
||||
{
|
||||
releaseDate: new Date(),
|
||||
warehouseId: this.options.warehouse,
|
||||
transfer: false,
|
||||
products: productRows,
|
||||
},
|
||||
],
|
||||
priority: false,
|
||||
}
|
||||
|
||||
return client.warehouses.createGoodsOutNote(id, goodsOut)
|
||||
}
|
||||
|
||||
async registerGoodsOutShipped(noteId, shipment) {
|
||||
const client = await this.getClient()
|
||||
return client.warehouses.registerGoodsOutEvent(noteId, {
|
||||
events: [
|
||||
{
|
||||
eventCode: "SHW",
|
||||
occured: new Date(),
|
||||
eventOwnerId: this.options.event_owner,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async registerGoodsOutTrackingNumber(noteId, shipment) {
|
||||
const client = await this.getClient()
|
||||
return client.warehouses.updateGoodsOutNote(noteId, {
|
||||
priority: false,
|
||||
shipping: {
|
||||
reference: shipment.tracking_numbers.join(", "),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async createRefundCredit(fromOrder, fromRefund) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const client = await this.getClient()
|
||||
const authData = await this.getAuthData()
|
||||
const orderId = fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (orderId) {
|
||||
let accountingCode = "4000"
|
||||
if (
|
||||
fromRefund.reason === "discount" &&
|
||||
this.options.discount_account_code
|
||||
) {
|
||||
accountingCode = this.options.discount_account_code
|
||||
}
|
||||
|
||||
const parentSo = await client.orders.retrieve(orderId)
|
||||
const order = {
|
||||
currency: parentSo.currency,
|
||||
ref: parentSo.ref,
|
||||
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: parentSo.customer,
|
||||
delivery: parentSo.delivery,
|
||||
parentId: orderId,
|
||||
rows: [
|
||||
{
|
||||
name: `${fromRefund.reason}: ${fromRefund.note}`,
|
||||
quantity: 1,
|
||||
taxCode: region.tax_code,
|
||||
net: fromRefund.amount / (1 + fromOrder.tax_rate),
|
||||
tax:
|
||||
fromRefund.amount - fromRefund.amount / (1 + fromOrder.tax_rate),
|
||||
nominalCode: accountingCode,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
return client.orders
|
||||
.createCredit(order)
|
||||
.then(async (creditId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "PAYMENT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`,
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: creditId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: fromRefund.amount,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
const existing = fromOrder.metadata.brightpearl_credit_ids || []
|
||||
const newIds = [...existing, creditId]
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return this.orderService_.setMetadata(
|
||||
fromOrder._id,
|
||||
"brightpearl_credit_ids",
|
||||
newIds
|
||||
)
|
||||
})
|
||||
.catch((err) => console.log(err.response.data.errors))
|
||||
}
|
||||
}
|
||||
|
||||
async createSalesCredit(fromOrder, fromReturn) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const client = await this.getClient()
|
||||
const authData = await this.getAuthData()
|
||||
const orderId = fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (orderId) {
|
||||
const parentSo = await client.orders.retrieve(orderId)
|
||||
const order = {
|
||||
currency: parentSo.currency,
|
||||
ref: parentSo.ref,
|
||||
externalRef: `${parentSo.externalRef}.${fromOrder.refunds.length}`,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: parentSo.customer,
|
||||
delivery: parentSo.delivery,
|
||||
parentId: orderId,
|
||||
rows: fromReturn.items.map((i) => {
|
||||
const parentRow = parentSo.rows.find((row) => {
|
||||
return row.externalRef === i.item_id
|
||||
})
|
||||
return {
|
||||
net: (parentRow.net / parentRow.quantity) * i.quantity,
|
||||
tax: (parentRow.tax / parentRow.quantity) * i.quantity,
|
||||
productId: parentRow.productId,
|
||||
taxCode: parentRow.taxCode,
|
||||
externalRef: parentRow.externalRef,
|
||||
nominalCode: parentRow.nominalCode,
|
||||
quantity: i.quantity,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
const total = order.rows.reduce((acc, next) => {
|
||||
return acc + next.net + next.tax
|
||||
}, 0)
|
||||
|
||||
const difference = fromReturn.refund_amount - total
|
||||
if (difference) {
|
||||
order.rows.push({
|
||||
name: "Difference",
|
||||
quantity: 1,
|
||||
taxCode: region.tax_code,
|
||||
net: difference / (1 + fromOrder.tax_rate),
|
||||
tax: difference - difference / (1 + fromOrder.tax_rate),
|
||||
nominalCode: this.options.sales_account_code || "4000",
|
||||
})
|
||||
}
|
||||
|
||||
return client.orders
|
||||
.createCredit(order)
|
||||
.then(async (creditId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "PAYMENT"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${fromOrder.refunds.length}`,
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: creditId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: fromReturn.refund_amount,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
const existing = fromOrder.metadata.brightpearl_credit_ids || []
|
||||
const newIds = [...existing, creditId]
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return this.orderService_.setMetadata(
|
||||
fromOrder._id,
|
||||
"brightpearl_credit_ids",
|
||||
newIds
|
||||
)
|
||||
})
|
||||
.catch((err) => console.log(err.response.data.errors))
|
||||
}
|
||||
}
|
||||
|
||||
async createSalesOrder(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
let customer = await this.retrieveCustomerByEmail(fromOrder.email)
|
||||
|
||||
// All sales orders must have a customer
|
||||
if (!customer) {
|
||||
customer = await this.createCustomer(fromOrder)
|
||||
}
|
||||
|
||||
const authData = await this.getAuthData()
|
||||
|
||||
const { shipping_address } = fromOrder
|
||||
const order = {
|
||||
currency: {
|
||||
code: fromOrder.currency_code,
|
||||
},
|
||||
ref: fromOrder.display_id,
|
||||
externalRef: fromOrder._id,
|
||||
channelId: this.options.channel_id || `1`,
|
||||
installedIntegrationInstanceId: authData.installation_instance_id,
|
||||
customer: {
|
||||
id: customer.contactId,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
},
|
||||
},
|
||||
delivery: {
|
||||
shippingMethodId: 0,
|
||||
address: {
|
||||
addressFullName: `${shipping_address.first_name} ${shipping_address.last_name}`,
|
||||
addressLine1: shipping_address.address_1,
|
||||
addressLine2: shipping_address.address_2,
|
||||
postalCode: shipping_address.postal_code,
|
||||
countryIsoCode: shipping_address.country_code,
|
||||
telephone: shipping_address.phone,
|
||||
email: fromOrder.email,
|
||||
},
|
||||
},
|
||||
rows: await this.getBrightpearlRows(fromOrder),
|
||||
}
|
||||
|
||||
return client.orders
|
||||
.create(order)
|
||||
.then(async (salesOrderId) => {
|
||||
const order = await client.orders.retrieve(salesOrderId)
|
||||
const resResult = await client.warehouses.createReservation(
|
||||
order,
|
||||
this.options.warehouse
|
||||
)
|
||||
return salesOrderId
|
||||
})
|
||||
.then(async (salesOrderId) => {
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const paymentType = "AUTH"
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: this.options.payment_method_code || "1220",
|
||||
orderId: salesOrderId,
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
paymentDate: new Date(),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
// Only if authorization type
|
||||
if (paymentType === "AUTH") {
|
||||
const today = new Date()
|
||||
const authExpire = today.setDate(today.getDate() + 7)
|
||||
payment.amountAuthorized = await this.totalsService_.getTotal(
|
||||
fromOrder
|
||||
)
|
||||
payment.authorizationExpiry = new Date(authExpire)
|
||||
} else {
|
||||
// For captured
|
||||
}
|
||||
|
||||
await client.payments.create(payment)
|
||||
|
||||
return salesOrderId
|
||||
})
|
||||
.then((salesOrderId) => {
|
||||
return this.orderService_.setMetadata(
|
||||
fromOrder._id,
|
||||
"brightpearl_sales_order_id",
|
||||
salesOrderId
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async createCapturedPayment(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
const soId =
|
||||
fromOrder.metadata && fromOrder.metadata.brightpearl_sales_order_id
|
||||
if (!soId) {
|
||||
return
|
||||
}
|
||||
|
||||
const paymentType = "CAPTURE"
|
||||
const paymentMethod = fromOrder.payment_method
|
||||
const payment = {
|
||||
transactionRef: `${paymentMethod._id}.${paymentType}`, // Brightpearl cannot accept an auth and capture with same ref
|
||||
transactionCode: fromOrder._id,
|
||||
paymentMethodCode: "1220",
|
||||
orderId: soId,
|
||||
paymentDate: new Date(),
|
||||
currencyIsoCode: fromOrder.currency_code,
|
||||
amountPaid: await this.totalsService_.getTotal(fromOrder),
|
||||
paymentType,
|
||||
}
|
||||
|
||||
await client.payments.create(payment)
|
||||
}
|
||||
|
||||
async getBrightpearlRows(fromOrder) {
|
||||
const region = await this.regionService_.retrieve(fromOrder.region_id)
|
||||
const discount = fromOrder.discounts.find(
|
||||
({ discount_rule }) => discount_rule.type !== "free_shipping"
|
||||
)
|
||||
let lineDiscounts = []
|
||||
if (discount) {
|
||||
lineDiscounts = this.discountService_.getLineDiscounts(
|
||||
fromOrder,
|
||||
discount
|
||||
)
|
||||
}
|
||||
|
||||
const lines = await Promise.all(
|
||||
fromOrder.items.map(async (item) => {
|
||||
const bpProduct = await this.retrieveProductBySKU(
|
||||
item.content.variant.sku
|
||||
)
|
||||
|
||||
const discount = lineDiscounts.find((l) =>
|
||||
l.item._id.equals(item._id)
|
||||
) || { amount: 0 }
|
||||
|
||||
const row = {}
|
||||
if (bpProduct) {
|
||||
row.productId = bpProduct.productId
|
||||
} else {
|
||||
row.name = item.title
|
||||
}
|
||||
row.net = item.content.unit_price * item.quantity - discount.amount
|
||||
row.tax = row.net * fromOrder.tax_rate
|
||||
row.quantity = item.quantity
|
||||
row.taxCode = region.tax_code
|
||||
row.externalRef = item._id
|
||||
row.nominalCode = this.options.sales_account_code || "4000"
|
||||
|
||||
return row
|
||||
})
|
||||
)
|
||||
|
||||
const shippingTotal = this.totalsService_.getShippingTotal(fromOrder)
|
||||
const shippingMethods = fromOrder.shipping_methods
|
||||
if (shippingMethods.length > 0) {
|
||||
lines.push({
|
||||
name: `Shipping: ${shippingMethods.map((m) => m.name).join(" + ")}`,
|
||||
quantity: 1,
|
||||
net: shippingTotal,
|
||||
tax: shippingTotal * fromOrder.tax_rate,
|
||||
taxCode: region.tax_code,
|
||||
nominalCode: this.options.shipping_account_code || "4040",
|
||||
})
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
async retrieveCustomerByEmail(email) {
|
||||
const client = await this.getClient()
|
||||
return client.customers.retrieveByEmail(email).then((customers) => {
|
||||
if (!customers.length) {
|
||||
return null
|
||||
}
|
||||
return customers.find((c) => c.primaryEmail === email)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieveProductBySKU(sku) {
|
||||
const client = await this.getClient()
|
||||
return client.products.retrieveBySKU(sku).then((products) => {
|
||||
if (!products.length) {
|
||||
return null
|
||||
}
|
||||
return products[0]
|
||||
})
|
||||
}
|
||||
|
||||
async createFulfillmentFromGoodsOut(id) {
|
||||
const client = await this.getClient()
|
||||
|
||||
// Get goods out and associated order
|
||||
const goodsOut = await client.warehouses.retrieveGoodsOutNote(id)
|
||||
const order = await client.orders.retrieve(goodsOut.orderId)
|
||||
|
||||
console.log(order)
|
||||
|
||||
// Combine the line items that we are going to create a fulfillment for
|
||||
const lines = Object.keys(goodsOut.orderRows)
|
||||
.map((key) => {
|
||||
const row = order.rows.find((r) => r.id == key)
|
||||
if (row) {
|
||||
return {
|
||||
item_id: row.externalRef,
|
||||
quantity: goodsOut.orderRows[key][0].quantity,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
.filter((i) => !!i)
|
||||
|
||||
return this.orderService_.createFulfillment(order.ref, lines, {
|
||||
goods_out_note: id,
|
||||
})
|
||||
}
|
||||
|
||||
async createCustomer(fromOrder) {
|
||||
const client = await this.getClient()
|
||||
const address = await client.addresses.create({
|
||||
addressLine1: fromOrder.shipping_address.address_1,
|
||||
addressLine2: fromOrder.shipping_address.address_2,
|
||||
postalCode: fromOrder.shipping_address.postal_code,
|
||||
countryIsoCode: fromOrder.shipping_address.country_code,
|
||||
})
|
||||
|
||||
const customer = await client.customers.create({
|
||||
firstName: fromOrder.shipping_address.first_name,
|
||||
lastName: fromOrder.shipping_address.last_name,
|
||||
postAddressIds: {
|
||||
DEF: address,
|
||||
BIL: address,
|
||||
DEL: address,
|
||||
},
|
||||
})
|
||||
|
||||
return { contactId: customer }
|
||||
}
|
||||
}
|
||||
|
||||
export default BrightpearlService
|
||||
40
packages/medusa-plugin-brightpearl/src/services/oauth.js
Normal file
40
packages/medusa-plugin-brightpearl/src/services/oauth.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import randomize from "randomatic"
|
||||
import { OauthService } from "medusa-interfaces"
|
||||
import Brightpearl from "../utils/brightpearl"
|
||||
|
||||
const CLIENT_SECRET = process.env.BP_CLIENT_SECRET || ""
|
||||
|
||||
class BrightpearlOauth extends OauthService {
|
||||
constructor({}, options) {
|
||||
super()
|
||||
|
||||
this.account_ = options.account
|
||||
}
|
||||
|
||||
static getAppDetails(options) {
|
||||
const client_id = "medusa-dev"
|
||||
const client_secret = CLIENT_SECRET
|
||||
const state = randomize("A0", 16)
|
||||
const redirect = "https://localhost:8000/a/oauth/brightpearl"
|
||||
return {
|
||||
application_name: "brightpearl",
|
||||
display_name: "Brightpearl",
|
||||
install_url: `https://oauth.brightpearl.com/authorize/${options.account}?response_type=code&client_id=${client_id}&redirect_uri=${redirect}&state=${state}`,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
async generateToken(code) {
|
||||
const params = {
|
||||
client_id: "medusa-dev",
|
||||
client_secret: CLIENT_SECRET,
|
||||
redirect: "https://localhost:8000/a/oauth/brightpearl",
|
||||
code,
|
||||
}
|
||||
|
||||
const data = await Brightpearl.createToken(this.account_, params)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
export default BrightpearlOauth
|
||||
55
packages/medusa-plugin-brightpearl/src/subscribers/order.js
Normal file
55
packages/medusa-plugin-brightpearl/src/subscribers/order.js
Normal file
@@ -0,0 +1,55 @@
|
||||
class OrderSubscriber {
|
||||
constructor({ eventBusService, orderService, brightpearlService }) {
|
||||
this.orderService_ = orderService
|
||||
this.brightpearlService_ = brightpearlService
|
||||
|
||||
eventBusService.subscribe("order.refund_created", this.registerRefund)
|
||||
|
||||
eventBusService.subscribe("order.items_returned", this.registerReturn)
|
||||
|
||||
eventBusService.subscribe("order.placed", this.sendToBrightpearl)
|
||||
|
||||
eventBusService.subscribe(
|
||||
"order.payment_captured",
|
||||
this.registerCapturedPayment
|
||||
)
|
||||
|
||||
eventBusService.subscribe("order.shipment_created", this.registerShipment)
|
||||
}
|
||||
|
||||
sendToBrightpearl = (order) => {
|
||||
return this.brightpearlService_.createSalesOrder(order)
|
||||
}
|
||||
|
||||
registerCapturedPayment = (order) => {
|
||||
return this.brightpearlService_.createCapturedPayment(order)
|
||||
}
|
||||
|
||||
registerShipment = async (data) => {
|
||||
const { order_id, shipment } = data
|
||||
const noteId = shipment.metadata.goods_out_note
|
||||
if (noteId) {
|
||||
await this.brightpearlService_.registerGoodsOutTrackingNumber(
|
||||
noteId,
|
||||
shipment
|
||||
)
|
||||
await this.brightpearlService_.registerGoodsOutShipped(noteId, shipment)
|
||||
}
|
||||
}
|
||||
|
||||
registerReturn = (data) => {
|
||||
const { order, return: fromReturn } = data
|
||||
return this.brightpearlService_
|
||||
.createSalesCredit(order, fromReturn)
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
registerRefund = (data) => {
|
||||
const { order, refund } = data
|
||||
return this.brightpearlService_
|
||||
.createRefundCredit(order, refund)
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderSubscriber
|
||||
250
packages/medusa-plugin-brightpearl/src/utils/brightpearl.js
Normal file
250
packages/medusa-plugin-brightpearl/src/utils/brightpearl.js
Normal file
@@ -0,0 +1,250 @@
|
||||
import axios from "axios"
|
||||
import qs from "querystring"
|
||||
|
||||
class BrightpearlClient {
|
||||
static createToken(account, data) {
|
||||
const params = {
|
||||
grant_type: "authorization_code",
|
||||
code: data.code,
|
||||
client_id: data.client_id,
|
||||
client_secret: data.client_secret,
|
||||
redirect_uri: data.redirect,
|
||||
}
|
||||
|
||||
return axios({
|
||||
url: `https://ws-eu1.brightpearl.com/${account}/oauth/token`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
data: qs.stringify(params),
|
||||
}).then(({ data }) => data)
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
this.client_ = axios.create({
|
||||
baseURL: `https://${options.url}/public-api/${options.account}`,
|
||||
headers: {
|
||||
"brightpearl-app-ref": "medusa-dev",
|
||||
"brightpearl-dev-ref": "sebrindom",
|
||||
Authorization: `${options.auth_type} ${options.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
this.webhooks = this.buildWebhookEndpoints()
|
||||
this.payments = this.buildPaymentEndpoints()
|
||||
this.warehouses = this.buildWarehouseEndpoints()
|
||||
this.orders = this.buildOrderEndpoints()
|
||||
this.addresses = this.buildAddressEndpoints()
|
||||
this.customers = this.buildCustomerEndpoints()
|
||||
this.products = this.buildProductEndpoints()
|
||||
}
|
||||
|
||||
buildSearchResults_(response) {
|
||||
const { results, metaData } = response
|
||||
// Map the column names to the columns
|
||||
return results.map((resColumns) => {
|
||||
const object = {}
|
||||
for (let i = 0; i < resColumns.length; i++) {
|
||||
const fieldName = metaData.columns[i].name
|
||||
object[fieldName] = resColumns[i]
|
||||
}
|
||||
return object
|
||||
})
|
||||
}
|
||||
|
||||
buildWebhookEndpoints = () => {
|
||||
return {
|
||||
list: () => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/integration-service/webhook`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
create: (data) => {
|
||||
return this.client_.request({
|
||||
url: `/integration-service/webhook`,
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildPaymentEndpoints = () => {
|
||||
return {
|
||||
create: (payment) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/accounting-service/customer-payment`,
|
||||
method: "POST",
|
||||
data: payment,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildWarehouseEndpoints = () => {
|
||||
return {
|
||||
retrieveReservation: (orderId) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/${orderId}/reservation`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
retrieveGoodsOutNote: (id) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/*/goods-note/goods-out/${id}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response && data.response[id])
|
||||
},
|
||||
createGoodsOutNote: (orderId, data) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/${orderId}/goods-note/goods-out`,
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
updateGoodsOutNote: (noteId, update) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/goods-note/goods-out/${noteId}`,
|
||||
method: "PUT",
|
||||
data: update,
|
||||
})
|
||||
},
|
||||
registerGoodsOutEvent: (noteId, data) => {
|
||||
return this.client_.request({
|
||||
url: `/warehouse-service/goods-note/goods-out/${noteId}/event`,
|
||||
method: "POST",
|
||||
data,
|
||||
})
|
||||
},
|
||||
createReservation: (order, warehouse) => {
|
||||
const id = order.id
|
||||
const data = order.rows.map((r) => ({
|
||||
productId: r.productId,
|
||||
salesOrderRowId: r.id,
|
||||
quantity: r.quantity,
|
||||
}))
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/order/${id}/reservation/warehouse/${warehouse}`,
|
||||
method: "POST",
|
||||
data: {
|
||||
products: data,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildOrderEndpoints = () => {
|
||||
return {
|
||||
retrieve: (orderId) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/order-service/sales-order/${orderId}`,
|
||||
method: "GET",
|
||||
})
|
||||
.then(({ data }) => data.response.length && data.response[0])
|
||||
.catch((err) => console.log(err))
|
||||
},
|
||||
create: (order) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/order-service/sales-order`,
|
||||
method: "POST",
|
||||
data: order,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
createCredit: (salesCredit) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/order-service/sales-credit`,
|
||||
method: "POST",
|
||||
data: salesCredit,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildAddressEndpoints = () => {
|
||||
return {
|
||||
create: (address) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/contact-service/postal-address`,
|
||||
method: "POST",
|
||||
data: address,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildProductEndpoints = () => {
|
||||
return {
|
||||
retrieveAvailability: (productId) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/warehouse-service/product-availability/${productId}`,
|
||||
})
|
||||
.then(({ data }) => data.response && data.response)
|
||||
},
|
||||
retrieve: (productId) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/product-service/product/${productId}`,
|
||||
})
|
||||
.then(({ data }) => data.response && data.response[0])
|
||||
},
|
||||
retrieveBySKU: (sku) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/product-service/product-search?SKU=${sku}`,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return this.buildSearchResults_(data.response)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
buildCustomerEndpoints = () => {
|
||||
return {
|
||||
retrieveByEmail: (email) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/contact-service/contact-search?primaryEmail=${email}`,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
return this.buildSearchResults_(data.response)
|
||||
})
|
||||
},
|
||||
|
||||
create: (customerData) => {
|
||||
return this.client_
|
||||
.request({
|
||||
url: `/contact-service/contact`,
|
||||
method: "POST",
|
||||
data: customerData,
|
||||
})
|
||||
.then(({ data }) => data.response)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BrightpearlClient
|
||||
300
packages/medusa-plugin-brightpearl/utils/brightpearl.js
Normal file
300
packages/medusa-plugin-brightpearl/utils/brightpearl.js
Normal file
@@ -0,0 +1,300 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports["default"] = void 0;
|
||||
|
||||
var _axios = _interopRequireDefault(require("axios"));
|
||||
|
||||
var _querystring = _interopRequireDefault(require("querystring"));
|
||||
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
|
||||
|
||||
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
|
||||
|
||||
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
||||
|
||||
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
|
||||
|
||||
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
|
||||
|
||||
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 BrightpearlClient = /*#__PURE__*/function () {
|
||||
_createClass(BrightpearlClient, null, [{
|
||||
key: "createToken",
|
||||
value: function createToken(account, data) {
|
||||
var params = {
|
||||
grant_type: "authorization_code",
|
||||
code: data.code,
|
||||
client_id: data.client_id,
|
||||
client_secret: data.client_secret,
|
||||
redirect_uri: data.redirect
|
||||
};
|
||||
return (0, _axios["default"])({
|
||||
url: "https://ws-eu1.brightpearl.com/".concat(account, "/oauth/token"),
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: _querystring["default"].stringify(params)
|
||||
}).then(function (_ref) {
|
||||
var data = _ref.data;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
function BrightpearlClient(options) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, BrightpearlClient);
|
||||
|
||||
_defineProperty(this, "buildWebhookEndpoints", function () {
|
||||
return {
|
||||
list: function list() {
|
||||
return _this.client_.request({
|
||||
url: "/integration-service/webhook",
|
||||
method: "GET"
|
||||
}).then(function (_ref2) {
|
||||
var data = _ref2.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
create: function create(data) {
|
||||
return _this.client_.request({
|
||||
url: "/integration-service/webhook",
|
||||
method: "POST",
|
||||
data: data
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildPaymentEndpoints", function () {
|
||||
return {
|
||||
create: function create(payment) {
|
||||
return _this.client_.request({
|
||||
url: "/accounting-service/customer-payment",
|
||||
method: "POST",
|
||||
data: payment
|
||||
}).then(function (_ref3) {
|
||||
var data = _ref3.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildWarehouseEndpoints", function () {
|
||||
return {
|
||||
retrieveReservation: function retrieveReservation(orderId) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(orderId, "/reservation"),
|
||||
method: "GET"
|
||||
}).then(function (_ref4) {
|
||||
var data = _ref4.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
retrieveGoodsOutNote: function retrieveGoodsOutNote(id) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/*/goods-note/goods-out/".concat(id),
|
||||
method: "GET"
|
||||
}).then(function (_ref5) {
|
||||
var data = _ref5.data;
|
||||
return data.response && data.response[id];
|
||||
});
|
||||
},
|
||||
createGoodsOutNote: function createGoodsOutNote(orderId, data) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(orderId, "/goods-note/goods-out"),
|
||||
method: "POST",
|
||||
data: data
|
||||
}).then(function (_ref6) {
|
||||
var data = _ref6.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
updateGoodsOutNote: function updateGoodsOutNote(noteId, update) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/goods-note/goods-out/".concat(noteId),
|
||||
method: "PUT",
|
||||
data: update
|
||||
});
|
||||
},
|
||||
registerGoodsOutEvent: function registerGoodsOutEvent(noteId, data) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/goods-note/goods-out/".concat(noteId, "/event"),
|
||||
method: "POST",
|
||||
data: data
|
||||
});
|
||||
},
|
||||
createReservation: function createReservation(order, warehouse) {
|
||||
var id = order.id;
|
||||
var data = order.rows.map(function (r) {
|
||||
return {
|
||||
productId: r.productId,
|
||||
salesOrderRowId: r.id,
|
||||
quantity: r.quantity
|
||||
};
|
||||
});
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/order/".concat(id, "/reservation/warehouse/").concat(warehouse),
|
||||
method: "POST",
|
||||
data: {
|
||||
products: data
|
||||
}
|
||||
}).then(function (_ref7) {
|
||||
var data = _ref7.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildOrderEndpoints", function () {
|
||||
return {
|
||||
retrieve: function retrieve(orderId) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-order/".concat(orderId),
|
||||
method: "GET"
|
||||
}).then(function (_ref8) {
|
||||
var data = _ref8.data;
|
||||
return data.response.length && data.response[0];
|
||||
})["catch"](function (err) {
|
||||
return console.log(err);
|
||||
});
|
||||
},
|
||||
create: function create(order) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-order",
|
||||
method: "POST",
|
||||
data: order
|
||||
}).then(function (_ref9) {
|
||||
var data = _ref9.data;
|
||||
return data.response;
|
||||
});
|
||||
},
|
||||
createCredit: function createCredit(salesCredit) {
|
||||
return _this.client_.request({
|
||||
url: "/order-service/sales-credit",
|
||||
method: "POST",
|
||||
data: salesCredit
|
||||
}).then(function (_ref10) {
|
||||
var data = _ref10.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildAddressEndpoints", function () {
|
||||
return {
|
||||
create: function create(address) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/postal-address",
|
||||
method: "POST",
|
||||
data: address
|
||||
}).then(function (_ref11) {
|
||||
var data = _ref11.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildProductEndpoints", function () {
|
||||
return {
|
||||
retrieveAvailability: function retrieveAvailability(productId) {
|
||||
return _this.client_.request({
|
||||
url: "/warehouse-service/product-availability/".concat(productId)
|
||||
}).then(function (_ref12) {
|
||||
var data = _ref12.data;
|
||||
return data.response && data.response;
|
||||
});
|
||||
},
|
||||
retrieve: function retrieve(productId) {
|
||||
return _this.client_.request({
|
||||
url: "/product-service/product/".concat(productId)
|
||||
}).then(function (_ref13) {
|
||||
var data = _ref13.data;
|
||||
return data.response && data.response[0];
|
||||
});
|
||||
},
|
||||
retrieveBySKU: function retrieveBySKU(sku) {
|
||||
return _this.client_.request({
|
||||
url: "/product-service/product-search?SKU=".concat(sku)
|
||||
}).then(function (_ref14) {
|
||||
var data = _ref14.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
_defineProperty(this, "buildCustomerEndpoints", function () {
|
||||
return {
|
||||
retrieveByEmail: function retrieveByEmail(email) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/contact-search?primaryEmail=".concat(email)
|
||||
}).then(function (_ref15) {
|
||||
var data = _ref15.data;
|
||||
return _this.buildSearchResults_(data.response);
|
||||
});
|
||||
},
|
||||
create: function create(customerData) {
|
||||
return _this.client_.request({
|
||||
url: "/contact-service/contact",
|
||||
method: "POST",
|
||||
data: customerData
|
||||
}).then(function (_ref16) {
|
||||
var data = _ref16.data;
|
||||
return data.response;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.client_ = _axios["default"].create({
|
||||
baseURL: "https://".concat(options.url, "/public-api/").concat(options.account),
|
||||
headers: {
|
||||
"brightpearl-app-ref": "medusa-dev",
|
||||
"brightpearl-dev-ref": "sebrindom",
|
||||
Authorization: "".concat(options.auth_type, " ").concat(options.access_token)
|
||||
}
|
||||
});
|
||||
this.webhooks = this.buildWebhookEndpoints();
|
||||
this.payments = this.buildPaymentEndpoints();
|
||||
this.warehouses = this.buildWarehouseEndpoints();
|
||||
this.orders = this.buildOrderEndpoints();
|
||||
this.addresses = this.buildAddressEndpoints();
|
||||
this.customers = this.buildCustomerEndpoints();
|
||||
this.products = this.buildProductEndpoints();
|
||||
}
|
||||
|
||||
_createClass(BrightpearlClient, [{
|
||||
key: "buildSearchResults_",
|
||||
value: function buildSearchResults_(response) {
|
||||
var results = response.results,
|
||||
metaData = response.metaData; // Map the column names to the columns
|
||||
|
||||
return results.map(function (resColumns) {
|
||||
var object = {};
|
||||
|
||||
for (var i = 0; i < resColumns.length; i++) {
|
||||
var fieldName = metaData.columns[i].name;
|
||||
object[fieldName] = resColumns[i];
|
||||
}
|
||||
|
||||
return object;
|
||||
});
|
||||
}
|
||||
}]);
|
||||
|
||||
return BrightpearlClient;
|
||||
}();
|
||||
|
||||
var _default = BrightpearlClient;
|
||||
exports["default"] = _default;
|
||||
@@ -2,16 +2,17 @@ export default async (req, res) => {
|
||||
try {
|
||||
const contentfulService = req.scope.resolve("contentfulService")
|
||||
|
||||
const contentfulType = req.body.contentType.sys.id
|
||||
const contentfulType = req.body.sys.contentType.sys.id
|
||||
const entryId = req.body.sys.id
|
||||
|
||||
let updated = {}
|
||||
switch (contentfulType) {
|
||||
case "product":
|
||||
updated = await contentfulService.sendContentfulProductToAdmin(req.body)
|
||||
updated = await contentfulService.sendContentfulProductToAdmin(entryId)
|
||||
break
|
||||
case "productVariant":
|
||||
updated = await contentfulService.sendContentfulProductVariantToAdmin(
|
||||
req.body
|
||||
entryId
|
||||
)
|
||||
break
|
||||
default:
|
||||
@@ -32,10 +32,6 @@ class ContentfulService extends BaseService {
|
||||
return reject(err)
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
return reject("Missing key")
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(reply))
|
||||
})
|
||||
})
|
||||
@@ -51,24 +47,26 @@ class ContentfulService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async getVariantEntries_(variantEntryIds) {
|
||||
if (!variantEntryIds) {
|
||||
return []
|
||||
}
|
||||
|
||||
async getVariantEntries_(productId) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
return Promise.all(variantEntryIds.map((v) => environment.getEntry(v)))
|
||||
const productVariants = await this.productService_.retrieveVariants(
|
||||
productId
|
||||
)
|
||||
|
||||
const contentfulVariants = await Promise.all(
|
||||
productVariants.map((variant) =>
|
||||
this.updateProductVariantInContentful(variant)
|
||||
)
|
||||
)
|
||||
|
||||
return contentfulVariants
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getVariantLinks_(variantEntries) {
|
||||
if (!variantEntries) {
|
||||
return []
|
||||
}
|
||||
|
||||
getVariantLinks_(variantEntries) {
|
||||
return variantEntries.map((v) => ({
|
||||
sys: {
|
||||
type: "Link",
|
||||
@@ -81,20 +79,30 @@ class ContentfulService extends BaseService {
|
||||
async createProductInContentful(product) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const variantEntries = await this.getVariantEntries_(product.variants)
|
||||
return environment.createEntryWithId("product", product._id, {
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": product.title,
|
||||
const variantEntries = await this.getVariantEntries_(product._id)
|
||||
const variantLinks = this.getVariantLinks_(variantEntries)
|
||||
const result = await environment.createEntryWithId(
|
||||
"product",
|
||||
product._id,
|
||||
{
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": product.title,
|
||||
},
|
||||
variants: {
|
||||
"en-US": variantLinks,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": product._id,
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
"en-US": this.getVariantLinks_(variantEntries),
|
||||
},
|
||||
objectId: {
|
||||
"en-US": product._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
||||
ignoreIds.push(product._id)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
||||
return result
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -103,22 +111,31 @@ class ContentfulService extends BaseService {
|
||||
async createProductVariantInContentful(variant) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
return environment.createEntryWithId("productVariant", variant._id, {
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": variant.title,
|
||||
const result = await environment.createEntryWithId(
|
||||
"productVariant",
|
||||
variant._id,
|
||||
{
|
||||
fields: {
|
||||
title: {
|
||||
"en-US": variant.title,
|
||||
},
|
||||
sku: {
|
||||
"en-US": variant.sku,
|
||||
},
|
||||
prices: {
|
||||
"en-US": variant.prices,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": variant._id,
|
||||
},
|
||||
},
|
||||
sku: {
|
||||
"en-US": variant.sku,
|
||||
},
|
||||
prices: {
|
||||
"en-US": variant.prices,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": variant._id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
||||
ignoreIds.push(variant._id)
|
||||
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
||||
return result
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
@@ -126,34 +143,46 @@ class ContentfulService extends BaseService {
|
||||
|
||||
async updateProductInContentful(product) {
|
||||
try {
|
||||
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
||||
|
||||
if (ignoreIds.includes(product._id)) {
|
||||
const newIgnoreIds = ignoreIds.filter((id) => id !== product._id)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds))
|
||||
return
|
||||
} else {
|
||||
ignoreIds.push(product._id)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
||||
}
|
||||
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
// check if product exists
|
||||
let productEntry = undefined
|
||||
try {
|
||||
productEntry = await environment.getEntry(product._id)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return this.createProductInContentful(product)
|
||||
}
|
||||
|
||||
const variantEntries = await this.getVariantEntries_(product.variants)
|
||||
const variantEntries = await this.getVariantEntries_(product._id)
|
||||
const variantLinks = this.getVariantLinks_(variantEntries)
|
||||
productEntry.fields = _.assignIn(productEntry.fields, {
|
||||
title: {
|
||||
"en-US": product.title,
|
||||
},
|
||||
options: {
|
||||
"en-US": product.options,
|
||||
},
|
||||
variants: {
|
||||
"en-US": this.getVariantLinks_(variantEntries),
|
||||
"en-US": variantLinks,
|
||||
},
|
||||
objectId: {
|
||||
"en-US": product._id,
|
||||
},
|
||||
})
|
||||
|
||||
await productEntry.update()
|
||||
const publishedEntry = await productEntry.publish()
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product")
|
||||
if (ignoreIds.includes(publishedEntry.sys.id)) {
|
||||
ignoreIds.filter((id) => id !== publishedEntry.sys.id)
|
||||
} else {
|
||||
this.eventBus_.emit("product.updated", publishedEntry)
|
||||
}
|
||||
const updatedEntry = await productEntry.update()
|
||||
const publishedEntry = await updatedEntry.publish()
|
||||
|
||||
return publishedEntry
|
||||
} catch (error) {
|
||||
@@ -163,12 +192,27 @@ class ContentfulService extends BaseService {
|
||||
|
||||
async updateProductVariantInContentful(variant) {
|
||||
try {
|
||||
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
||||
|
||||
if (ignoreIds.includes(variant._id)) {
|
||||
const newIgnoreIds = ignoreIds.filter((id) => id !== variant._id)
|
||||
this.redis_.set(
|
||||
"product_variant_ignore_ids",
|
||||
JSON.stringify(newIgnoreIds)
|
||||
)
|
||||
return
|
||||
} else {
|
||||
ignoreIds.push(variant._id)
|
||||
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
||||
}
|
||||
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
// check if product exists
|
||||
let variantEntry = undefined
|
||||
variantEntry = await environment.getEntry(variant._id)
|
||||
// if not, we create a new one
|
||||
if (!variantEntry) {
|
||||
try {
|
||||
variantEntry = await environment.getEntry(variant._id)
|
||||
} catch (error) {
|
||||
return this.createProductVariantInContentful(variant)
|
||||
}
|
||||
|
||||
@@ -179,6 +223,9 @@ class ContentfulService extends BaseService {
|
||||
sku: {
|
||||
"en-US": variant.sku,
|
||||
},
|
||||
options: {
|
||||
"en-US": variant.options,
|
||||
},
|
||||
prices: {
|
||||
"en-US": variant.prices,
|
||||
},
|
||||
@@ -187,15 +234,8 @@ class ContentfulService extends BaseService {
|
||||
},
|
||||
})
|
||||
|
||||
await variantEntry.update()
|
||||
const publishedEntry = await variantEntry.publish()
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product_variant")
|
||||
if (ignoreIds.includes(publishedEntry.sys.id)) {
|
||||
ignoreIds.filter((id) => id !== publishedEntry.sys.id)
|
||||
} else {
|
||||
this.eventBus_.emit("product-variant.updated", publishedEntry)
|
||||
}
|
||||
const updatedEntry = await variantEntry.update()
|
||||
const publishedEntry = await updatedEntry.publish()
|
||||
|
||||
return publishedEntry
|
||||
} catch (error) {
|
||||
@@ -203,22 +243,24 @@ class ContentfulService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentfulProductToAdmin(product) {
|
||||
async sendContentfulProductToAdmin(productId) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const productEntry = await environment.getEntry(product.sys.id)
|
||||
const productEntry = await environment.getEntry(productId)
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product")
|
||||
ignoreIds.push(product.sys.id)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
||||
const ignoreIds = (await this.getIgnoreIds_("product")) || []
|
||||
if (ignoreIds.includes(productId)) {
|
||||
const newIgnoreIds = ignoreIds.filter((id) => id !== productId)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(newIgnoreIds))
|
||||
return
|
||||
} else {
|
||||
ignoreIds.push(productId)
|
||||
this.redis_.set("product_ignore_ids", JSON.stringify(ignoreIds))
|
||||
}
|
||||
|
||||
const updatedProduct = await this.productService_.update(
|
||||
productEntry.objectId,
|
||||
{
|
||||
title: productEntry.fields.title["en-US"],
|
||||
variants: productEntry.fields.variants["en-US"],
|
||||
}
|
||||
)
|
||||
const updatedProduct = await this.productService_.update(productId, {
|
||||
title: productEntry.fields.title["en-US"],
|
||||
})
|
||||
|
||||
return updatedProduct
|
||||
} catch (error) {
|
||||
@@ -226,21 +268,28 @@ class ContentfulService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
async sendContentfulProductVariantToAdmin(variant) {
|
||||
async sendContentfulProductVariantToAdmin(variantId) {
|
||||
try {
|
||||
const environment = await this.getContentfulEnvironment_()
|
||||
const variantEntry = await environment.getEntry(variant.sys.id)
|
||||
const variantEntry = await environment.getEntry(variantId)
|
||||
|
||||
const ignoreIds = await this.getIgnoreIds_("product_variant")
|
||||
ignoreIds.push(variant.sys.id)
|
||||
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
||||
const ignoreIds = (await this.getIgnoreIds_("product_variant")) || []
|
||||
if (ignoreIds.includes(variantId)) {
|
||||
const newIgnoreIds = ignoreIds.filter((id) => id !== variantId)
|
||||
this.redis_.set(
|
||||
"product_variant_ignore_ids",
|
||||
JSON.stringify(newIgnoreIds)
|
||||
)
|
||||
return
|
||||
} else {
|
||||
ignoreIds.push(variantId)
|
||||
this.redis_.set("product_variant_ignore_ids", JSON.stringify(ignoreIds))
|
||||
}
|
||||
|
||||
const updatedVariant = await this.variantService_.update(
|
||||
variantEntry.objectId,
|
||||
const updatedVariant = await this.productVariantService_.update(
|
||||
variantId,
|
||||
{
|
||||
title: variantEntry.fields.title["en-US"],
|
||||
sku: variantEntry.fields.sku["en-US"],
|
||||
prices: variantEntry.fields.prices["en-US"],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class ContentfulSubscriber {
|
||||
})
|
||||
|
||||
this.eventBus_.subscribe("product.created", async (data) => {
|
||||
await this.contentfulService_.createProductVariantInContentful(data)
|
||||
await this.contentfulService_.createProductInContentful(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user