Stripe payment provider plugin (#65)
Adds Stripe payment provider plugin Closes #44
This commit is contained in:
committed by
GitHub
parent
420c1d8cdb
commit
3debd7e108
@@ -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": {
|
||||
|
||||
5
packages/medusa-payment-stripe/.gitignore
vendored
5
packages/medusa-payment-stripe/.gitignore
vendored
@@ -6,8 +6,5 @@ node_modules
|
||||
!index.js
|
||||
yarn.lock
|
||||
|
||||
/api
|
||||
/services
|
||||
/models
|
||||
/subscribers
|
||||
/dist
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
212
packages/medusa-payment-stripe/src/__mocks__/cart.js
Normal file
212
packages/medusa-payment-stripe/src/__mocks__/cart.js
Normal 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
|
||||
36
packages/medusa-payment-stripe/src/__mocks__/customer.js
Normal file
36
packages/medusa-payment-stripe/src/__mocks__/customer.js
Normal 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
|
||||
10
packages/medusa-payment-stripe/src/__mocks__/eventbus.js
Normal file
10
packages/medusa-payment-stripe/src/__mocks__/eventbus.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export const EventBusServiceMock = {
|
||||
emit: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return EventBusServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
85
packages/medusa-payment-stripe/src/__mocks__/stripe.js
Normal file
85
packages/medusa-payment-stripe/src/__mocks__/stripe.js
Normal 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
|
||||
9
packages/medusa-payment-stripe/src/__mocks__/totals.js
Normal file
9
packages/medusa-payment-stripe/src/__mocks__/totals.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export const TotalsServiceMock = {
|
||||
getTotal: jest.fn(),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return TotalsServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default (fn) => (...args) => fn(...args).catch(args[2])
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as wrap } from "./await-middleware"
|
||||
|
||||
export default {
|
||||
wrap,
|
||||
}
|
||||
17
packages/medusa-payment-stripe/src/api/routes/hooks/index.js
Normal file
17
packages/medusa-payment-stripe/src/api/routes/hooks/index.js
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user