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

+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()
}
}