Stripe payment provider plugin (#65)

Adds Stripe payment provider plugin

Closes #44
This commit is contained in:
Oliver Windall Juhl
2020-05-25 21:08:16 +02:00
committed by GitHub
parent 420c1d8cdb
commit 3debd7e108
18 changed files with 1078 additions and 21 deletions

View File

@@ -1,7 +1,8 @@
{
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-instanceof"
"@babel/plugin-transform-instanceof",
"@babel/plugin-transform-classes"
],
"presets": ["@babel/preset-env"],
"env": {

View File

@@ -6,8 +6,5 @@ node_modules
!index.js
yarn.lock
/api
/services
/models
/subscribers
/dist

View File

@@ -13,11 +13,12 @@
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/node": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/plugin-transform-classes": "^7.9.5",
"@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.5",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.9.6",
"client-sessions": "^0.8.0",
"cross-env": "^5.2.1",
@@ -25,14 +26,19 @@
"jest": "^25.5.2"
},
"scripts": {
"build": "babel src --out-dir . --ignore **/__tests__",
"build": "babel src -d dist",
"prepare": "cross-env NODE_ENV=production npm run build",
"watch": "babel -w src --out-dir . --ignore **/__tests__"
"watch": "babel -w src --out-dir . --ignore **/__tests__",
"test": "jest"
},
"dependencies": {
"@babel/plugin-transform-classes": "^7.9.5",
"express": "^4.17.1",
"medusa-core-utils": "^0.3.0",
"medusa-interfaces": "^0.3.0"
"medusa-interfaces": "^0.3.0",
"medusa-test-utils": "^0.3.0",
"stripe": "^8.50.0",
"body-parser": "^1.19.0"
},
"gitHead": "35e0930650d5f4aedf2610749cd131ae8b7e17cc"
}
}

View File

