Setup future usage

This commit is contained in:
Sebastian Rindom
2020-07-13 17:35:50 +02:00
parent eeae2a056c
commit cd989c56a7
20 changed files with 282 additions and 26 deletions

View File

@@ -7,12 +7,13 @@ Joi.address = () => {
first_name: Joi.string().required(),
last_name: Joi.string().required(),
address_1: Joi.string().required(),
address_2: Joi.string(),
address_2: Joi.string().allow(""),
city: Joi.string().required(),
country_code: Joi.string().required(),
province: Joi.string(),
province: Joi.string().allow(""),
postal_code: Joi.string().required(),
metadata: Joi.object()
phone: Joi.string().required(),
metadata: Joi.object(),
})
}

View File

@@ -65,6 +65,14 @@ class BasePaymentService extends BaseService {
deletePayment() {
throw Error("deletePayment must be overridden by the child class")
}
/**
* If the payment provider can save a payment method this function will
* retrieve them.
*/
retrieveSavedMethods(customer) {
return Promise.resolve([])
}
}
export default BasePaymentService

View File

@@ -50,7 +50,22 @@ class StripeProviderService extends PaymentService {
return status
}
async retrieveSavedMethods(customer) {
if (customer.metadata && customer.metadata.stripe_id) {
const methods = await this.stripe_.paymentMethods.list({
customer: customer.metadata.stripe_id, type: "card"
})
return methods.data
}
return Promise.resolve([])
}
async retrieveCustomer(customerId) {
if (!customerId) {
return Promise.resolve()
}
return this.stripe_.customers.retrieve(customerId)
}
@@ -104,6 +119,7 @@ class StripeProviderService extends PaymentService {
customer: stripeCustomerId,
amount: amount * 100, // Stripe amount is in cents
currency: currency_code,
setup_future_usage: "on-session",
capture_method: "manual",
metadata: { cart_id: `${cart._id}` },
})

View File

