Replaces MongoDB support with PostgreSQL (#151)

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

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

No files matched your search

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