@@ -0,0 +1,212 @@
import { IdMap } from "medusa-test-utils"
export const carts = {
emptyCart: {
_id: IdMap.getId("emptyCart"),
items: [],
region_id: IdMap.getId("testRegion"),
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
data: {
some_data: "yes",
},
},
],
},
frCart: {
_id: IdMap.getId("fr-cart"),
email: "lebron@james.com",
title: "test",
region_id: IdMap.getId("region-france"),
items: [
{
_id: IdMap.getId("line"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: [
{
unit_price: 8,
variant: {
_id: IdMap.getId("eur-8-us-10"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
{
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
],
quantity: 10,
},
{
_id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 10,
},
],
shipping_methods: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
payment_sessions: [
{
provider_id: "stripe",
data: {
id: "pi_123456789",
customer: IdMap.getId("not-lebron"),
},
},
],
payment_method: {
provider_id: "stripe",
data: {
id: "pi_123456789",
customer: IdMap.getId("not-lebron"),
},
},
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: IdMap.getId("lebron"),
},
frCartNoStripeCustomer: {
_id: IdMap.getId("fr-cart-no-customer"),
title: "test",
region_id: IdMap.getId("region-france"),
items: [
{
_id: IdMap.getId("line"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: [
{
unit_price: 8,
variant: {
_id: IdMap.getId("eur-8-us-10"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
{
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
],
quantity: 10,
},
{
_id: IdMap.getId("existingLine"),
title: "merge line",
description: "This is a new line",
thumbnail: "test-img-yeah.com/thumb",
content: {
unit_price: 10,
variant: {
_id: IdMap.getId("eur-10-us-12"),
},
product: {
_id: IdMap.getId("product"),
},
quantity: 1,
},
quantity: 10,
},
],
shipping_methods: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
shipping_options: [
{
_id: IdMap.getId("freeShipping"),
profile_id: "default_profile",
},
],
payment_sessions: [
{
provider_id: "stripe",
data: {
id: "pi_123456789",
customer: IdMap.getId("not-lebron"),
},
},
],
payment_method: {
provider_id: "stripe",
data: {
id: "pi_123456789",
customer: IdMap.getId("not-lebron"),
},
},
shipping_address: {},
billing_address: {},
discounts: [],
customer_id: IdMap.getId("vvd"),
},
}
export const CartServiceMock = {
retrieve: jest.fn().mockImplementation((cartId) => {
if (cartId === IdMap.getId("fr-cart")) {
return Promise.resolve(carts.frCart)
}
if (cartId === IdMap.getId("emptyCart")) {
return Promise.resolve(carts.emptyCart)
}
return Promise.resolve(undefined)
}),
updatePaymentSession: jest
.fn()
.mockImplementation((cartId, stripe, paymentIntent) => {
return Promise.resolve()
}),
}
const mock = jest.fn().mockImplementation(() => {
return CartServiceMock
})
export default mock

View File

@@ -0,0 +1,36 @@
import { IdMap } from "medusa-test-utils"
export const CustomerServiceMock = {
retrieve: jest.fn().mockImplementation((id) => {
if (id === IdMap.getId("lebron")) {
return Promise.resolve({
_id: IdMap.getId("lebron"),
first_name: "LeBron",
last_name: "James",
email: "lebron@james.com",
password_hash: "1234",
metadata: {
stripe_id: "cus_123456789_new",
},
})
}
if (id === IdMap.getId("vvd")) {
return Promise.resolve({
_id: IdMap.getId("vvd"),
first_name: "Virgil",
last_name: "Van Dijk",
email: "virg@vvd.com",
password_hash: "1234",
metadata: {},
})
}
return Promise.resolve(undefined)
}),
setMetadata: jest.fn().mockReturnValue(Promise.resolve()),
}
const mock = jest.fn().mockImplementation(() => {
return CustomerServiceMock
})
export default mock

View File

@@ -0,0 +1,10 @@
export const EventBusServiceMock = {
emit: jest.fn(),
subscribe: jest.fn(),
}
const mock = jest.fn().mockImplementation(() => {
return EventBusServiceMock
})
export default mock

View File

@@ -0,0 +1,85 @@
export const StripeMock = {
customers: {
create: jest.fn().mockImplementation((data) => {
if (data.email === "virg@vvd.com") {
return Promise.resolve({
id: "cus_vvd",
email: "virg@vvd.com",
})
}
if (data.email === "lebron@james.com") {
return Promise.resolve({
id: "cus_lebron",
email: "lebron@james.com",
})
}
}),
},
paymentIntents: {
create: jest.fn().mockImplementation((data) => {
if (data.customer === "cus_123456789_new") {
return Promise.resolve({
id: "pi_lebron",
amount: 100,
customer: "cus_123456789_new",
})
}
if (data.customer === "cus_lebron") {
return Promise.resolve({
id: "pi_lebron",
amount: 100,
customer: "cus_lebron",
})
}
}),
retrieve: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: "pi_lebron",
customer: "cus_lebron",
})
}),
update: jest.fn().mockImplementation((pi, data) => {
if (data.customer === "cus_lebron_2") {
return Promise.resolve({
id: "pi_lebron",
customer: "cus_lebron_2",
amount: 1000,
})
}
return Promise.resolve({
id: "pi_lebron",
customer: "cus_lebron",
amount: 1000,
})
}),
capture: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: "pi_lebron",
customer: "cus_lebron",
amount: 1000,
status: "succeeded",
})
}),
cancel: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: "pi_lebron",
customer: "cus_lebron",
status: "cancelled",
})
}),
},
refunds: {
create: jest.fn().mockImplementation((data) => {
return Promise.resolve({
id: "re_123",
payment_intent: "pi_lebron",
amount: 1000,
status: "succeeded",
})
}),
},
}
const stripe = jest.fn(() => StripeMock)
export default stripe

View File

@@ -0,0 +1,9 @@
export const TotalsServiceMock = {
getTotal: jest.fn(),
}
const mock = jest.fn().mockImplementation(() => {
return TotalsServiceMock
})
export default mock

View File

