Skip to content

Commit

Permalink
Fix checkout issues
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Oct 7, 2024
1 parent 24885d9 commit 302336a
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 52 deletions.
28 changes: 27 additions & 1 deletion api/src/common/databaseHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Notification from '../models/Notification'
import NotificationCounter from '../models/NotificationCounter'
import PushToken from '../models/PushToken'
import Token, { TOKEN_EXPIRE_AT_INDEX_NAME } from '../models/Token'
import User from '../models/User'
import User, { USER_EXPIRE_AT_INDEX_NAME } from '../models/User'
import Country from '../models/Country'
import ParkingSpot from '../models/ParkingSpot'
import AdditionalDriver from '../models/AdditionalDriver'
Expand Down Expand Up @@ -249,6 +249,16 @@ const createBookingIndex = async (): Promise<void> => {
await Booking.collection.createIndex({ expireAt: 1 }, { name: BOOKING_EXPIRE_AT_INDEX_NAME, expireAfterSeconds: env.BOOKING_EXPIRE_AT, background: true })
}

/**
* Create User TTL index.
*
* @async
* @returns {Promise<void>}
*/
const createUserIndex = async (): Promise<void> => {
await User.collection.createIndex({ expireAt: 1 }, { name: USER_EXPIRE_AT_INDEX_NAME, expireAfterSeconds: env.USER_EXPIRE_AT, background: true })
}

