Skip to content

Commit

Permalink
Update stripe integration
Browse files Browse the repository at this point in the history
  • Loading branch information
aelassas committed Apr 30, 2024
1 parent 2d1b03a commit 0c6e6be
Show file tree
Hide file tree
Showing 37 changed files with 2,651 additions and 1,969 deletions.
1 change: 1 addition & 0 deletions api/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ MI_FRONTEND_HOST=http://localhost:3004/
MI_MINIMUM_AGE=21
MI_EXPO_ACCESS_TOKEN=EXPO_ACCESS_TOKEN
MI_STRIPE_SECRET_KEY=STRIPE_SECRET_KEY
MI_STRIPE_SESSION_EXPIRE_AT=82800
137 changes: 117 additions & 20 deletions api/src/common/databaseHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import mongoose, { ConnectOptions } from 'mongoose'
import mongoose, { ConnectOptions, Model } from 'mongoose'
import * as env from '../config/env.config'
import * as logger from './logger'
import Booking, { BOOKING_EXPIRE_AT_INDEX_NAME } from '../models/Booking'
import Location from '../models/Location'
import LocationValue from '../models/LocationValue'
import Notification from '../models/Notification'
import NotificationCounter from '../models/NotificationCounter'
import Property from '../models/Property'
import PushToken from '../models/PushToken'
import Token, { TOKEN_EXPIRE_AT_INDEX_NAME } from '../models/Token'
import User from '../models/User'