@@ -29,8 +29,14 @@ class CartSubscriber {
const customer = await this.customerService_.retrieve(customer_id)
const stripeSession = payment_sessions.find(s => s.provider_id === "stripe")
if (!stripeSession) {
return Promise.resolve()
}
const paymentIntent = await this.stripeProviderService_.retrievePayment(
cart
stripeSession.data
)
let stripeCustomer = await this.stripeProviderService_.retrieveCustomer(

View File

@@ -0,0 +1,18 @@
import passport from "passport"
export default () => {
// Always go to next
return (req, res, next) => {
passport.authenticate(
["jwt", "bearer"],
{ session: false },
(err, user, info) => {
if (err) {
return next(err)
}
req.user = user
return next()
}
)(req, res, next)
}
}

View File

@@ -1,7 +1,9 @@
import { default as authenticateCustomer } from "./authenticate-customer"
import { default as authenticate } from "./authenticate"
import { default as wrap } from "./await-middleware"
export default {
authenticate,
wrap
authenticateCustomer,
wrap,
}

View File

@@ -28,7 +28,17 @@ export default async (req, res) => {
regionId = regions[0]._id
}
let cart = await cartService.create({ region_id: regionId })
let customerId = ""
if (req.user && req.user.customer_id) {
const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieve(req.user.customer_id)
customerId = customer._id
}
let cart = await cartService.create({
region_id: regionId,
customer_id: customerId,
})
if (value.items) {
await Promise.all(

View File

@@ -3,6 +3,18 @@ export default async (req, res) => {
try {
const cartService = req.scope.resolve("cartService")
let cart = await cartService.retrieve(id)
// If there is a logged in user add the user to the cart
if (req.user && req.user.customer_id) {
if (!cart.customer_id || cart.customer_id !== req.user.customer_id) {
const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieve(req.user.customer_id)
cart = await cartService.updateCustomerId(id, customer._id)
cart = await cartService.updateEmail(id, customer.email)
}
}
cart = await cartService.decorate(cart, [], ["region"])
res.json({ cart })
} catch (err) {

View File

@@ -12,7 +12,6 @@ export default async (req, res) => {
discounts: Validator.array().items({
code: Validator.string(),
}),
customer_id: Validator.string(),
})
const { value, error } = schema.validate(req.body)
@@ -28,10 +27,6 @@ export default async (req, res) => {
}
try {
if (value.customer_id) {
await cartService.updateCustomerId(id, value.customer_id)
}
if (value.region_id) {
await cartService.setRegion(id, value.region_id)
}

View File

@@ -0,0 +1,27 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id, address_id } = req.params
const schema = Validator.object().keys({
address: Validator.address(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
const customerService = req.scope.resolve("customerService")
try {
const customer = await customerService.addAddress(id, value.address)
const data = await customerService.decorate(
customer,
["email", "first_name", "last_name", "shipping_addresses"],
["orders"]
)
res.json({ customer: data })
} catch (err) {
throw err
}
}

View File

@@ -0,0 +1,16 @@
export default async (req, res) => {
const { id, address_id } = req.params
const customerService = req.scope.resolve("customerService")
try {
const customer = await customerService.removeAddress(id, address_id)
const data = await customerService.decorate(
customer,
["email", "first_name", "last_name", "shipping_addresses"],
["orders"]
)
res.json({ customer: data })
} catch (err) {
throw err
}
}

View File

@@ -4,7 +4,7 @@ export default async (req, res) => {
const customerService = req.scope.resolve("customerService")
let customer = await customerService.retrieve(id)
customer = customerService.decorate(
customer._id,
customer,
["email", "first_name", "last_name", "shipping_addresses"],
["orders"]
)

View File

@@ -0,0 +1,28 @@
export default async (req, res) => {
const { id } = req.params
try {
const storeService = req.scope.resolve("storeService")
const paymentProviderService = req.scope.resolve("paymentProviderService")
const customerService = req.scope.resolve("customerService")
let customer = await customerService.retrieve(id)
const store = await storeService.retrieve()
const methods = await Promise.all(
store.payment_providers.map(async next => {
const provider = paymentProviderService.retrieveProvider(next)
const pMethods = await provider.retrieveSavedMethods(customer)
return pMethods.map(m => ({
provider_id: next,
data: m,
}))
})
)
res.json({ payment_methods: methods.flat() })
} catch (err) {
throw err
}
}

View File

@@ -29,5 +29,26 @@ export default app => {
"/:id/password",
middlewares.wrap(require("./update-password").default)
)
route.post(
"/:id/addresses",
middlewares.wrap(require("./create-address").default)
)
route.post(
"/:id/addresses/:address_id",
middlewares.wrap(require("./update-address").default)
)
route.delete(
"/:id/addresses/:address_id",
middlewares.wrap(require("./delete-address").default)
)
route.get(
"/:id/payment-methods",
middlewares.wrap(require("./get-payment-methods").default)
)
return app
}

View File

@@ -0,0 +1,31 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { id, address_id } = req.params
const schema = Validator.object().keys({
address: Validator.address(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
const customerService = req.scope.resolve("customerService")
try {
const customer = await customerService.updateAddress(
id,
address_id,
value.address
)
const data = await customerService.decorate(
customer,
["email", "first_name", "last_name", "shipping_addresses"],
["orders"]
)
res.json({ customer: data })
} catch (err) {
throw err
}
}

View File

@@ -1,6 +1,8 @@
import { Router } from "express"
import cors from "cors"
import middlewares from "../../middlewares"
import authRoutes from "./auth"
import productRoutes from "./products"
import cartRoutes from "./carts"
@@ -22,6 +24,8 @@ export default (app, container, config) => {
})
)
route.use(middlewares.authenticateCustomer())
authRoutes(route)
customerRoutes(route)
productRoutes(route)

View File

@@ -12,5 +12,6 @@ export default new mongoose.Schema({
country_code: { type: String, required: true },
province: { type: String },
postal_code: { type: String, required: true },
phone: { type: String },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
})

View File

@@ -8,6 +8,7 @@ import { BaseService } from "medusa-interfaces"
*/
class CartService extends BaseService {
static Events = {
CUSTOMER_UPDATED: "cart.customer_updated",
CREATED: "cart.created",
UPDATED: "cart.updated",
}
@@ -207,9 +208,20 @@ class CartService extends BaseService {
}
const region = await this.regionService_.retrieve(region_id)
if (region.countries.length === 1) {
// Preselect the country if the region only has 1
data.shipping_address = {
country_code: countries[0],
}
data.billing_address = {
country_code: countries[0],
}
}
return this.cartModel_
.create({
...data,
region_id: region._id,
})
.then(result => {
@@ -445,10 +457,8 @@ class CartService extends BaseService {
*/
async updateCustomerId(cartId, customerId) {
const cart = await this.retrieve(cartId)
const schema = Validator.string()
.objectId()
.required()
const { value, error } = schema.validate(customerId)
const schema = Validator.objectId().required()
const { value, error } = schema.validate(customerId.toString())
if (error) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -467,7 +477,7 @@ class CartService extends BaseService {
)
.then(result => {
// Notify subscribers
this.eventBus_.emit(CartService.Events.UPDATED, result)
this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result)
return result
})
}
@@ -499,6 +509,8 @@ class CartService extends BaseService {
customer = await this.customerService_.create({ email })
}
const customerChanged = !customer._id.equals(cart.customer_id)
return this.cartModel_
.updateOne(
{
@@ -513,6 +525,9 @@ class CartService extends BaseService {
)
.then(result => {
// Notify subscribers
if (customerChanged) {
this.eventBus_.emit(CartService.Events.CUSTOMER_UPDATED, result)
}
this.eventBus_.emit(CartService.Events.UPDATED, result)
return result
})
@@ -966,17 +981,11 @@ class CartService extends BaseService {
// If the country code of a shipping address is set we need to clear it
let shippingAddress = cart.shipping_address
if (!_.isEmpty(shippingAddress) && shippingAddress.country_code) {
shippingAddress.country_code = ""
shippingAddress.country_code =
region.countries.length === 1 ? region.countries[0] : ""
update.shipping_address = shippingAddress
}
// If the country code of a billing address is set we need to clear it
let billingAddress = cart.billing_address
if (!_.isEmpty(billingAddress) && billingAddress.country_code) {
billingAddress.country_code = ""
update.billing_address = billingAddress
}
// Shipping methods are determined by region so the user needs to find a
// new shipping method
if (cart.shipping_methods && cart.shipping_methods.length) {
@@ -1000,7 +1009,8 @@ class CartService extends BaseService {
// Payment methods are region specific so the user needs to find a
// new payment method
if (!_.isEmpty(cart.payment_method)) {
if (!_.isEmpty(cart.payment_method) || cart.payment_sessions.length) {
update.payment_sessions = []
update.payment_method = undefined
}

View File

@@ -255,6 +255,52 @@ class CustomerService extends BaseService {
)
}
async addAddress(customerId, address) {
const customer = await this.retrieve(customerId)
this.validateBillingAddress_(address)
let shouldAdd = !!customer.shipping_addresses.find(
a =>
a.country_code === address.country_code &&
a.address_1 === address.address_1 &&
a.address_2 === address.address_2 &&
a.city === address.city &&
a.phone === address.phone &&
a.postal_code === address.postal_code &&
a.province === address.province &&
a.first_name === address.first_name &&
a.last_name === address.last_name
)
if (shouldAdd) {
return this.customerModel_.updateOne(
{ _id: customer._id },
{ $addToSet: { shipping_addresses: address } }
)
} else {
return customer
}
}
async updateAddress(customerId, addressId, address) {
const customer = await this.retrieve(customerId)
this.validateBillingAddress_(address)
return this.customerModel_.updateOne(
{ _id: customer._id, "shipping_addresses._id": addressId },
{ $set: { "shipping_addresses.$": address } }
)
}
async removeAddress(customerId, addressId) {
const customer = await this.retrieve(customerId)
return this.customerModel_.updateOne(
{ _id: customer._id },
{ $pull: { shipping_addresses: { _id: addressId } } }
)
}
/**
* Deletes a customer from a given customer id.
* @param {string} customerId - the id of the customer to delete. Must be

View File

@@ -21,6 +21,10 @@ class OrderSubscriber {
this.eventBus_.subscribe("order.placed", async order => {
await this.customerService_.addOrder(order.customer_id, order._id)
await this.customerService_.addAddress(
order.customer_id,
order.shipping_address
)
})
this.eventBus_.subscribe("order.placed", async order => {