const createCollection = async<T>(model: Model<T>) => {
try {
await model.collection.indexes()
Expand Down Expand Up @@ -297,6 +307,22 @@ export const initialize = async (): Promise<boolean> => {
}
}

//
// Update User TTL index if configuration changes
//
const userIndexes = await User.collection.indexes()
const userIndex = userIndexes.find((index: any) => index.name === USER_EXPIRE_AT_INDEX_NAME && index.expireAfterSeconds !== env.USER_EXPIRE_AT)
if (userIndex) {
try {
await User.collection.dropIndex(userIndex.name!)
} catch (err) {
logger.error('Failed dropping User TTL index', err)
} finally {
await createUserIndex()
await User.createIndexes()
}
}

//
// Update Token TTL index if configuration changes
//
Expand Down
10 changes: 10 additions & 0 deletions api/src/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ export const STRIPE_SESSION_EXPIRE_AT = stripeSessionExpireAt
*/
export const BOOKING_EXPIRE_AT = STRIPE_SESSION_EXPIRE_AT + (10 * 60)

/**
* User expiration in seconds.
* Non verified and active users created from checkout with Stripe are temporary and are automatically deleted if the payment checkout session expires.
*
*
* @type {number}
*/
export const USER_EXPIRE_AT = BOOKING_EXPIRE_AT

/**
* Admin email.
*
Expand Down Expand Up @@ -348,6 +357,7 @@ export interface User extends Document {
blacklisted?: boolean
payLater?: boolean
customerId?: string
expireAt?: Date
}

/**
Expand Down
87 changes: 51 additions & 36 deletions api/src/controllers/bookingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,41 +182,6 @@ export const checkout = async (req: Request, res: Response) => {
throw new Error('Booking missing')
}

if (!body.payLater) {
const { paymentIntentId, sessionId } = body

if (!paymentIntentId && !sessionId) {
const message = 'Payment intent and session missing'
logger.error(message, body)
return res.status(400).send(message)
}

body.booking.customerId = body.customerId

if (paymentIntentId) {
const paymentIntent = await stripeAPI.paymentIntents.retrieve(paymentIntentId)
if (paymentIntent.status !== 'succeeded') {
const message = `Payment failed: ${paymentIntent.status}`
logger.error(message, body)
return res.status(400).send(message)
}

body.booking.paymentIntentId = paymentIntentId
body.booking.status = bookcarsTypes.BookingStatus.Paid
} else {
//
// Bookings created from checkout with Stripe are temporary
// and are automatically deleted if the payment checkout session expires.
//
const expireAt = new Date()
expireAt.setSeconds(expireAt.getSeconds() + env.BOOKING_EXPIRE_AT)

body.booking.sessionId = body.sessionId
body.booking.status = bookcarsTypes.BookingStatus.Void
body.booking.expireAt = expireAt
}
}

if (driver) {
driver.verified = false
driver.blacklisted = false
Expand Down Expand Up @@ -253,6 +218,51 @@ export const checkout = async (req: Request, res: Response) => {
return res.sendStatus(204)
}

if (!body.payLater) {
const { paymentIntentId, sessionId } = body

if (!paymentIntentId && !sessionId) {
const message = 'Payment intent and session missing'
logger.error(message, body)
return res.status(400).send(message)
}

body.booking.customerId = body.customerId

if (paymentIntentId) {
const paymentIntent = await stripeAPI.paymentIntents.retrieve(paymentIntentId)
if (paymentIntent.status !== 'succeeded') {
const message = `Payment failed: ${paymentIntent.status}`
logger.error(message, body)
return res.status(400).send(message)
}

body.booking.paymentIntentId = paymentIntentId
body.booking.status = bookcarsTypes.BookingStatus.Paid
} else {
//
// Bookings created from checkout with Stripe are temporary
// and are automatically deleted if the payment checkout session expires.
//
let expireAt = new Date()
expireAt.setSeconds(expireAt.getSeconds() + env.BOOKING_EXPIRE_AT)

body.booking.sessionId = body.sessionId
body.booking.status = bookcarsTypes.BookingStatus.Void
body.booking.expireAt = expireAt

//
// Non verified and active users created from checkout with Stripe are temporary
// and are automatically deleted if the payment checkout session expires.
//
expireAt = new Date()
expireAt.setSeconds(expireAt.getSeconds() + env.USER_EXPIRE_AT)

user.expireAt = expireAt
await user.save()
}
}

const { customerId } = body
if (customerId) {
user.customerId = customerId
Expand Down Expand Up @@ -613,7 +623,12 @@ export const deleteTempBooking = async (req: Request, res: Response) => {
const { bookingId, sessionId } = req.params

try {
await Booking.deleteOne({ _id: bookingId, sessionId, status: bookcarsTypes.BookingStatus.Void, expireAt: { $ne: null } })
const booking = await Booking.findOne({ _id: bookingId, sessionId, status: bookcarsTypes.BookingStatus.Void, expireAt: { $ne: null } })
if (booking) {
const user = await User.findOne({ _id: booking.driver, verified: false, expireAt: { $ne: null } })
await user?.deleteOne()
}
await booking?.deleteOne()
return res.sendStatus(200)
} catch (err) {
logger.error(`[booking.deleteTempBooking] ${i18n.t('DB_ERROR')} ${JSON.stringify({ bookingId, sessionId })}`, err)
Expand Down
3 changes: 3 additions & 0 deletions api/src/controllers/stripeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export const checkCheckoutSession = async (req: Request, res: Response) => {
return res.sendStatus(204)
}

user.expireAt = undefined
await user.save()

if (!await bookingController.confirm(user, booking, false)) {
return res.sendStatus(400)
}
Expand Down
6 changes: 5 additions & 1 deletion api/src/controllers/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export const activate = async (req: Request, res: Response) => {

user.active = true
user.verified = true
user.expireAt = undefined
await user.save()

return res.sendStatus(200)
Expand Down Expand Up @@ -1319,7 +1320,7 @@ export const getUsers = async (req: Request, res: Response) => {
const { body }: { body: bookcarsTypes.GetUsersBody } = req
const { types, user: userId } = body

const $match: mongoose.FilterQuery<bookcarsTypes.User> = {
const $match: mongoose.FilterQuery<env.User> = {
$and: [
{
type: { $in: types },
Expand All @@ -1330,6 +1331,9 @@ export const getUsers = async (req: Request, res: Response) => {
{ email: { $regex: keyword, $options: options } },
],
},
{
expireAt: null,
},
],
}

Expand Down
1 change: 1 addition & 0 deletions api/src/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export const en = {
SUBJECT: 'Subject',
FROM: 'From',
MESSAGE: 'Message',
LOCATION_IMAGE_NOT_FOUND: 'Location image not found',
}
1 change: 1 addition & 0 deletions api/src/lang/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ export const fr = {
SUBJECT: 'Objet',
FROM: 'De',
MESSAGE: 'Message',
LOCATION_IMAGE_NOT_FOUND: 'Image de lieu introuvable',
}
10 changes: 10 additions & 0 deletions api/src/models/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Schema, model } from 'mongoose'
import * as bookcarsTypes from ':bookcars-types'
import * as env from '../config/env.config'

export const USER_EXPIRE_AT_INDEX_NAME = 'expireAt'

const userSchema = new Schema<env.User>(
{
supplier: {
Expand Down Expand Up @@ -101,6 +103,14 @@ const userSchema = new Schema<env.User>(
customerId: {
type: String,
},
expireAt: {
//
// Non verified and active users created from checkout with Stripe are temporary and
// are automatically deleted if the payment checkout session expires.
//
type: Date,
index: { name: USER_EXPIRE_AT_INDEX_NAME, expireAfterSeconds: env.USER_EXPIRE_AT, background: true },
},
},
{
timestamps: true,
Expand Down
17 changes: 10 additions & 7 deletions backend/src/components/MultipleSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,11 +300,14 @@ const MultipleSelect = ({
<Chip {...getTagProps({ index })} key={option._id} label={option.name} />
))}
renderOption={(props, option) => {
if ('key' in props) delete props.key
if ('key' in props) {
delete props.key
}
const _props = props as React.HTMLAttributes<HTMLLIElement>

if (type === bookcarsTypes.RecordType.User) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image">
{option.image ? <Avatar src={bookcarsHelper.joinURL(env.CDN_USERS, option.image)} className="avatar-medium" /> : <AccountCircle className="avatar-medium" color="disabled" />}
</span>
Expand All @@ -315,7 +318,7 @@ const MultipleSelect = ({

if (type === bookcarsTypes.RecordType.Supplier) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image supplier-ia">
<img src={bookcarsHelper.joinURL(env.CDN_USERS, option.image)} alt={option.name} />
</span>
Expand All @@ -326,7 +329,7 @@ const MultipleSelect = ({

if (type === bookcarsTypes.RecordType.Location) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image">
<LocationIcon />
</span>
Expand All @@ -337,7 +340,7 @@ const MultipleSelect = ({

if (type === bookcarsTypes.RecordType.Country) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image">
<CountryIcon />
</span>
Expand All @@ -348,7 +351,7 @@ const MultipleSelect = ({

if (type === bookcarsTypes.RecordType.Car) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image car-ia">
<img
src={bookcarsHelper.joinURL(env.CDN_CARS, option.image)}
Expand All @@ -364,7 +367,7 @@ const MultipleSelect = ({
}

return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span>{option.name}</span>
</li>
)
Expand Down
15 changes: 10 additions & 5 deletions frontend/src/components/MultipleSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,14 @@ const MultipleSelect = ({
<Chip {...getTagProps({ index })} key={option._id} label={option.name} />
))}
renderOption={(props, option) => {
if ('key' in props) {
delete props.key
}
const _props = props as React.HTMLAttributes<HTMLLIElement>

if (type === bookcarsTypes.RecordType.User) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image">
{option.image ? <Avatar src={bookcarsHelper.joinURL(env.CDN_USERS, option.image)} className="avatar-medium" /> : <AccountCircle className="avatar-medium" color="disabled" />}
</span>
Expand All @@ -330,7 +335,7 @@ const MultipleSelect = ({
)
} if (type === bookcarsTypes.RecordType.Supplier) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image supplier-ia">
<img
src={bookcarsHelper.joinURL(env.CDN_USERS, option.image)}
Expand All @@ -343,7 +348,7 @@ const MultipleSelect = ({
)
} if (type === bookcarsTypes.RecordType.Location) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image">
<LocationIcon />
</span>
Expand All @@ -352,7 +357,7 @@ const MultipleSelect = ({
)
} if (type === bookcarsTypes.RecordType.Car) {
return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span className="option-image car-ia">
<img
src={bookcarsHelper.joinURL(env.CDN_CARS, option.image)}
Expand All @@ -368,7 +373,7 @@ const MultipleSelect = ({
}

return (
<li {...props} key={option._id} className={`${props.className} ms-option`}>
<li {..._props} key={option._id} className={`${props.className} ms-option`}>
<span>{option.name}</span>
</li>
)
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/services/UserService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3350,7 +3350,7 @@ export const hasPassword = (id: string): Promise<number> => axiosInstance
* @returns {void}
*/
export const setStayConnected = (value: boolean) => {
localStorage.setItem('oj-stay-connected', JSON.stringify(value))
localStorage.setItem('bc-stay-connected', JSON.stringify(value))
}

/**
Expand All @@ -3360,6 +3360,6 @@ export const setStayConnected = (value: boolean) => {
* @returns {boolean}
*/
export const getStayConnected = () => {
const value = JSON.parse(localStorage.getItem('oj-stay-connected') ?? 'false')
const value = JSON.parse(localStorage.getItem('bc-stay-connected') ?? 'false')
return value as boolean
}

0 comments on commit 302336a

Please sign in to comment.