/**
* Connect to database.
Expand All @@ -13,27 +22,27 @@ import * as logger from './logger'
* @returns {Promise<boolean>}
*/
export const Connect = async (uri: string, ssl: boolean, debug: boolean): Promise<boolean> => {
let options: ConnectOptions = {}

if (ssl) {
options = {
tls: true,
tlsCertificateKeyFile: env.DB_SSL_CERT,
tlsCAFile: env.DB_SSL_CA,
}
let options: ConnectOptions = {}

if (ssl) {
options = {
tls: true,
tlsCertificateKeyFile: env.DB_SSL_CERT,
tlsCAFile: env.DB_SSL_CA,
}
}

mongoose.set('debug', debug)
mongoose.Promise = globalThis.Promise
mongoose.set('debug', debug)
mongoose.Promise = globalThis.Promise

try {
await mongoose.connect(uri, options)
logger.info('Database is connected')
return true
} catch (err) {
logger.error('Cannot connect to the database:', err)
return false
}
try {
await mongoose.connect(uri, options)
logger.info('Database is connected')
return true
} catch (err) {
logger.error('Cannot connect to the database:', err)
return false
}
}

/**
Expand All @@ -45,5 +54,93 @@ export const Connect = async (uri: string, ssl: boolean, debug: boolean): Promis
* @returns {Promise<void>}
*/
export const Close = async (force: boolean = false): Promise<void> => {
await mongoose.connection.close(force)
await mongoose.connection.close(force)
}

/**
* Create Token TTL index.
*
* @async
* @returns {Promise<void>}
*/
const createTokenIndex = async (): Promise<void> => {
await Token.collection.createIndex({ expireAt: 1 }, { name: TOKEN_EXPIRE_AT_INDEX_NAME, expireAfterSeconds: env.TOKEN_EXPIRE_AT, background: true })
}

/**
* Create Booking TTL index.
*
* @async
* @returns {Promise<void>}
*/
const createBookingIndex = async (): Promise<void> => {
await Booking.collection.createIndex({ expireAt: 1 }, { name: BOOKING_EXPIRE_AT_INDEX_NAME, expireAfterSeconds: env.BOOKING_EXPIRE_AT, background: true })
}

const createCollection = async<T>(model: Model<T>) => {
try {
await model.collection.indexes()
} catch (err) {
await model.createCollection()
await model.createIndexes()
}
}

/**
* Initialize database.
*
* @async
* @returns {Promise<boolean>}
*/
export const initialize = async (): Promise<boolean> => {
try {
if (mongoose.connection.readyState) {
await createCollection<env.Booking>(Booking)
await createCollection<env.Location>(Location)
await createCollection<env.LocationValue>(LocationValue)
await createCollection<env.Notification>(Notification)
await createCollection<env.NotificationCounter>(NotificationCounter)
await createCollection<env.Property>(Property)
await createCollection<env.PushToken>(PushToken)
await createCollection<env.Token>(Token)
await createCollection<env.User>(User)
}

//
// Update Booking TTL index if configuration changes
//
const bookingIndexes = await Booking.collection.indexes()
const bookingIndex = bookingIndexes.find((index: any) => index.name === BOOKING_EXPIRE_AT_INDEX_NAME && index.expireAfterSeconds !== env.BOOKING_EXPIRE_AT)
if (bookingIndex) {
try {
await Booking.collection.dropIndex(bookingIndex.name)
} catch (err) {
logger.error('Failed dropping Booking TTL index', err)
} finally {
await createBookingIndex()
await Booking.createIndexes()
}
}

//
// Update Token TTL index if configuration changes
//
const tokenIndexes = await Token.collection.indexes()
const tokenIndex = tokenIndexes.find((index: any) => index.name.includes(TOKEN_EXPIRE_AT_INDEX_NAME))
if (tokenIndex) {
try {
await Token.collection.dropIndex(tokenIndex.name)
} catch (err) {
logger.error('Failed dropping Token TTL index', err)
} finally {
await createTokenIndex()
await Token.createIndexes()
}
}

return true
} catch (err) {
logger.error('An error occured while initializing database:', err)
return false
}
}
43 changes: 33 additions & 10 deletions api/src/common/logger.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import winston, { format, transports } from 'winston'

let ENABLE_LOGGING = true
let ENABLE_ERROR_LOGGING = true

const logFormat = format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`)

const logger = winston.createLogger({
Expand Down Expand Up @@ -27,19 +30,39 @@ const logger = winston.createLogger({
})

export const info = (message: string, obj?: any) => {
if (obj) {
logger.info(`${message} ${JSON.stringify(obj)}`)
} else {
logger.info(message)
if (ENABLE_LOGGING) {
if (obj) {
logger.info(`${message} ${JSON.stringify(obj)}`)
} else {
logger.info(message)
}
}
}

export const error = (message: string, obj?: unknown) => {
if (obj instanceof Error) {
logger.error(`${message} ${obj.message}`) // ${err.stack}
} else if (obj) {
logger.error(`${message} ${JSON.stringify(obj)}`)
} else {
logger.error(message)
if (ENABLE_LOGGING && ENABLE_ERROR_LOGGING) {
if (obj instanceof Error) {
logger.error(`${message} ${obj.message}`) // ${err.stack}
} else if (obj) {
logger.error(`${message} ${JSON.stringify(obj)}`)
} else {
logger.error(message)
}
}
}

export const enableLogging = () => {
ENABLE_LOGGING = true
}

export const disableLogging = () => {
ENABLE_LOGGING = false
}

export const enableErrorLogging = () => {
ENABLE_ERROR_LOGGING = true
}

export const disableErrorLogging = () => {
ENABLE_ERROR_LOGGING = false
}
24 changes: 24 additions & 0 deletions api/src/config/env.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,26 @@ export const EXPO_ACCESS_TOKEN = __env__('MI_EXPO_ACCESS_TOKEN', false)
* @type {string}
*/
export const STRIPE_SECRET_KEY = __env__('MI_STRIPE_SECRET_KEY', false, 'STRIPE_SECRET_KEY')
let stripeSessionExpireAt = Number.parseInt(__env__('MI_STRIPE_SESSION_EXPIRE_AT', false, '82800'), 10)
stripeSessionExpireAt = stripeSessionExpireAt < 1800 ? 1800 : stripeSessionExpireAt
stripeSessionExpireAt = stripeSessionExpireAt <= 82800 ? stripeSessionExpireAt : 82800

/**
* Stripe Checkout Session expiration in seconds. Should be at least 1800 seconds (30min) and max 82800 seconds. Default is 82800 seconds (~23h).
* If the value is lower than 1800 seconds, it wil be set to 1800 seconds.
* If the value is greater than 82800 seconds, it wil be set to 82800 seconds.
*
* @type {number}
*/
export const STRIPE_SESSION_EXPIRE_AT = stripeSessionExpireAt

/**
* Booking expiration in seconds.
* Bookings created from checkout with Stripe are temporary and are automatically deleted if the payment checkout session expires.
*
* @type {number}
*/
export const BOOKING_EXPIRE_AT = STRIPE_SESSION_EXPIRE_AT + (10 * 60)

/**
* User Document.
Expand Down Expand Up @@ -337,6 +357,10 @@ export interface Booking extends Document {
cancellation?: boolean
cancelRequest?: boolean
price: number
sessionId?: string
paymentIntentId?: string
customerId?: string
expireAt?: Date
}

/**
Expand Down
2 changes: 2 additions & 0 deletions api/src/config/stripeRoutes.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const routes = {
createCheckoutSession: '/api/create-checkout-session',
checkCheckoutSession: '/api/check-checkout-session/:sessionId',
createPaymentIntent: '/api/create-payment-intent',
}

Expand Down
39 changes: 28 additions & 11 deletions api/src/controllers/bookingController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,21 +106,38 @@ export const checkout = async (req: Request, res: Response) => {
}

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

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

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.customerId = body.customerId

body.booking.status = movininTypes.BookingStatus.Paid
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 = movininTypes.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 = movininTypes.BookingStatus.Void
body.booking.expireAt = expireAt
}
}

if (renter) {
Expand Down Expand Up @@ -221,7 +238,7 @@ export const checkout = async (req: Request, res: Response) => {
i18n.locale = agency.language
await notifyAgency(user, booking._id.toString(), agency, i18n.t('BOOKING_NOTIFICATION'))

return res.sendStatus(200)
return res.status(200).send({ bookingId: booking._id })
} catch (err) {
logger.error(`[booking.checkout] ${i18n.t('ERROR')}`, err)
return res.status(400).send(i18n.t('ERROR') + err)
Expand Down Expand Up @@ -538,7 +555,7 @@ export const getBookings = async (req: Request, res: Response) => {
const options = 'i'

const $match: mongoose.FilterQuery<any> = {
$and: [{ 'agency._id': { $in: agencies } }, { status: { $in: statuses } }],
$and: [{ 'agency._id': { $in: agencies } }, { status: { $in: statuses } }, { expireAt: null }],
}

if (user) {
Expand Down
Loading

0 comments on commit 0c6e6be

Please sign in to comment.