perf: 优化空场监控
This commit is contained in:
@@ -88,6 +88,38 @@ describe('范思伯特福中福响应映射', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('把同一场地连续时段作为一笔组合订单提交并汇总金额', () => {
|
||||
const first = createSlot(0, true)
|
||||
const second = {
|
||||
...createSlot(0, true),
|
||||
beginDatetime: '2026-07-16 09:00:00',
|
||||
endDatetime: '2026-07-16 09:59:00',
|
||||
cost: 90
|
||||
}
|
||||
const slots = parseAvailabilityResponse({
|
||||
successed: true,
|
||||
result: { slots: [first, second] }
|
||||
}, query).slots
|
||||
|
||||
expect(buildCreateBookingRequestBody(slots)).toEqual({
|
||||
userId: 6038652,
|
||||
projectType: 0,
|
||||
classroomItems: [{
|
||||
classroomUid: '1775556146897330236',
|
||||
beginDatetime: '2026-07-16 08:00:00',
|
||||
endDatetime: '2026-07-16 08:59:00',
|
||||
peopleNum: 1
|
||||
}, {
|
||||
classroomUid: '1775556146897330236',
|
||||
beginDatetime: '2026-07-16 09:00:00',
|
||||
endDatetime: '2026-07-16 09:59:00',
|
||||
peopleNum: 1
|
||||
}],
|
||||
remark: '',
|
||||
combinationPayments: [{ paymentMethod: 900, cost: 170 }]
|
||||
})
|
||||
})
|
||||
|
||||
it('把下单成功响应映射为待支付锁场,不暴露支付脚本给公共订单', () => {
|
||||
const slot = parseAvailabilityResponse({
|
||||
successed: true,
|
||||
@@ -146,7 +178,7 @@ describe('范思伯特福中福响应映射', () => {
|
||||
visitorId: 'visitor-token'
|
||||
}
|
||||
|
||||
await fansiboteFuzhongAdapter.createBooking!(slot, settings)
|
||||
await fansiboteFuzhongAdapter.createBooking!([slot], settings)
|
||||
await fansiboteFuzhongAdapter.cancelBooking!(settings)
|
||||
|
||||
const createRequest = fetchMock.mock.calls[0]?.[1] as RequestInit
|
||||
@@ -181,7 +213,7 @@ describe('范思伯特福中福响应映射', () => {
|
||||
const fetchMock = vi.fn()
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(fansiboteFuzhongAdapter.createBooking!(slot, {
|
||||
await expect(fansiboteFuzhongAdapter.createBooking!([slot], {
|
||||
courtId: query.courtId
|
||||
})).rejects.toThrow('PSPLVISITORID')
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
@@ -198,7 +230,7 @@ describe('范思伯特福中福响应映射', () => {
|
||||
|
||||
expect(result.slots).toEqual([
|
||||
expect.objectContaining({
|
||||
id: '1775556146897330236:2026-07-16 08:00:00',
|
||||
id: '1775556146897330236:2026-07-16 08:00:00:2026-07-16 08:59:00',
|
||||
courtLabel: '1号风雨场',
|
||||
startTime: '08:00',
|
||||
endTime: '08:59',
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
EnrollmentActivity
|
||||
} from '../../../shared/contracts'
|
||||
import type { CourtRuntimeSettings } from '../../settings/types'
|
||||
import { isContinuousSingleCourtSequence, sortSlots } from '../../bookings/slotSequence'
|
||||
import { ProviderMutationError, type AdapterBookingResult, type CourtAdapter } from '../types'
|
||||
|
||||
const AVAILABILITY_ENDPOINT =
|
||||
@@ -213,7 +214,9 @@ function parseSlot(value: unknown, enrollments: ParsedEnrollment[]): BookingSlot
|
||||
|
||||
if (
|
||||
typeof slot.txtClassroomUid !== 'string' ||
|
||||
slot.txtClassroomUid.trim() === '' ||
|
||||
typeof slot.classRoomName !== 'string' ||
|
||||
slot.classRoomName.trim() === '' ||
|
||||
typeof slot.beginDatetime !== 'string' ||
|
||||
typeof slot.endDatetime !== 'string' ||
|
||||
startTime === null ||
|
||||
@@ -245,7 +248,7 @@ function parseSlot(value: unknown, enrollments: ParsedEnrollment[]): BookingSlot
|
||||
: []
|
||||
|
||||
return {
|
||||
id: `${slot.txtClassroomUid}:${slotBeginDatetime}`,
|
||||
id: `${slot.txtClassroomUid}:${slotBeginDatetime}:${slotEndDatetime}`,
|
||||
courtLabel: slot.classRoomName,
|
||||
startTime,
|
||||
endTime,
|
||||
@@ -355,32 +358,42 @@ function createWriteHeaders(settings: CourtRuntimeSettings) {
|
||||
}
|
||||
|
||||
export function buildCreateBookingRequestBody(
|
||||
slot: BookingSlot
|
||||
input: BookingSlot | BookingSlot[]
|
||||
) {
|
||||
if (!slot.providerReference) throw new Error('时段缺少下单引用信息')
|
||||
const slots = sortSlots(Array.isArray(input) ? input : [input])
|
||||
if (slots.length === 0 || slots.some((slot) => !slot.providerReference)) {
|
||||
throw new Error('时段缺少下单引用信息')
|
||||
}
|
||||
if (!isContinuousSingleCourtSequence(slots)) {
|
||||
throw new Error('下单时段不是同一场地的连续区间')
|
||||
}
|
||||
|
||||
return {
|
||||
userId: Number(DEFAULT_VENUE_ID),
|
||||
projectType: 0,
|
||||
classroomItems: [{
|
||||
classroomUid: slot.providerReference.classroomUid,
|
||||
beginDatetime: slot.providerReference.beginDatetime,
|
||||
endDatetime: slot.providerReference.endDatetime,
|
||||
classroomItems: slots.map((slot) => ({
|
||||
classroomUid: slot.providerReference!.classroomUid,
|
||||
beginDatetime: slot.providerReference!.beginDatetime,
|
||||
endDatetime: slot.providerReference!.endDatetime,
|
||||
peopleNum: 1
|
||||
}],
|
||||
})),
|
||||
remark: '',
|
||||
combinationPayments: [{
|
||||
paymentMethod: 900,
|
||||
cost: slot.priceCents / 100
|
||||
cost: slots.reduce((total, slot) => total + slot.priceCents, 0) / 100
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCreateBookingResponse(
|
||||
payload: unknown,
|
||||
slot: BookingSlot,
|
||||
input: BookingSlot | BookingSlot[],
|
||||
courtId: string
|
||||
): AdapterBookingResult {
|
||||
const slots = sortSlots(Array.isArray(input) ? input : [input])
|
||||
if (slots.length === 0) {
|
||||
throw new ProviderMutationError('下单时段为空', 'rejected')
|
||||
}
|
||||
if (!isRecord(payload)) {
|
||||
throw new ProviderMutationError('球场下单接口响应格式错误', 'uncertain')
|
||||
}
|
||||
@@ -410,10 +423,10 @@ export function parseCreateBookingResponse(
|
||||
courtId,
|
||||
orderId: result.apptUid,
|
||||
venueAppointmentIds: result.venueAppointUids,
|
||||
amountCents: slot.priceCents,
|
||||
courtLabel: slot.courtLabel,
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
amountCents: slots.reduce((total, slot) => total + slot.priceCents, 0),
|
||||
courtLabel: slots[0]!.courtLabel,
|
||||
startTime: slots[0]!.startTime,
|
||||
endTime: slots.at(-1)!.endTime,
|
||||
expiresAt: result.script.expireTime,
|
||||
status: 'pending-payment'
|
||||
},
|
||||
@@ -422,15 +435,20 @@ export function parseCreateBookingResponse(
|
||||
}
|
||||
|
||||
async function createBooking(
|
||||
slot: BookingSlot,
|
||||
slots: BookingSlot[],
|
||||
settings: CourtRuntimeSettings
|
||||
): Promise<AdapterBookingResult> {
|
||||
if (slot.status !== 'available' && slot.status !== 'limited') {
|
||||
if (slots.some((slot) => slot.status !== 'available' && slot.status !== 'limited')) {
|
||||
throw new Error('该时段当前不可预约')
|
||||
}
|
||||
if (slot.enrollment) throw new Error('活动时段不能通过普通场地接口下单')
|
||||
if (slots.some((slot) => slot.enrollment)) {
|
||||
throw new Error('活动时段不能通过普通场地接口下单')
|
||||
}
|
||||
if (!isContinuousSingleCourtSequence(slots)) {
|
||||
throw new Error('下单时段不是同一场地的连续区间')
|
||||
}
|
||||
const headers = createWriteHeaders(settings)
|
||||
const requestBody = buildCreateBookingRequestBody(slot)
|
||||
const requestBody = buildCreateBookingRequestBody(slots)
|
||||
logDevRequest('booking.request', {
|
||||
url: CREATE_BOOKING_ENDPOINT,
|
||||
method: 'POST',
|
||||
@@ -482,7 +500,7 @@ async function createBooking(
|
||||
'rejected'
|
||||
)
|
||||
}
|
||||
return parseCreateBookingResponse(payload, slot, settings.courtId)
|
||||
return parseCreateBookingResponse(payload, slots, settings.courtId)
|
||||
}
|
||||
|
||||
async function cancelBooking(settings: CourtRuntimeSettings): Promise<void> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PendingBookingStore } from '../bookings/PendingBookingStore'
|
||||
import { AdapterRegistry } from './registry'
|
||||
|
||||
const courtId = 'fansibote-fuzhong'
|
||||
const slotId = '1775556146897330236:2026-07-16 08:00:00'
|
||||
const slotId = '1775556146897330236:2026-07-16 08:00:00:2026-07-16 08:59:00'
|
||||
|
||||
function availabilityPayload() {
|
||||
return {
|
||||
@@ -82,6 +82,145 @@ describe('AdapterRegistry 预约状态机', () => {
|
||||
await expect(registry.getPendingBooking(courtId)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it('重新校验同场连续时段并作为一笔组合订单下单', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'tennis-book-registry-sequence-'))
|
||||
const settingsStore = new CourtSettingsStore(join(directory, 'settings.json'))
|
||||
await settingsStore.update({ courtId, visitorId: 'visitor-token' })
|
||||
const registry = new AdapterRegistry(
|
||||
settingsStore,
|
||||
new PendingBookingStore(join(directory, 'pending.json'))
|
||||
)
|
||||
const secondSlotId = '1775556146897330236:2026-07-16 09:00:00:2026-07-16 09:59:00'
|
||||
const fetchMock = vi.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
successed: true,
|
||||
result: {
|
||||
slots: [
|
||||
...availabilityPayload().result.slots,
|
||||
{
|
||||
...availabilityPayload().result.slots[0],
|
||||
beginDatetime: '2026-07-16 09:00:00',
|
||||
endDatetime: '2026-07-16 09:59:00',
|
||||
cost: 90
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
successed: true,
|
||||
result: {
|
||||
apptUid: 'order-sequence',
|
||||
script: { expireTime: '2026-07-16 08:10:00' },
|
||||
venueAppointUids: ['order-sequence']
|
||||
}
|
||||
})
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const booking = await registry.createBooking({
|
||||
courtId,
|
||||
date: '2026-07-16',
|
||||
slotIds: [slotId, secondSlotId],
|
||||
expectedPriceCents: 17000,
|
||||
expectedStartTime: '08:00',
|
||||
expectedEndTime: '10:00'
|
||||
})
|
||||
|
||||
expect(booking).toEqual(expect.objectContaining({
|
||||
amountCents: 17000,
|
||||
startTime: '08:00',
|
||||
endTime: '09:59'
|
||||
}))
|
||||
const writeBody = JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))
|
||||
expect(writeBody.classroomItems).toHaveLength(2)
|
||||
expect(writeBody.combinationPayments).toEqual([{ paymentMethod: 900, cost: 170 }])
|
||||
})
|
||||
|
||||
it('组合时段来自不同物理场地时拒绝发送下单请求', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'tennis-book-registry-cross-court-'))
|
||||
const settingsStore = new CourtSettingsStore(join(directory, 'settings.json'))
|
||||
await settingsStore.update({ courtId, visitorId: 'visitor-token' })
|
||||
const registry = new AdapterRegistry(
|
||||
settingsStore,
|
||||
new PendingBookingStore(join(directory, 'pending.json'))
|
||||
)
|
||||
const secondSlotId = '1775556146897330999:2026-07-16 09:00:00:2026-07-16 09:59:00'
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
successed: true,
|
||||
result: {
|
||||
slots: [
|
||||
...availabilityPayload().result.slots,
|
||||
{
|
||||
...availabilityPayload().result.slots[0],
|
||||
txtClassroomUid: '1775556146897330999',
|
||||
classRoomName: '2号风雨场',
|
||||
beginDatetime: '2026-07-16 09:00:00',
|
||||
endDatetime: '2026-07-16 09:59:00'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(registry.createBooking({
|
||||
courtId,
|
||||
date: '2026-07-16',
|
||||
slotIds: [slotId, secondSlotId],
|
||||
expectedPriceCents: 16000,
|
||||
expectedStartTime: '08:00',
|
||||
expectedEndTime: '10:00'
|
||||
})).rejects.toThrow('不是同一场地')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('写前重拉后的组合不再完整覆盖预期范围时拒绝下单', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'tennis-book-registry-range-drift-'))
|
||||
const settingsStore = new CourtSettingsStore(join(directory, 'settings.json'))
|
||||
await settingsStore.update({ courtId, visitorId: 'visitor-token' })
|
||||
const registry = new AdapterRegistry(
|
||||
settingsStore,
|
||||
new PendingBookingStore(join(directory, 'pending.json'))
|
||||
)
|
||||
const secondSlotId = '1775556146897330236:2026-07-16 09:00:00:2026-07-16 09:59:00'
|
||||
const fetchMock = vi.fn().mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
successed: true,
|
||||
result: {
|
||||
slots: [
|
||||
...availabilityPayload().result.slots,
|
||||
{
|
||||
...availabilityPayload().result.slots[0],
|
||||
beginDatetime: '2026-07-16 09:00:00',
|
||||
endDatetime: '2026-07-16 09:59:00'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
await expect(registry.createBooking({
|
||||
courtId,
|
||||
date: '2026-07-16',
|
||||
slotIds: [slotId, secondSlotId],
|
||||
expectedPriceCents: 16000,
|
||||
expectedStartTime: '08:00',
|
||||
expectedEndTime: '11:00'
|
||||
})).rejects.toThrow('不能完整覆盖')
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('在主进程串行化并发下单,只允许生成一个锁场订单', async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), 'tennis-book-concurrent-'))
|
||||
const settingsStore = new CourtSettingsStore(join(directory, 'settings.json'))
|
||||
|
||||
@@ -8,9 +8,18 @@ import type { CourtSettingsStore } from '../settings/CourtSettingsStore'
|
||||
import type { PendingBookingStore } from '../bookings/PendingBookingStore'
|
||||
import { fansiboteFuzhongAdapter } from './fansibote-fuzhong/adapter'
|
||||
import { ProviderMutationError, type CourtAdapter } from './types'
|
||||
import {
|
||||
isCompleteContinuousSequence,
|
||||
isContinuousSingleCourtSequence,
|
||||
sortSlots
|
||||
} from '../bookings/slotSequence'
|
||||
|
||||
const adapters: CourtAdapter[] = [fansiboteFuzhongAdapter]
|
||||
|
||||
interface ParsedCreateBookingInput extends Omit<CreateBookingInput, 'slotId' | 'slotIds'> {
|
||||
slotIds: string[]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -107,7 +116,7 @@ export class AdapterRegistry {
|
||||
return pending
|
||||
}
|
||||
|
||||
private async createBookingExclusive(parsedInput: CreateBookingInput) {
|
||||
private async createBookingExclusive(parsedInput: ParsedCreateBookingInput) {
|
||||
if (await this.pendingBookingStore.get(parsedInput.courtId)) {
|
||||
throw new Error('当前球场已有待支付订单,请先取消释放')
|
||||
}
|
||||
@@ -122,13 +131,42 @@ export class AdapterRegistry {
|
||||
courtId: parsedInput.courtId,
|
||||
date: parsedInput.date
|
||||
})
|
||||
const slot = availability.slots.find((candidate) => candidate.id === parsedInput.slotId)
|
||||
if (!slot) throw new Error('目标时段已不存在,请刷新后重试')
|
||||
if (slot.status !== 'available' && slot.status !== 'limited') {
|
||||
throw new Error('目标时段已不可预约,请刷新列表')
|
||||
const selectedSlots = parsedInput.slotIds.map((slotId) =>
|
||||
availability.slots.filter((candidate) => candidate.id === slotId)
|
||||
)
|
||||
if (selectedSlots.some((matches) => matches.length !== 1)) {
|
||||
throw new Error('目标连续时段已不完整,请刷新后重试')
|
||||
}
|
||||
if (slot.enrollment) throw new Error('活动时段不能通过普通场地接口下单')
|
||||
if (slot.priceCents !== parsedInput.expectedPriceCents) {
|
||||
const slots = sortSlots(selectedSlots.map((matches) => matches[0]!))
|
||||
if (slots.some((slot) => slot.status !== 'available' && slot.status !== 'limited')) {
|
||||
throw new Error('目标连续时段已不可预约,请刷新列表')
|
||||
}
|
||||
if (slots.some((slot) => slot.enrollment)) {
|
||||
throw new Error('活动时段不能通过普通场地接口下单')
|
||||
}
|
||||
if (slots.some((slot) =>
|
||||
!slot.providerReference ||
|
||||
slot.providerReference.beginDatetime.slice(0, 10) !== parsedInput.date ||
|
||||
slot.providerReference.endDatetime.slice(0, 10) !== parsedInput.date
|
||||
)) {
|
||||
throw new Error('目标时段日期与预约日期不一致')
|
||||
}
|
||||
if (!isContinuousSingleCourtSequence(slots)) {
|
||||
throw new Error('目标时段不是同一场地的连续区间')
|
||||
}
|
||||
if (
|
||||
parsedInput.expectedStartTime &&
|
||||
parsedInput.expectedEndTime &&
|
||||
!isCompleteContinuousSequence(
|
||||
slots,
|
||||
parsedInput.expectedStartTime,
|
||||
parsedInput.expectedEndTime
|
||||
)
|
||||
) {
|
||||
throw new Error('目标时段已不能完整覆盖监控时间范围')
|
||||
}
|
||||
const totalPriceCents = slots.reduce((total, slot) => total + slot.priceCents, 0)
|
||||
if (totalPriceCents !== parsedInput.expectedPriceCents) {
|
||||
throw new Error('价格已变化,请刷新列表并重新确认金额')
|
||||
}
|
||||
adapter.preflightWriteSettings?.(settings)
|
||||
@@ -137,17 +175,17 @@ export class AdapterRegistry {
|
||||
courtId: parsedInput.courtId,
|
||||
orderId: `uncertain:${Date.now()}`,
|
||||
venueAppointmentIds: [],
|
||||
amountCents: slot.priceCents,
|
||||
courtLabel: slot.courtLabel,
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
amountCents: totalPriceCents,
|
||||
courtLabel: slots[0]!.courtLabel,
|
||||
startTime: slots[0]!.startTime,
|
||||
endTime: slots.at(-1)!.endTime,
|
||||
status: 'submitting-uncertain' as const
|
||||
}
|
||||
await this.pendingBookingStore.set(uncertainBooking)
|
||||
|
||||
let result
|
||||
try {
|
||||
result = await adapter.createBooking(slot, settings)
|
||||
result = await adapter.createBooking(slots, settings)
|
||||
} catch (error) {
|
||||
if (error instanceof ProviderMutationError && error.outcome === 'rejected') {
|
||||
await this.pendingBookingStore.clear(parsedInput.courtId)
|
||||
@@ -220,12 +258,29 @@ export class AdapterRegistry {
|
||||
this.releaseConfirmedCourts.delete(input.courtId)
|
||||
}
|
||||
|
||||
private parseCreateBookingInput(input: unknown): CreateBookingInput {
|
||||
private parseCreateBookingInput(input: unknown): ParsedCreateBookingInput {
|
||||
const hasSingleSlot = isRecord(input) && typeof input.slotId === 'string'
|
||||
const slotIds = isRecord(input) && Array.isArray(input.slotIds)
|
||||
? input.slotIds
|
||||
: null
|
||||
const hasSlotList = slotIds !== null
|
||||
if (
|
||||
!isRecord(input) ||
|
||||
typeof input.courtId !== 'string' ||
|
||||
typeof input.date !== 'string' ||
|
||||
typeof input.slotId !== 'string' ||
|
||||
hasSingleSlot === hasSlotList ||
|
||||
(hasSlotList && (
|
||||
slotIds.length === 0 ||
|
||||
!slotIds.every((slotId) => typeof slotId === 'string') ||
|
||||
new Set(slotIds).size !== slotIds.length
|
||||
)) ||
|
||||
(input.expectedStartTime !== undefined && typeof input.expectedStartTime !== 'string') ||
|
||||
(input.expectedEndTime !== undefined && typeof input.expectedEndTime !== 'string') ||
|
||||
((input.expectedStartTime === undefined) !== (input.expectedEndTime === undefined)) ||
|
||||
(hasSlotList && slotIds.length > 1 && (
|
||||
typeof input.expectedStartTime !== 'string' ||
|
||||
typeof input.expectedEndTime !== 'string'
|
||||
)) ||
|
||||
typeof input.expectedPriceCents !== 'number' ||
|
||||
!Number.isSafeInteger(input.expectedPriceCents) ||
|
||||
input.expectedPriceCents < 0
|
||||
@@ -235,8 +290,14 @@ export class AdapterRegistry {
|
||||
return {
|
||||
courtId: input.courtId,
|
||||
date: input.date,
|
||||
slotId: input.slotId,
|
||||
expectedPriceCents: input.expectedPriceCents
|
||||
slotIds: hasSingleSlot ? [input.slotId as string] : [...slotIds as string[]],
|
||||
expectedPriceCents: input.expectedPriceCents,
|
||||
...(typeof input.expectedStartTime === 'string'
|
||||
? {
|
||||
expectedStartTime: input.expectedStartTime,
|
||||
expectedEndTime: input.expectedEndTime as string
|
||||
}
|
||||
: {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export interface CourtAdapter {
|
||||
getAvailability(query: AvailabilityQuery): Promise<AvailabilityDay>
|
||||
preflightWriteSettings?(settings: CourtRuntimeSettings): void
|
||||
createBooking?(
|
||||
slot: BookingSlot,
|
||||
slots: BookingSlot[],
|
||||
settings: CourtRuntimeSettings
|
||||
): Promise<AdapterBookingResult>
|
||||
cancelBooking?(settings: CourtRuntimeSettings): Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user