Skip to content

Commit

Permalink
530 expire checkout sessions last spot (#556)
Browse files Browse the repository at this point in the history
* Webhook expires other checkout sessions when the last booking spot is taken.

* codegen

* Refactored logic to calculate available slots.

* removed isLastSpotTaken() from BookingSlotsService.ts

* codegen

* NZ doesn't accept async payment

* Added parsing of session.metadata[BOOKING_SLOTS_KEY] to handle the metadata correctly.

Adjusted the logic to filter sessions based on the booking slot IDs from parsed metadata.

* initialised booking data/slot services in function rather than passing it through.

* Used relevant method in StripeService.ts to fetch sessions 24hrs ago.

* codegen

* codegen

* Moved isLastSpotTaken function to BookingUtils.ts and wrote tests.

* prettier
  • Loading branch information
AzizPatel786 authored Jul 4, 2024
1 parent 6dab60b commit 6a78876
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 0 deletions.
41 changes: 41 additions & 0 deletions server/src/business-layer/services/StripeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import BookingSlotService from "data-layer/services/BookingSlotsService"
import console from "console"
import MailService from "./MailService"
import BookingUtils from "../utils/BookingUtils"

const stripe = new Stripe(process.env.STRIPE_API_KEY)

Expand Down Expand Up @@ -432,6 +433,12 @@ export default class StripeService {
stripe_payment_id: session.id,
user_id: uid
})

// Check if the last spot is taken and expire other sessions
const isLastSpot = await BookingUtils.isLastSpotTaken(bookingSlotId)
if (isLastSpot) {
await this.expireOtherCheckoutSessions(bookingSlotId)
}
})
)
/**
Expand All @@ -452,6 +459,40 @@ export default class StripeService {
}
}

private async expireOtherCheckoutSessions(
bookingSlotId: string
): Promise<void> {
try {
// Fetch all checkout sessions for the specific booking slot id
const sessions = await this.getRecentActiveSessions(
CheckoutTypeValues.BOOKING,
1440
)

const sessionsToExpire = sessions.filter((session) => {
const bookingSlots = JSON.parse(
session.metadata[BOOKING_SLOTS_KEY]
) as Array<string>
return bookingSlots.includes(bookingSlotId)
})

// Expire each session that matches the booking slot id
await Promise.all(
sessionsToExpire.map(async (session) => {
if (session.id) {
await stripe.checkout.sessions.expire(session.id)
}
})
)

console.log(
`[WEBHOOK] Expired ${sessionsToExpire.length} sessions for booking slot ${bookingSlotId}`
)
} catch (err) {
console.error(`[WEBHOOK] Error expiring checkout sessions: ${err}`)
}
}

public async addCouponToUser(
stripeId: string,
amount: number
Expand Down
64 changes: 64 additions & 0 deletions server/src/business-layer/utils/BookingUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Timestamp } from "firebase-admin/firestore"
import BookingUtils, { _earliestDate, _latestDate } from "./BookingUtils"
import { LodgePricingTypeValues } from "./StripeProductMetadata"
import BookingDataService from "../../data-layer/services/BookingDataService"
import { cleanFirestore } from "../../test-config/TestUtils"
import { BookingSlot } from "../../data-layer/models/firebase"
import BookingSlotsService from "../../data-layer/services/BookingSlotsService"

describe("BookingUtils", () => {
describe("hasInvalidStartAndEndDates", () => {
Expand Down Expand Up @@ -95,4 +99,64 @@ describe("BookingUtils", () => {
)
})
})

describe("isLastSpotTaken", () => {
afterEach(async () => {
await cleanFirestore()
})

it("should return true if the last spot is taken", async () => {
// Create a booking slot with a maximum of 2 bookings
const timestamp = Timestamp.fromDate(new Date(2024, 4, 23))
const bookingSlotData: BookingSlot = {
date: timestamp,
description: "booking_slot_description",
max_bookings: 2
}
const { id: slotId } = await new BookingSlotsService().createBookingSlot(
bookingSlotData
)

// Create two bookings for the same slot
await new BookingDataService().createBooking({
user_id: "ronaldo",
booking_slot_id: slotId,
stripe_payment_id: "stripeID3"
})

await new BookingDataService().createBooking({
user_id: "sui",
booking_slot_id: slotId,
stripe_payment_id: "stripeID1"
})

const result = await BookingUtils.isLastSpotTaken(slotId)

expect(result).toBe(true)
})

it("should return false if spots are still available", async () => {
// Create a booking slot with a maximum of 7 bookings
const timestamp = Timestamp.fromDate(new Date(2024, 4, 23))
const bookingSlotData: BookingSlot = {
date: timestamp,
description: "booking_slot_description",
max_bookings: 7
}
const { id: slotId } = await new BookingSlotsService().createBookingSlot(
bookingSlotData
)

// Create 1 booking for the slot
await new BookingDataService().createBooking({
user_id: "sdf",
booking_slot_id: slotId,
stripe_payment_id: "stripeID3"
})

const result = await BookingUtils.isLastSpotTaken(slotId)

expect(result).toBe(false)
})
})
})
21 changes: 21 additions & 0 deletions server/src/business-layer/utils/BookingUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Timestamp } from "firebase-admin/firestore"
import { LodgePricingTypeValues } from "./StripeProductMetadata"
import { firestoreTimestampToDate } from "data-layer/adapters/DateUtils"
import BookingDataService from "../../data-layer/services/BookingDataService"
import BookingSlotService from "../../data-layer/services/BookingSlotsService"

// Need to validate the booking date through a startDate and endDate range.
/**
Expand Down Expand Up @@ -90,6 +92,25 @@ const BookingUtils = {
} else {
return LodgePricingTypeValues.Normal
}
},

/**
* Checks if the last spot is taken for a specific booking slot
* @param bookingSlotId The ID of the booking slot
* @returns true if the last spot is taken, false otherwise
*/
isLastSpotTaken: async function (bookingSlotId: string): Promise<boolean> {
const bookingDataService = new BookingDataService()
const bookingSlotService = new BookingSlotService()

const bookingSlot =
await bookingSlotService.getBookingSlotById(bookingSlotId)

const bookings = await bookingDataService.getBookingsBySlotId(bookingSlotId)
const bookingCount = bookings.length

const availableSlots = bookingSlot.max_bookings - bookingCount
return availableSlots <= 0
}
} as const

Expand Down

0 comments on commit 6a78876

Please sign in to comment.