@@ -1,14 +1,10 @@
import { Router } from "express"
import hooks from "./routes/hooks"
export default () => {
export default (container) => {
const app = Router()
app.get("/stripe", (req, res) => {
console.log("hi")
res.json({
success: true
})
})
hooks(app)
return app
}

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
import { Router } from "express"
import bodyParser from "body-parser"
import middlewares from "../../middlewares"
const route = Router()
export default (app) => {
app.use("/hooks", route)
route.post(
"/stripe",
// stripe constructEvent fails without body-parser
bodyParser.raw({ type: "application/json" }),
middlewares.wrap(require("./stripe").default)
)
return app
}

View File

@@ -0,0 +1,35 @@
export default async (req, res) => {
const signature = req.headers["stripe-signature"]
let event
try {
const stripeProviderService = req.resolve("pp_stripe")
event = stripeProviderService.constructWebhookEvent(req.body, signature)
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`)
return
}
const paymentIntent = event.data.object
// handle payment intent events
switch (event.type) {
case "payment_intent.succeeded":
break
case "payment_intent.canceled":
break
case "payment_intent.created":
break
case "payment_intent.payment_failed":
break
case "payment_intent.amount_capturable_updated":
break
case "payment_intent.processing":
break
default:
res.status(400)
return
}
res.sendStatus(200)
}

View File

@@ -0,0 +1,52 @@
import { IdMap } from "medusa-test-utils"
export const StripeProviderServiceMock = {
retrievePayment: jest.fn().mockImplementation((cart) => {
if (cart._id === IdMap.getId("fr-cart")) {
return Promise.resolve({
id: "pi",
customer: "cus_123456789",
})
}
if (cart._id === IdMap.getId("fr-cart-no-customer")) {
return Promise.resolve({
id: "pi",
})
}
return Promise.resolve(undefined)
}),
cancelPayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
updatePaymentIntentCustomer: jest.fn().mockImplementation((cart) => {
return Promise.resolve()
}),
retrieveCustomer: jest.fn().mockImplementation((customerId) => {
if (customerId === "cus_123456789_new") {
return Promise.resolve({
id: "cus_123456789_new",
})
}
return Promise.resolve(undefined)
}),
createCustomer: jest.fn().mockImplementation((customer) => {
if (customer._id === IdMap.getId("vvd")) {
return Promise.resolve({
id: "cus_123456789_new_vvd",
})
}
return Promise.resolve(undefined)
}),
createPayment: jest.fn().mockImplementation((cart) => {
return Promise.resolve({
id: "pi_new",
customer: "cus_123456789_new",
})
}),
}
const mock = jest.fn().mockImplementation(() => {
return StripeProviderServiceMock
})
export default mock

View File

@@ -0,0 +1,243 @@
import { IdMap } from "medusa-test-utils"
import StripeProviderService from "../stripe-provider"
import { CustomerServiceMock } from "../../__mocks__/customer"
import { carts } from "../../__mocks__/cart"
import { TotalsServiceMock } from "../../__mocks__/totals"
describe("StripeProviderService", () => {
describe("createCustomer", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
result = await stripeProviderService.createCustomer({
_id: IdMap.getId("vvd"),
first_name: "Virgil",
last_name: "Van Dijk",
email: "virg@vvd.com",
password_hash: "1234",
metadata: {},
})
})
it("returns created stripe customer", () => {
expect(result).toEqual({
id: "cus_vvd",
email: "virg@vvd.com",
})
})
})
describe("createPayment", () => {
let result
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
beforeEach(async () => {
jest.clearAllMocks()
})
it("returns created stripe payment intent for cart with existing customer", async () => {
result = await stripeProviderService.createPayment(carts.frCart)
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_123456789_new",
amount: 100,
})
})
it("returns created stripe payment intent for cart with no customer", async () => {
carts.frCart.customer_id = ""
result = await stripeProviderService.createPayment(carts.frCart)
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
amount: 100,
})
})
})
describe("retrievePayment", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
result = await stripeProviderService.retrievePayment({
payment_method: {
data: {
id: "pi_lebron",
},
},
})
})
it("returns stripe payment intent", () => {
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
})
})
})
describe("updatePayment", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
result = await stripeProviderService.updatePayment(
{
payment_method: {
data: {
id: "pi_lebron",
},
},
},
{
amount: 1000,
}
)
})
it("returns cancelled stripe payment intent", () => {
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
amount: 1000,
})
})
})
describe("updatePaymentIntentCustomer", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{
customerService: CustomerServiceMock,
totalsService: TotalsServiceMock,
},
{
api_key: "test",
}
)
result = await stripeProviderService.updatePaymentIntentCustomer(
"pi_lebron",
"cus_lebron_2"
)
})
it("returns update stripe payment intent", () => {
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron_2",
amount: 1000,
})
})
})
describe("capturePayment", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
{
api_key: "test",
}
)
result = await stripeProviderService.capturePayment("pi_lebron")
})
it("returns captured stripe payment intent", () => {
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
amount: 1000,
status: "succeeded",
})
})
})
describe("refundPayment", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
{
api_key: "test",
}
)
result = await stripeProviderService.refundPayment("pi_lebron", 1000)
})
it("returns refunded stripe payment intent", () => {
expect(result).toEqual({
id: "re_123",
payment_intent: "pi_lebron",
amount: 1000,
status: "succeeded",
})
})
})
describe("cancelPayment", () => {
let result
beforeAll(async () => {
jest.clearAllMocks()
const stripeProviderService = new StripeProviderService(
{},
{
api_key: "test",
}
)
result = await stripeProviderService.cancelPayment("pi_lebron")
})
it("returns cancelled stripe payment intent", () => {
expect(result).toEqual({
id: "pi_lebron",
customer: "cus_lebron",
status: "cancelled",
})
})
})
})

View File

@@ -1,12 +1,214 @@
import _ from "lodash"
import Stripe from "stripe"
import { PaymentService } from "medusa-interfaces"
class StripeProviderService extends PaymentService {
static identifier = "stripe"
constructor(appScope, options) {
constructor({ customerService, totalsService }, options) {
super()
console.log(options)
this.options_ = options
this.stripe_ = Stripe(options.api_key)
this.customerService_ = customerService
this.totalsService_ = totalsService
}
/**
* Status for Stripe PaymentIntent.
* @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"
if (paymentIntent.status === "requires_payment_method") {
return status
}
if (paymentIntent.status === "requires_action") {
status = "authorized"
}
if (paymentIntent.status === "succeeded") {
status = "succeeded"
}
if (paymentIntent.status === "cancelled") {
status = "cancelled"
}
return status
}
async retrieveCustomer(customerId) {
return this.stripe_.customers.retrieve(customerId)
}
// customer metadata
async createCustomer(customer) {
try {
const stripeCustomer = await this.stripe_.customers.create({
email: customer.email,
})
await this.customerService_.setMetadata(
customer._id,
"stripe_id",
stripeCustomer.id
)
return stripeCustomer
} catch (error) {
throw error
}
}
/**
* Creates Stripe PaymentIntent.
* @param {string} cart - the cart to create a payment for
* @param {number} amount - the amount to create a payment for
* @returns {string} id of payment intent
*/
async createPayment(cart) {
const { customer_id } = cart
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.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 = this.totalsService_.getTotal(cart)
const paymentIntent = await this.stripe_.paymentIntents.create({
customer: stripeCustomerId,
amount,
})
return paymentIntent
}
/**
* Retrieves Stripe PaymentIntent.
* @param {string} cart - the cart to retrieve payment intent for
* @returns {Object} Stripe PaymentIntent
*/
async retrievePayment(cart) {
try {
const { data } = cart.payment_method
return this.stripe_.paymentIntents.retrieve(data.id)
} catch (error) {
throw error
}
}
/**
* Updates 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
*/
async updatePayment(cart, update) {
try {
const { data } = cart.payment_method
return this.stripe_.paymentIntents.update(data.id, update)
} catch (error) {
throw error
}
}
/**
* 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
*/
async updatePaymentIntentCustomer(paymentIntent, id) {
try {
return this.stripe_.paymentIntents.update(paymentIntent, {
customer: id,
})
} catch (error) {
throw error
}
}
/**
* Captures payment for Stripe PaymentIntent.
* @param {Object} paymentData - payment method data from cart
* @returns {Object} Stripe PaymentIntent
*/
async capturePayment(paymentData) {
const { id } = paymentData
try {
return this.stripe_.paymentIntents.capture(id)
} catch (error) {
throw error
}
}
/**
* Refunds payment for Stripe PaymentIntent.
* @param {Object} paymentData - payment method data from cart
* @returns {string} id of payment intent
*/
async refundPayment(paymentData, amount) {
const { id } = paymentData
try {
return this.stripe_.refunds.create({
amount,
payment_intent: id,
})
} catch (error) {
throw error
}
}
/**
* Cancels payment for Stripe PaymentIntent.
* @param {Object} paymentData - payment method data from cart
* @returns {string} id of payment intent
*/
async cancelPayment(paymentData) {
const { id } = paymentData
try {
return this.stripe_.paymentIntents.cancel(id)
} catch (error) {
throw error
}
}
/**
* 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
* ensures integrity of the webhook event
* @returns {Object} Stripe Webhook event
*/
constructWebhookEvent(data, signature) {
return this.stripe_.webhooks.constructEvent(
data,
signature,
this.options_.webhook_secret
)
}
}

View File

@@ -0,0 +1,90 @@
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
)
expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledTimes(1)
expect(StripeProviderServiceMock.cancelPayment).toHaveBeenCalledWith("pi")
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
)
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")
})
})
})

View File

@@ -1,11 +1,71 @@
class CartSubscriber {
constructor({ cartService, eventBusService }) {
constructor({
cartService,
customerService,
stripeProviderService,
eventBusService,
}) {
this.cartService_ = cartService
this.customerService_ = customerService
this.stripeProviderService_ = stripeProviderService
this.eventBus_ = eventBusService
this.eventBus_.subscribe("cart.created", (data) => {
console.log(data)
})
this.eventBus_.subscribe("cart.customer_updated", async (cart) => {
await this.onCustomerUpdated(cart)
})
}
async onCustomerUpdated(cart) {
const { customer_id, payment_sessions } = cart
if (!payment_sessions) {
return Promise.resolve()
}
const customer = await this.customerService_.retrieve(customer_id)
const paymentIntent = await this.stripeProviderService_.retrievePayment(
cart
)
let 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.id)
const newPaymentIntent = await this.stripeProviderService_.createPayment(
cart
)
await this.cartService_.updatePaymentSession(
cart._id,
"stripe",
newPaymentIntent
)
}
return Promise.resolve()
}
}