Merge pull request #89 from medusajs/alpha

Alpha release
This commit is contained in:
Oliver Windall Juhl
2020-08-20 13:36:01 +02:00
committed by GitHub
373 changed files with 72739 additions and 1213 deletions

View File

@@ -3,5 +3,8 @@
"private": true,
"devDependencies": {
"lerna": "^3.19.0"
},
"dependencies": {
"axios": "^0.19.2"
}
}

View File

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

View File

@@ -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(

View File

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

View File

@@ -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(),
})
}

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

View 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
View File

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

View File

@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View 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"
}

View 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

View File

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

View File

@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View 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;

View File

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

View File

@@ -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({})
}
}

View File

@@ -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()
}
/**

View 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

View File

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

View File

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

View 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

View File

@@ -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

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

View File

@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

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

View File

@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View 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"
}

View 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
}

View File

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

View File

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

View File

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

View 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
}

View File

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

View File

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

View 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
}

View File

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

View File

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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

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

View File

@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

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

View File

@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View File

@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: "node",
}

View 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"
}

View File

@@ -0,0 +1,5 @@
const mockAxios = jest.genMockFromModule("axios")
mockAxios.create = jest.fn(() => mockAxios)
export default mockAxios

View 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

View 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

View 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

View File

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

View File

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

View File

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

View File

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

View 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
}

View 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
}
}

View File

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

View File

@@ -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

View File

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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -12,4 +12,5 @@ yarn.lock
/services
/models
/subscribers
/__mocks__

View File

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

View File

@@ -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)

View File

@@ -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)

View File

@@ -120,14 +120,10 @@ describe("StripeProviderService", () => {
result = await stripeProviderService.updatePayment(
{
payment_method: {
data: {
id: "pi_lebron",
},
},
id: "pi_lebron",
},
{
amount: 1000,
total: 1000,
}
)
})

View File

@@ -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) {

View File

@@ -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
)

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

View File

@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}

View File

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

View File

@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock

View File

@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

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

View 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;

View 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;

View 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"
}
}

View 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
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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;

View File

@@ -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:

View File

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

View File

@@ -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