diff --git a/packages/medusa-core-utils/src/validator.js b/packages/medusa-core-utils/src/validator.js index 54be1dec22..18f8619bd2 100644 --- a/packages/medusa-core-utils/src/validator.js +++ b/packages/medusa-core-utils/src/validator.js @@ -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(), }) } diff --git a/packages/medusa-interfaces/src/payment-service.js b/packages/medusa-interfaces/src/payment-service.js index 01a39eb2ad..9d5d957269 100644 --- a/packages/medusa-interfaces/src/payment-service.js +++ b/packages/medusa-interfaces/src/payment-service.js @@ -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 diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index 752e3d3681..3ef693116d 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -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}` }, }) diff --git a/packages/medusa-payment-stripe/src/subscribers/cart.js b/packages/medusa-payment-stripe/src/subscribers/cart.js index ef3b65fa5d..4cca3bdfcb 100644 --- a/packages/medusa-payment-stripe/src/subscribers/cart.js +++ b/packages/medusa-payment-stripe/src/subscribers/cart.js @@ -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( diff --git a/packages/medusa/src/api/middlewares/authenticate-customer.js b/packages/medusa/src/api/middlewares/authenticate-customer.js new file mode 100644 index 0000000000..61ca8e1b3a --- /dev/null +++ b/packages/medusa/src/api/middlewares/authenticate-customer.js @@ -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) + } +} diff --git a/packages/medusa/src/api/middlewares/index.js b/packages/medusa/src/api/middlewares/index.js index 10b3070450..eea2022fd5 100644 --- a/packages/medusa/src/api/middlewares/index.js +++ b/packages/medusa/src/api/middlewares/index.js @@ -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, } diff --git a/packages/medusa/src/api/routes/store/carts/create-cart.js b/packages/medusa/src/api/routes/store/carts/create-cart.js index 389e37e79a..46c263367f 100644 --- a/packages/medusa/src/api/routes/store/carts/create-cart.js +++ b/packages/medusa/src/api/routes/store/carts/create-cart.js @@ -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( diff --git a/packages/medusa/src/api/routes/store/carts/get-cart.js b/packages/medusa/src/api/routes/store/carts/get-cart.js index a5b2f4f84e..639cee6efc 100644 --- a/packages/medusa/src/api/routes/store/carts/get-cart.js +++ b/packages/medusa/src/api/routes/store/carts/get-cart.js @@ -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) { diff --git a/packages/medusa/src/api/routes/store/carts/update-cart.js b/packages/medusa/src/api/routes/store/carts/update-cart.js index 84ab93b0fe..f9f38cee1e 100644 --- a/packages/medusa/src/api/routes/store/carts/update-cart.js +++ b/packages/medusa/src/api/routes/store/carts/update-cart.js @@ -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) } diff --git a/packages/medusa/src/api/routes/store/customers/create-address.js b/packages/medusa/src/api/routes/store/customers/create-address.js new file mode 100644 index 0000000000..b1d507be8e --- /dev/null +++ b/packages/medusa/src/api/routes/store/customers/create-address.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/store/customers/delete-address.js b/packages/medusa/src/api/routes/store/customers/delete-address.js new file mode 100644 index 0000000000..c38b5bee00 --- /dev/null +++ b/packages/medusa/src/api/routes/store/customers/delete-address.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/store/customers/get-customer.js b/packages/medusa/src/api/routes/store/customers/get-customer.js index 2372276e87..3f7f32d808 100644 --- a/packages/medusa/src/api/routes/store/customers/get-customer.js +++ b/packages/medusa/src/api/routes/store/customers/get-customer.js @@ -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"] ) diff --git a/packages/medusa/src/api/routes/store/customers/get-payment-methods.js b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js new file mode 100644 index 0000000000..08936c0ed5 --- /dev/null +++ b/packages/medusa/src/api/routes/store/customers/get-payment-methods.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/store/customers/index.js b/packages/medusa/src/api/routes/store/customers/index.js index 854b37ed8b..e1a78e2681 100644 --- a/packages/medusa/src/api/routes/store/customers/index.js +++ b/packages/medusa/src/api/routes/store/customers/index.js @@ -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 } diff --git a/packages/medusa/src/api/routes/store/customers/update-address.js b/packages/medusa/src/api/routes/store/customers/update-address.js new file mode 100644 index 0000000000..ecc6d74c19 --- /dev/null +++ b/packages/medusa/src/api/routes/store/customers/update-address.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index c6429b697e..7791190262 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -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) diff --git a/packages/medusa/src/models/schemas/address.js b/packages/medusa/src/models/schemas/address.js index dec1249745..c79625c696 100644 --- a/packages/medusa/src/models/schemas/address.js +++ b/packages/medusa/src/models/schemas/address.js @@ -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: {} }, }) diff --git a/packages/medusa/src/services/cart.js b/packages/medusa/src/services/cart.js index bf61fc7400..a716ef11b7 100644 --- a/packages/medusa/src/services/cart.js +++ b/packages/medusa/src/services/cart.js @@ -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 } diff --git a/packages/medusa/src/services/customer.js b/packages/medusa/src/services/customer.js index 2f3e29664b..158841cf54 100644 --- a/packages/medusa/src/services/customer.js +++ b/packages/medusa/src/services/customer.js @@ -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 diff --git a/packages/medusa/src/subscribers/order.js b/packages/medusa/src/subscribers/order.js index 6e7afb0ec7..5349207d0b 100644 --- a/packages/medusa/src/subscribers/order.js +++ b/packages/medusa/src/subscribers/order.js @@ -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 => {