Adds account support

This commit is contained in:
Sebastian Rindom committed 2020-07-12 12:11:31 +02:00
1 parent 3eb2e9fe49
commit 60b8bca753
18 files changed
+195 -39

No files matched your search

@@ -19,10 +19,7 @@ export default async (req, res) => {
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id) await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id)
} catch (err) { } catch (err) {
if (err.type === MeudsaError.Types.NOT_FOUND) { if (err.type === MeudsaError.Types.NOT_FOUND) {
let cart = await cartService.retrieve(cartId) const cart = await cartService.retrieve(cartId)
const method = cart.payment_sessions.find(p => p.provider_id === "klarna")
cart = await cartService.setPaymentMethod(cart._id, method)
const order = await orderService.createFromCart(cart) const order = await orderService.createFromCart(cart)
await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id) await klarnaProviderService.acknowledgeOrder(klarnaOrder.order_id, order._id)
} }
@@ -30,6 +27,7 @@ export default async (req, res) => {
res.sendStatus(200) res.sendStatus(200)
} catch (error) { } catch (error) {
console.log(error)
throw error throw error
} }
} }
@@ -78,7 +78,7 @@ class KlarnaProviderService extends PaymentService {
0 0
// Withdraw discount from the total item amount // Withdraw discount from the total item amount
const quantity = item.content.quantity const quantity = item.quantity
const unit_price = item.content.unit_price * 100 * (taxRate + 1) const unit_price = item.content.unit_price * 100 * (taxRate + 1)
const total_discount_amount = itemDiscount * (taxRate + 1) * 100 const total_discount_amount = itemDiscount * (taxRate + 1) * 100
const total_amount = unit_price * quantity - total_discount_amount const total_amount = unit_price * quantity - total_discount_amount
@@ -155,7 +155,7 @@ class KlarnaProviderService extends PaymentService {
terms: this.options_.merchant_urls.terms, terms: this.options_.merchant_urls.terms,
checkout: this.options_.merchant_urls.checkout, checkout: this.options_.merchant_urls.checkout,
confirmation: this.options_.merchant_urls.confirmation, confirmation: this.options_.merchant_urls.confirmation,
push: `${this.backendUrl_}/klarna/push`, push: `${this.backendUrl_}/klarna/push?klarna_order_id={checkout.order_id}`,
shipping_option_update: `${this.backendUrl_}/klarna/shipping`, shipping_option_update: `${this.backendUrl_}/klarna/shipping`,
address_update: `${this.backendUrl_}/klarna/address`, address_update: `${this.backendUrl_}/klarna/address`,
} }
@@ -274,6 +274,7 @@ class KlarnaProviderService extends PaymentService {
) )
return klarnaOrderId return klarnaOrderId
} catch (error) { } catch (error) {
throw error throw error
} }
} }
@@ -13,21 +13,17 @@ export default async (req, res) => {
const paymentIntent = event.data.object const paymentIntent = event.data.object
const orderService = req.scope.resolve("orderService") const orderService = req.scope.resolve("orderService")
const cartId = paymentIntent.metadata.cart_id
const order = await orderService.retrieveByCartId(cartId)
// handle payment intent events // handle payment intent events
switch (event.type) { switch (event.type) {
case "payment_intent.succeeded": case "payment_intent.succeeded":
const cartId = paymentIntent.metadata.cart_id
const order = await orderService.retrieveByCartId(cartId)
await orderService.update(order._id, { await orderService.update(order._id, {
payment_status: "captured", payment_status: "captured",
}) })
break break
case "payment_intent.cancelled": case "payment_intent.cancelled":
const cartId = paymentIntent.metadata.cart_id
const order = await orderService.retrieveByCartId(cartId)
await orderService.update(order._id, { await orderService.update(order._id, {
status: "cancelled", status: "cancelled",
}) })
@@ -15,6 +15,8 @@ export default async (req, res) => {
} }
const authService = req.scope.resolve("authService") const authService = req.scope.resolve("authService")
const customerService = req.scope.resolve("customerService")
const result = await authService.authenticateCustomer( const result = await authService.authenticateCustomer(
value.email, value.email,
value.password value.password
@@ -26,12 +28,21 @@ export default async (req, res) => {
// Add JWT to cookie // Add JWT to cookie
req.session.jwt = jwt.sign( req.session.jwt = jwt.sign(
{ customer_id: result.user._id }, { customer_id: result.customer._id },
config.jwtSecret, config.jwtSecret,
{ {
expiresIn: "30d", expiresIn: "30d",
} }
) )
res.json({ customer: result.customer }) const data = await customerService.decorate(result.customer, [
"_id",
"email",
"orders",
"shipping_addresses",
"first_name",
"last_name",
])
res.json({ customer: data })
} }
@@ -0,0 +1,4 @@
export default async (req, res) => {
req.session.jwt = {}
res.json({})
}
@@ -0,0 +1,13 @@
import { Validator, MedusaError } from "medusa-core-utils"
export default async (req, res) => {
const { email } = req.params
try {
const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieveByEmail(email)
res.status(200).json({ exists: !!customer.password_hash })
} catch (err) {
res.status(200).json({ exists: false })
}
}
@@ -0,0 +1,26 @@
export default async (req, res) => {
try {
if (req.user && req.user.customer_id) {
const customerService = req.scope.resolve("customerService")
const customer = await customerService.retrieve(req.user.customer_id)
const data = await customerService.decorate(
customer,
[
"_id",
"email",
"orders",
"shipping_addresses",
"first_name",
"last_name",
],
["orders"]
)
res.json({ customer: data })
}
res.sendStatus(401)
} catch (err) {
throw err
}
}
@@ -5,6 +5,15 @@ const route = Router()
export default app => { export default app => {
app.use("/auth", route) app.use("/auth", route)
route.get(
"/",
middlewares.authenticate(),
middlewares.wrap(require("./get-session").default)
)
route.get("/:email", middlewares.wrap(require("./exists").default))
route.delete("/", middlewares.wrap(require("./delete-session").default))
route.post("/", middlewares.wrap(require("./create-session").default)) route.post("/", middlewares.wrap(require("./create-session").default))
return app return app
} }
@@ -1,4 +1,6 @@
import jwt from "jsonwebtoken"
import { Validator, MedusaError } from "medusa-core-utils" import { Validator, MedusaError } from "medusa-core-utils"
import config from "../../../../config"
export default async (req, res) => { export default async (req, res) => {
const schema = Validator.object().keys({ const schema = Validator.object().keys({
@@ -17,7 +19,24 @@ export default async (req, res) => {
try { try {
const customerService = req.scope.resolve("customerService") const customerService = req.scope.resolve("customerService")
const customer = await customerService.create(value) const customer = await customerService.create(value)
const data = await customerService.decorate(customer, ["_id", "email"])
// Add JWT to cookie
req.session.jwt = jwt.sign(
{ customer_id: customer._id },
config.jwtSecret,
{
expiresIn: "30d",
}
)
const data = await customerService.decorate(customer, [
"_id",
"email",
"orders",
"shipping_addresses",
"first_name",
"last_name",
])
res.status(201).json({ customer: data }) res.status(201).json({ customer: data })
} catch (err) { } catch (err) {
throw err throw err
@@ -6,9 +6,6 @@ const route = Router()
export default app => { export default app => {
app.use("/customers", route) app.use("/customers", route)
route.get("/", middlewares.wrap(require("./list-customers").default))
route.get("/:id", middlewares.wrap(require("./get-customer").default))
route.post("/", middlewares.wrap(require("./create-customer").default)) route.post("/", middlewares.wrap(require("./create-customer").default))
route.post( route.post(
@@ -26,6 +23,7 @@ export default app => {
route.param("id", middlewares.wrap(require("./authorize-customer").default)) route.param("id", middlewares.wrap(require("./authorize-customer").default))
route.get("/:id", middlewares.wrap(require("./get-customer").default))
route.post("/:id", middlewares.wrap(require("./update-customer").default)) route.post("/:id", middlewares.wrap(require("./update-customer").default))
route.post( route.post(
"/:id/password", "/:id/password",
@@ -1,12 +0,0 @@
export default async (req, res) => {
const selector = {}
try {
const customerService = req.scope.resolve("customerService")
const customers = await customerService.list(selector)
res.json({ customers })
} catch (error) {
throw error
}
}
@@ -1,6 +1,7 @@
import { Router } from "express" import { Router } from "express"
import cors from "cors" import cors from "cors"
import authRoutes from "./auth"
import productRoutes from "./products" import productRoutes from "./products"
import cartRoutes from "./carts" import cartRoutes from "./carts"
import orderRoutes from "./orders" import orderRoutes from "./orders"
@@ -21,6 +22,7 @@ export default (app, container, config) => {
}) })
) )
authRoutes(route)
customerRoutes(route) customerRoutes(route)
productRoutes(route) productRoutes(route)
orderRoutes(route) orderRoutes(route)
@@ -36,6 +36,26 @@ export default async (req, res) => {
res.status(200).json({ order }) res.status(200).json({ order })
} catch (err) { } catch (err) {
throw err // If something fails it might be because the order has already been created
// if it has we find it from the cart id
const orderService = req.scope.resolve("orderService")
let order = await orderService.retrieveByCartId(value.cartId)
order = await orderService.decorate(order, [
"status",
"fulfillment_status",
"payment_status",
"email",
"billing_address",
"shipping_address",
"items",
"region",
"discounts",
"customer_id",
"payment_method",
"shipping_methods",
"metadata",
])
res.status(200).json({ order })
} }
} }
+4 -2
View File
@@ -11,9 +11,11 @@ class CustomerModel extends BaseModel {
static schema = { static schema = {
email: { type: String, required: true, unique: true }, email: { type: String, required: true, unique: true },
first_name: { type: String, required: true }, first_name: { type: String },
last_name: { type: String, required: true }, last_name: { type: String },
billing_address: { type: AddressSchema }, billing_address: { type: AddressSchema },
payment_methods: { type: [mongoose.Schema.Types.Mixed], default: [] },
shipping_addresses: { type: [AddressSchema], default: [] },
password_hash: { type: String }, password_hash: { type: String },
has_account: { type: Boolean, default: false }, has_account: { type: Boolean, default: false },
orders: { type: [String], default: [] }, orders: { type: [String], default: [] },
+21 -1
View File
@@ -22,6 +22,7 @@ class CartService extends BaseService {
lineItemService, lineItemService,
shippingOptionService, shippingOptionService,
shippingProfileService, shippingProfileService,
customerService,
discountService, discountService,
totalsService, totalsService,
}) { }) {
@@ -51,6 +52,9 @@ class CartService extends BaseService {
/** @private @const {ShippingProfileService} */ /** @private @const {ShippingProfileService} */
this.shippingProfileService_ = shippingProfileService this.shippingProfileService_ = shippingProfileService
/** @private @const {CustomerService} */
this.customerService_ = customerService
/** @private @const {ShippingOptionService} */ /** @private @const {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService this.shippingOptionService_ = shippingOptionService
@@ -487,13 +491,24 @@ class CartService extends BaseService {
) )
} }
let customer = await this.customerService_
.retrieveByEmail(value)
.catch(err => undefined)
if (!customer) {
customer = await this.customerService_.create({ email })
}
return this.cartModel_ return this.cartModel_
.updateOne( .updateOne(
{ {
_id: cart._id, _id: cart._id,
}, },
{ {
$set: { email: value }, $set: {
email: value,
customer_id: customer._id,
},
} }
) )
.then(result => { .then(result => {
@@ -998,6 +1013,11 @@ class CartService extends BaseService {
}) })
} }
async delete(cartId) {
const cart = await this.retrieve(cartId)
return this.cartModel_.deleteOne({ _id: cart._id })
}
/** /**
* Dedicated method to set metadata for a cart. * Dedicated method to set metadata for a cart.
* To ensure that plugins does not overwrite each * To ensure that plugins does not overwrite each
+36 -2
View File
@@ -13,7 +13,7 @@ class CustomerService extends BaseService {
PASSWORD_RESET: "customer.password_reset", PASSWORD_RESET: "customer.password_reset",
} }
constructor({ customerModel, eventBusService }) { constructor({ customerModel, orderService, eventBusService }) {
super() super()
/** @private @const {CustomerModel} */ /** @private @const {CustomerModel} */
@@ -21,6 +21,9 @@ class CustomerService extends BaseService {
/** @private @const {EventBus} */ /** @private @const {EventBus} */
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
/** @private @const {EventBus} */
this.orderService_ = orderService
} }
/** /**
@@ -171,6 +174,21 @@ class CustomerService extends BaseService {
this.validateBillingAddress_(billing_address) this.validateBillingAddress_(billing_address)
} }
const existing = await this.retrieveByEmail(email).catch(err => undefined)
if (existing && password && !existing.has_account) {
const hashedPassword = await bcrypt.hash(password, 10)
customer.password_hash = hashedPassword
customer.has_account = true
delete customer.password
return this.customerModel_.updateOne(
{ _id: existing._id },
{
$set: customer,
}
)
} else {
if (password) { if (password) {
const hashedPassword = await bcrypt.hash(password, 10) const hashedPassword = await bcrypt.hash(password, 10)
customer.password_hash = hashedPassword customer.password_hash = hashedPassword
@@ -182,6 +200,7 @@ class CustomerService extends BaseService {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message) throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
}) })
} }
}
/** /**
* Updates a customer. Metadata updates and address updates should * Updates a customer. Metadata updates and address updates should
@@ -228,6 +247,14 @@ class CustomerService extends BaseService {
}) })
} }
async addOrder(customerId, orderId) {
const customer = await this.retrieve(customerId)
return this.customerModel_.updateOne(
{ _id: customer._id },
{ $addToSet: { orders: orderId } }
)
}
/** /**
* Deletes a customer from a given customer id. * Deletes a customer from a given customer id.
* @param {string} customerId - the id of the customer to delete. Must be * @param {string} customerId - the id of the customer to delete. Must be
@@ -255,9 +282,16 @@ class CustomerService extends BaseService {
* @param {string[]} expandFields - fields to expand. * @param {string[]} expandFields - fields to expand.
* @return {Customer} return the decorated customer. * @return {Customer} return the decorated customer.
*/ */
decorate(customer, fields, expandFields = []) { async decorate(customer, fields, expandFields = []) {
const requiredFields = ["_id", "metadata"] const requiredFields = ["_id", "metadata"]
const decorated = _.pick(customer, fields.concat(requiredFields)) const decorated = _.pick(customer, fields.concat(requiredFields))
if (expandFields.includes("orders")) {
decorated.orders = await Promise.all(
customer.orders.map(async o => this.orderService_.retrieve(o))
)
}
return decorated return decorated
} }
+2 -2
View File
@@ -317,8 +317,8 @@ class OrderService extends BaseService {
}) })
// Emit and return // Emit and return
this.eventBus_.emit(OrderService.Events.PLACED, orderDocument) this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0])
return orderDocument return orderDocument[0]
}) })
} }
+16 -1
View File
@@ -1,6 +1,13 @@
class OrderSubscriber { class OrderSubscriber {
constructor({ paymentProviderService, eventBusService }) { constructor({
paymentProviderService,
cartService,
customerService,
eventBusService,
}) {
this.paymentProviderService_ = paymentProviderService this.paymentProviderService_ = paymentProviderService
this.customerService_ = customerService
this.cartService_ = cartService
this.eventBus_ = eventBusService this.eventBus_ = eventBusService
@@ -11,6 +18,14 @@ class OrderSubscriber {
await paymentProvider.capturePayment(order._id) await paymentProvider.capturePayment(order._id)
}) })
this.eventBus_.subscribe("order.placed", async order => {
await this.customerService_.addOrder(order.customer_id, order._id)
})
this.eventBus_.subscribe("order.placed", async order => {
await this.cartService_.delete(order.cart_id)
})
} }
} }