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:
1 parent
5f819486fc
commit
f1baca3cbd
499 files changed
+25909
-16128
No files matched your search
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-transform-instanceof",
|
||||
"@babel/plugin-transform-classes"
|
||||
|
||||
@@ -12,5 +12,4 @@ yarn.lock
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/__mocks__
|
||||
|
||||
/__mocks__
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user