@fayz-ai/plugin-agenda 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/bridges/create-financial-bridge.d.ts +1 -1
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/bridges/financial-bridge.d.ts.map +1 -0
- package/dist/{chunk-Q2PAWBPR.js → chunk-76ZOHRBM.js} +2 -2
- package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
- package/dist/components/PaymentStatusBadge.d.ts +1 -1
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
- package/dist/public/hooks/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +3 -3
- package/dist/public/index.js.map +1 -1
- package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
- package/dist/public/lib/format.d.ts.map +1 -0
- package/dist/public/views/BookingWidget.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
- package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
- package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
- package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
- package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/styles/calendar-styles.d.ts.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
- package/dist/views/AgendaPage.d.ts.map +1 -0
- package/package.json +6 -9
- package/dist/AgendaContext.d.ts.map +0 -1
- package/dist/AgendaPage.d.ts.map +0 -1
- package/dist/calendar-styles.d.ts.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/public/BookingWidget.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/format.d.ts.map +0 -1
- package/dist/public/hooks.d.ts.map +0 -1
- package/dist/public/steps/ContactStep.d.ts.map +0 -1
- package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
- package/dist/public/steps/OverviewStep.d.ts.map +0 -1
- package/dist/public/steps/PaymentStep.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/AgendaContext.tsx +0 -11
- package/src/AgendaPage.tsx +0 -33
- package/src/bridges/create-financial-bridge.ts +0 -296
- package/src/calendar-styles.css +0 -245
- package/src/calendar-styles.ts +0 -285
- package/src/components/AgendaGeneralSettings.tsx +0 -86
- package/src/components/AgendaMobileList.tsx +0 -103
- package/src/components/AppointmentModal.tsx +0 -1050
- package/src/components/AppointmentPopover.tsx +0 -231
- package/src/components/BookingPaymentPanel.tsx +0 -394
- package/src/components/CalendarSidebar.tsx +0 -119
- package/src/components/CalendarToolbar.tsx +0 -86
- package/src/components/EventContextMenu.tsx +0 -125
- package/src/components/FloatingPanel.tsx +0 -123
- package/src/components/MiniCalendar.tsx +0 -120
- package/src/components/PaymentStatusBadge.tsx +0 -62
- package/src/components/StatusBadge.tsx +0 -23
- package/src/config.ts +0 -368
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -344
- package/src/data/supabase.ts +0 -636
- package/src/data/types.ts +0 -37
- package/src/financial-bridge.ts +0 -96
- package/src/hooks/useAgendaSettings.ts +0 -17
- package/src/index.ts +0 -297
- package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
- package/src/integrations/google-calendar/connector.ts +0 -20
- package/src/integrations/google-calendar/connectorDef.tsx +0 -185
- package/src/integrations/google-calendar/data/supabase.ts +0 -170
- package/src/integrations/google-calendar/data/tables.ts +0 -12
- package/src/integrations/google-calendar/index.ts +0 -45
- package/src/integrations/google-calendar/integration.test.ts +0 -199
- package/src/integrations/google-calendar/mapping.ts +0 -183
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
- package/src/integrations/google-calendar/types.ts +0 -142
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -220
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -220
- package/src/migrations/000b_v_bookings_rename.sql +0 -21
- package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
- package/src/migrations/001_public_booking.sql +0 -296
- package/src/migrations/002_v_bookings_compat.sql +0 -14
- package/src/migrations/003_public_client_kind.sql +0 -175
- package/src/migrations/004_public_booking_assignee.sql +0 -176
- package/src/migrations/005_agent_rpcs.sql +0 -168
- package/src/migrations/006_agent_rpc_localtime.sql +0 -181
- package/src/migrations/007_schedule_exception_precedence.sql +0 -290
- package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
- package/src/migrations/009_public_booking_location.sql +0 -162
- package/src/migrations/010_public_service_curation.sql +0 -31
- package/src/migrations/011_default_assignee.sql +0 -167
- package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
- package/src/migrations/013_normalize_v_appointments.sql +0 -199
- package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
- package/src/migrations/index.ts +0 -2377
- package/src/public/BookingWidget.tsx +0 -324
- package/src/public/components/BrandHeader.tsx +0 -27
- package/src/public/components/ServiceCard.tsx +0 -39
- package/src/public/components/SidebarExtras.tsx +0 -30
- package/src/public/components/StepRailItem.tsx +0 -27
- package/src/public/components/SuccessPanel.tsx +0 -27
- package/src/public/components/SummaryRail.tsx +0 -90
- package/src/public/components/defaults.ts +0 -14
- package/src/public/context.tsx +0 -103
- package/src/public/createPublicBookingPlugin.ts +0 -281
- package/src/public/data.supabase.ts +0 -97
- package/src/public/data.ts +0 -105
- package/src/public/format.ts +0 -96
- package/src/public/hooks.ts +0 -110
- package/src/public/index.ts +0 -69
- package/src/public/steps/ContactStep.tsx +0 -184
- package/src/public/steps/DateTimeStep.tsx +0 -138
- package/src/public/steps/OverviewStep.tsx +0 -81
- package/src/public/steps/PaymentStep.tsx +0 -180
- package/src/public/types.ts +0 -250
- package/src/registries.ts +0 -35
- package/src/store.ts +0 -311
- package/src/types.ts +0 -273
- package/src/views/CalendarView.tsx +0 -1112
- package/src/views/ConfirmationsView.tsx +0 -155
- /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
- /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
- /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
// Google Calendar as a unified ConnectorDefinition.
|
|
2
|
-
//
|
|
3
|
-
// Surfaces inside plugin-agenda's settings → Integrations tab (the host),
|
|
4
|
-
// rendered by the shared ConnectorsHub. The hub handles the OAuth connect/
|
|
5
|
-
// disconnect; the ExtraPanel below adds the calendar picker, manual sync and
|
|
6
|
-
// run history (only meaningful once connected).
|
|
7
|
-
import React, { useEffect, useState } from 'react'
|
|
8
|
-
import { RefreshCw, History, CheckCircle2, AlertCircle, Loader2, CalendarCog, CalendarPlus } from 'lucide-react'
|
|
9
|
-
import { Button, toast } from '@fayz-ai/ui'
|
|
10
|
-
import type { ConnectorDefinition } from '@fayz-ai/core'
|
|
11
|
-
import { createGoogleCalendarProvider } from './data/supabase'
|
|
12
|
-
import type { CalendarChannel, CalendarSyncLogEntry } from './types'
|
|
13
|
-
import { ChannelMappingList } from './components/ChannelMappingList'
|
|
14
|
-
import { ImportEventsWizard } from './components/ImportEventsWizard'
|
|
15
|
-
|
|
16
|
-
const provider = createGoogleCalendarProvider()
|
|
17
|
-
|
|
18
|
-
/** Channels Google is allowed to write INTO the agenda from (gate the import wizard). */
|
|
19
|
-
function inboundChannels(channels: CalendarChannel[]): CalendarChannel[] {
|
|
20
|
-
return channels.filter(
|
|
21
|
-
(c) => c.isActive && (c.direction === 'inbound' || c.direction === 'bidirectional'),
|
|
22
|
-
)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function GoogleCalendarExtraPanel() {
|
|
26
|
-
const [connected, setConnected] = useState(false)
|
|
27
|
-
const [lastSyncAt, setLastSyncAt] = useState<string | undefined>()
|
|
28
|
-
const [syncing, setSyncing] = useState(false)
|
|
29
|
-
const [log, setLog] = useState<CalendarSyncLogEntry[]>([])
|
|
30
|
-
const [loaded, setLoaded] = useState(false)
|
|
31
|
-
const [channels, setChannels] = useState<CalendarChannel[]>([])
|
|
32
|
-
// Progressive disclosure: once connected the de-para is opened either on the
|
|
33
|
-
// "Configure calendars" CTA or automatically when channels already exist.
|
|
34
|
-
const [showMapping, setShowMapping] = useState(false)
|
|
35
|
-
const [showWizard, setShowWizard] = useState(false)
|
|
36
|
-
|
|
37
|
-
async function load() {
|
|
38
|
-
const integ = await provider.getIntegration()
|
|
39
|
-
setConnected(!!integ?.connected)
|
|
40
|
-
if (integ) {
|
|
41
|
-
setLastSyncAt(integ.lastSyncAt)
|
|
42
|
-
void provider.getSyncLog().then(setLog)
|
|
43
|
-
void provider.listChannels().then((ch) => {
|
|
44
|
-
setChannels(ch)
|
|
45
|
-
if (ch.length > 0) setShowMapping(true)
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
setLoaded(true)
|
|
49
|
-
}
|
|
50
|
-
useEffect(() => { void load() }, [])
|
|
51
|
-
|
|
52
|
-
// Before connecting there's nothing to configure here — a short hint only.
|
|
53
|
-
if (!loaded) return null
|
|
54
|
-
if (!connected) {
|
|
55
|
-
return (
|
|
56
|
-
<p className="text-xs text-muted-foreground border-t pt-4">
|
|
57
|
-
Connect a Google account above to map your calendars and import events.
|
|
58
|
-
</p>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function handleSyncNow() {
|
|
63
|
-
setSyncing(true)
|
|
64
|
-
try {
|
|
65
|
-
const r = await provider.syncNow()
|
|
66
|
-
toast.success(`Synced — ${r.written} updated`)
|
|
67
|
-
void provider.getSyncLog().then(setLog)
|
|
68
|
-
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt))
|
|
69
|
-
} catch (e: any) { toast.error(e?.message ?? 'Sync failed') }
|
|
70
|
-
finally { setSyncing(false) }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const canImport = inboundChannels(channels).length > 0
|
|
74
|
-
|
|
75
|
-
return (
|
|
76
|
-
<div className="space-y-5 border-t pt-4">
|
|
77
|
-
{/* 1 — De-para (mapping). CTA first when nothing is configured yet. */}
|
|
78
|
-
<section className="space-y-2">
|
|
79
|
-
<div className="flex items-center gap-2">
|
|
80
|
-
<CalendarCog className="h-4 w-4 text-muted-foreground" />
|
|
81
|
-
<h4 className="text-sm font-semibold">Calendars</h4>
|
|
82
|
-
</div>
|
|
83
|
-
{showMapping ? (
|
|
84
|
-
<ChannelMappingList provider={provider} onChannelsChange={setChannels} />
|
|
85
|
-
) : (
|
|
86
|
-
<div className="rounded-md border border-dashed px-4 py-5 text-center space-y-2">
|
|
87
|
-
<p className="text-sm text-muted-foreground">
|
|
88
|
-
Map each Google calendar to what it syncs — a professional, service, location, or your
|
|
89
|
-
whole agenda.
|
|
90
|
-
</p>
|
|
91
|
-
<Button variant="outline" size="sm" onClick={() => setShowMapping(true)}>
|
|
92
|
-
<CalendarCog className="h-3.5 w-3.5" /> Configure calendars
|
|
93
|
-
</Button>
|
|
94
|
-
</div>
|
|
95
|
-
)}
|
|
96
|
-
</section>
|
|
97
|
-
|
|
98
|
-
{/* 2 — Import future events (only meaningful once an inbound channel exists). */}
|
|
99
|
-
{showMapping && (
|
|
100
|
-
<section className="space-y-2">
|
|
101
|
-
{showWizard ? (
|
|
102
|
-
<ImportEventsWizard
|
|
103
|
-
provider={provider}
|
|
104
|
-
channels={inboundChannels(channels)}
|
|
105
|
-
onClose={() => setShowWizard(false)}
|
|
106
|
-
onImported={() => {
|
|
107
|
-
void provider.getSyncLog().then(setLog)
|
|
108
|
-
void provider.getIntegration().then((i) => setLastSyncAt(i?.lastSyncAt))
|
|
109
|
-
}}
|
|
110
|
-
/>
|
|
111
|
-
) : (
|
|
112
|
-
<Button
|
|
113
|
-
variant="outline"
|
|
114
|
-
size="sm"
|
|
115
|
-
disabled={!canImport}
|
|
116
|
-
title={canImport ? undefined : 'Enable an importing calendar first'}
|
|
117
|
-
onClick={() => setShowWizard(true)}
|
|
118
|
-
>
|
|
119
|
-
<CalendarPlus className="h-3.5 w-3.5" /> Import future events
|
|
120
|
-
</Button>
|
|
121
|
-
)}
|
|
122
|
-
</section>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
{/* 3 — Status: last sync + manual pull + run history. */}
|
|
126
|
-
<section className="space-y-2">
|
|
127
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
128
|
-
<Button variant="outline" size="sm" onClick={handleSyncNow} disabled={syncing}>
|
|
129
|
-
<RefreshCw className={`h-3.5 w-3.5 ${syncing ? 'animate-spin' : ''}`} /> Sync now
|
|
130
|
-
</Button>
|
|
131
|
-
{lastSyncAt && (
|
|
132
|
-
<span className="text-[11px] text-muted-foreground">
|
|
133
|
-
Last sync: {new Date(lastSyncAt).toLocaleString()}
|
|
134
|
-
</span>
|
|
135
|
-
)}
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{log.length > 0 && (
|
|
139
|
-
<div className="space-y-2">
|
|
140
|
-
<div className="flex items-center gap-2">
|
|
141
|
-
<History className="h-4 w-4 text-muted-foreground" />
|
|
142
|
-
<h4 className="text-sm font-semibold">History</h4>
|
|
143
|
-
</div>
|
|
144
|
-
<div className="rounded-md border divide-y text-sm">
|
|
145
|
-
{log.map((r) => (
|
|
146
|
-
<div key={r.id} className="flex items-center gap-3 px-3 py-2">
|
|
147
|
-
<span className="text-muted-foreground text-xs">{new Date(r.createdAt).toLocaleString()}</span>
|
|
148
|
-
<span className="text-[10px] uppercase tracking-wide px-1 py-px rounded bg-muted text-muted-foreground">{r.direction}</span>
|
|
149
|
-
<span className="ml-auto text-xs">{r.written} written / {r.fetched} fetched</span>
|
|
150
|
-
{r.status === 'success'
|
|
151
|
-
? <CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
|
152
|
-
: <AlertCircle className="h-3.5 w-3.5 text-warning" />}
|
|
153
|
-
</div>
|
|
154
|
-
))}
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
)}
|
|
158
|
-
</section>
|
|
159
|
-
</div>
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export const googleCalendarConnectorDef: ConnectorDefinition = {
|
|
164
|
-
id: 'google-calendar',
|
|
165
|
-
hostPluginId: 'agenda',
|
|
166
|
-
name: 'Google Calendar',
|
|
167
|
-
description: 'Two-way sync between your bookings and Google Calendar.',
|
|
168
|
-
icon: 'Calendar',
|
|
169
|
-
authKind: 'oauth',
|
|
170
|
-
category: 'scheduling',
|
|
171
|
-
direction: 'outbound',
|
|
172
|
-
// Sync only, and deliberately: the agenda has to exist inside the app — to be
|
|
173
|
-
// listed, filtered, reported on, and read when Google is unreachable. An
|
|
174
|
-
// agentic plane would answer questions but leave the calendar screen empty.
|
|
175
|
-
planes: ['sync'],
|
|
176
|
-
entities: ['booking'],
|
|
177
|
-
docsUrl: 'https://support.google.com/calendar',
|
|
178
|
-
async getStatus() {
|
|
179
|
-
const integ = await provider.getIntegration()
|
|
180
|
-
return { connected: !!integ?.connected }
|
|
181
|
-
},
|
|
182
|
-
startOAuth: (redirectTo) => provider.getConnectUrl(redirectTo),
|
|
183
|
-
disconnect: () => provider.disconnect(),
|
|
184
|
-
ExtraPanel: GoogleCalendarExtraPanel,
|
|
185
|
-
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
// Google Calendar integration (control plane) — Supabase client.
|
|
2
|
-
//
|
|
3
|
-
// Thin client. Split of responsibilities:
|
|
4
|
-
// • anything that needs the OAuth client secret or a Google API call goes
|
|
5
|
-
// through the google-calendar-sync edge function (invoke);
|
|
6
|
-
// • plain tenant-scoped reads/writes (integration row, channels, log) hit the
|
|
7
|
-
// tables directly under RLS.
|
|
8
|
-
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
|
|
9
|
-
import { channelRowToChannel } from '../mapping'
|
|
10
|
-
import { GCAL_TABLES } from './tables'
|
|
11
|
-
import type {
|
|
12
|
-
CalendarIntegration,
|
|
13
|
-
CalendarSyncLogEntry,
|
|
14
|
-
CalendarChannel,
|
|
15
|
-
ExternalEventPreview,
|
|
16
|
-
GoogleCalendarListEntry,
|
|
17
|
-
ImportEventsResult,
|
|
18
|
-
} from '../types'
|
|
19
|
-
|
|
20
|
-
const FUNCTION = 'google-calendar-sync'
|
|
21
|
-
|
|
22
|
-
function sb() {
|
|
23
|
-
const supabase = getSupabaseClientOptional() as any
|
|
24
|
-
if (!supabase) throw new Error('Supabase not initialized')
|
|
25
|
-
return supabase
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Snake_case patch for plg_calendar_channels writes (RLS-scoped). */
|
|
29
|
-
export interface CalendarChannelInput {
|
|
30
|
-
id?: string
|
|
31
|
-
integrationId?: string
|
|
32
|
-
googleCalendarId: string
|
|
33
|
-
summary?: string | null
|
|
34
|
-
color?: string | null
|
|
35
|
-
direction?: CalendarChannel['direction']
|
|
36
|
-
targetKind?: CalendarChannel['targetKind']
|
|
37
|
-
targetId?: string | null
|
|
38
|
-
importMode?: CalendarChannel['importMode']
|
|
39
|
-
isActive?: boolean
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface GoogleCalendarProvider {
|
|
43
|
-
getIntegration(): Promise<CalendarIntegration | null>
|
|
44
|
-
/** Start the OAuth connect flow — returns the Google consent URL to redirect to. */
|
|
45
|
-
getConnectUrl(redirectTo?: string): Promise<string>
|
|
46
|
-
setCalendar(calendarId: string): Promise<void>
|
|
47
|
-
disconnect(): Promise<void>
|
|
48
|
-
/** Manually pull external changes now (inbound). */
|
|
49
|
-
syncNow(): Promise<{ fetched: number; written: number; discovered?: number }>
|
|
50
|
-
getSyncLog(): Promise<CalendarSyncLogEntry[]>
|
|
51
|
-
// --- smart sync: multi-calendar channels + import wizard ---
|
|
52
|
-
/** List the user's Google calendars for the de-para picker (needs the API → function). */
|
|
53
|
-
listGoogleCalendars(): Promise<GoogleCalendarListEntry[]>
|
|
54
|
-
/** Read this tenant's channels (RLS). */
|
|
55
|
-
listChannels(): Promise<CalendarChannel[]>
|
|
56
|
-
/** Create/update a channel (RLS upsert on integration_id + google_calendar_id). */
|
|
57
|
-
saveChannel(input: CalendarChannelInput): Promise<void>
|
|
58
|
-
/** Delete a channel by id (RLS). */
|
|
59
|
-
deleteChannel(id: string): Promise<void>
|
|
60
|
-
/** Preview future events not yet linked to an appointment (import wizard). */
|
|
61
|
-
listExternalEvents(params?: { channelId?: string; timeMin?: string }): Promise<ExternalEventPreview[]>
|
|
62
|
-
/** Import selected external events as appointments. */
|
|
63
|
-
importEvents(items: { channelId: string; eventId: string }[]): Promise<ImportEventsResult>
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function createGoogleCalendarProvider(): GoogleCalendarProvider {
|
|
67
|
-
return {
|
|
68
|
-
async getIntegration() {
|
|
69
|
-
const { data } = await sb().from(GCAL_TABLES.integrations).select('*').eq('provider', 'google').limit(1).maybeSingle()
|
|
70
|
-
if (!data) return null
|
|
71
|
-
return {
|
|
72
|
-
id: data.id,
|
|
73
|
-
provider: data.provider,
|
|
74
|
-
calendarId: data.calendar_id ?? 'primary',
|
|
75
|
-
active: data.active ?? true,
|
|
76
|
-
connected: !!data.oauth_refresh_token,
|
|
77
|
-
lastSyncAt: data.last_sync_at ?? undefined,
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
|
|
81
|
-
async getConnectUrl(redirectTo) {
|
|
82
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
83
|
-
body: { action: 'oauth_start', tenantId: getActiveTenantId(), redirectTo: redirectTo ?? window.location.href },
|
|
84
|
-
})
|
|
85
|
-
if (error) throw new Error(error.message)
|
|
86
|
-
return (data as { url: string }).url
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
async setCalendar(calendarId) {
|
|
90
|
-
await sb().from(GCAL_TABLES.integrations).update({ calendar_id: calendarId, updated_at: new Date().toISOString() }).eq('provider', 'google')
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
async disconnect() {
|
|
94
|
-
await sb().from(GCAL_TABLES.integrations).update({ active: false, oauth_refresh_token: null, oauth_access_token: null }).eq('provider', 'google')
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
async syncNow() {
|
|
98
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: 'pull_events', tenantId: getActiveTenantId(), trigger: 'manual' } })
|
|
99
|
-
if (error) throw new Error(error.message)
|
|
100
|
-
return data as { fetched: number; written: number; discovered?: number }
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
async getSyncLog() {
|
|
104
|
-
const { data } = await sb().from(GCAL_TABLES.syncLog).select('*').order('created_at', { ascending: false }).limit(20)
|
|
105
|
-
return (data ?? []).map((r: any) => ({
|
|
106
|
-
id: r.id, direction: r.direction, trigger: r.trigger ?? undefined,
|
|
107
|
-
status: r.status, fetched: r.fetched ?? 0, written: r.written ?? 0,
|
|
108
|
-
error: r.error ?? undefined, createdAt: r.created_at,
|
|
109
|
-
}))
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
async listGoogleCalendars() {
|
|
113
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, { body: { action: 'list_calendars', tenantId: getActiveTenantId() } })
|
|
114
|
-
if (error) throw new Error(error.message)
|
|
115
|
-
return (data as { calendars: GoogleCalendarListEntry[] }).calendars ?? []
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
async listChannels() {
|
|
119
|
-
const { data } = await sb().from(GCAL_TABLES.channels).select('*').order('created_at', { ascending: true })
|
|
120
|
-
return (data ?? []).map(channelRowToChannel)
|
|
121
|
-
},
|
|
122
|
-
|
|
123
|
-
async saveChannel(input) {
|
|
124
|
-
const tenantId = getActiveTenantId()
|
|
125
|
-
// Resolve the integration id when the caller didn't pass one.
|
|
126
|
-
let integrationId = input.integrationId
|
|
127
|
-
if (!integrationId) {
|
|
128
|
-
const { data } = await sb().from(GCAL_TABLES.integrations).select('id').eq('provider', 'google').limit(1).maybeSingle()
|
|
129
|
-
integrationId = data?.id
|
|
130
|
-
}
|
|
131
|
-
const row: Record<string, unknown> = {
|
|
132
|
-
integration_id: integrationId,
|
|
133
|
-
tenant_id: tenantId,
|
|
134
|
-
google_calendar_id: input.googleCalendarId,
|
|
135
|
-
updated_at: new Date().toISOString(),
|
|
136
|
-
}
|
|
137
|
-
if (input.id !== undefined) row.id = input.id
|
|
138
|
-
if (input.summary !== undefined) row.summary = input.summary
|
|
139
|
-
if (input.color !== undefined) row.color = input.color
|
|
140
|
-
if (input.direction !== undefined) row.direction = input.direction
|
|
141
|
-
if (input.targetKind !== undefined) row.target_kind = input.targetKind
|
|
142
|
-
if (input.targetId !== undefined) row.target_id = input.targetId
|
|
143
|
-
if (input.importMode !== undefined) row.import_mode = input.importMode
|
|
144
|
-
if (input.isActive !== undefined) row.is_active = input.isActive
|
|
145
|
-
const { error } = await sb().from(GCAL_TABLES.channels).upsert(row, { onConflict: 'integration_id,google_calendar_id' })
|
|
146
|
-
if (error) throw new Error(error.message)
|
|
147
|
-
},
|
|
148
|
-
|
|
149
|
-
async deleteChannel(id) {
|
|
150
|
-
const { error } = await sb().from(GCAL_TABLES.channels).delete().eq('id', id)
|
|
151
|
-
if (error) throw new Error(error.message)
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
async listExternalEvents(params) {
|
|
155
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
156
|
-
body: { action: 'list_external_events', tenantId: getActiveTenantId(), channelId: params?.channelId, timeMin: params?.timeMin },
|
|
157
|
-
})
|
|
158
|
-
if (error) throw new Error(error.message)
|
|
159
|
-
return (data as { events: ExternalEventPreview[] }).events ?? []
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
async importEvents(items) {
|
|
163
|
-
const { data, error } = await sb().functions.invoke(FUNCTION, {
|
|
164
|
-
body: { action: 'import_events', tenantId: getActiveTenantId(), items },
|
|
165
|
-
})
|
|
166
|
-
if (error) throw new Error(error.message)
|
|
167
|
-
return data as ImportEventsResult
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Canonical DB object names for the Google Calendar connector.
|
|
2
|
-
//
|
|
3
|
-
// Plugin-owned tables carry the platform `plg_<plugin>_` prefix (DATA-MODEL.md
|
|
4
|
-
// Ring 1). Single source of truth so the connector data layer (supabase.ts) and
|
|
5
|
-
// the google-calendar-sync edge function never drift on a table name. The SQL
|
|
6
|
-
// side is owned by ./migrations/001_google_calendar.sql + 002_smart_sync.sql
|
|
7
|
-
// (fresh installs) and 003_plg_rename.sql (legacy-pool rename).
|
|
8
|
-
export const GCAL_TABLES = {
|
|
9
|
-
integrations: 'plg_calendar_integrations',
|
|
10
|
-
channels: 'plg_calendar_channels',
|
|
11
|
-
syncLog: 'plg_calendar_sync_log',
|
|
12
|
-
} as const
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Google Calendar integration — official (in-SDK) addon for plugin-agenda.
|
|
2
|
-
//
|
|
3
|
-
// Settings-only PluginManifest: contributes a "Google Calendar" tab to /settings
|
|
4
|
-
// for connecting an account and running a manual sync. Outbound (booking →
|
|
5
|
-
// Google) is automatic via a DB trigger; inbound (Google → booking) via pg_cron
|
|
6
|
-
// or a Google watch webhook. The data plane is the google-calendar-sync edge
|
|
7
|
-
// function (see ./functions/google-calendar-sync). The booking↔event link lives
|
|
8
|
-
// on public.appointments.metadata.googleCalendarEventId.
|
|
9
|
-
//
|
|
10
|
-
// Graduation path: extract to its own `@fayz-ai/plugin-google-calendar` package.
|
|
11
|
-
import type { PluginManifest } from '@fayz-ai/core'
|
|
12
|
-
import { googleCalendarConnectorDef } from './connectorDef'
|
|
13
|
-
|
|
14
|
-
export { createGoogleCalendarProvider } from './data/supabase'
|
|
15
|
-
export { googleCalendarConnector } from './connector'
|
|
16
|
-
export { googleCalendarConnectorDef } from './connectorDef'
|
|
17
|
-
export {
|
|
18
|
-
bookingToEvent,
|
|
19
|
-
eventToBookingPatch,
|
|
20
|
-
bookingSummary,
|
|
21
|
-
appointmentRowToEvent,
|
|
22
|
-
eventToAppointmentPatch,
|
|
23
|
-
eventTimes,
|
|
24
|
-
resolveTargetChannel,
|
|
25
|
-
channelRowToChannel,
|
|
26
|
-
} from './mapping'
|
|
27
|
-
export * from './types'
|
|
28
|
-
|
|
29
|
-
// Addon plugin: contributes the Google Calendar connector into its host
|
|
30
|
-
// (plugin-agenda). It appears in Agenda settings → Integrations — no top-level
|
|
31
|
-
// settings tab of its own. See packages/saas ConnectorsHub.
|
|
32
|
-
export function createGoogleCalendarPlugin(): PluginManifest {
|
|
33
|
-
return {
|
|
34
|
-
id: 'google-calendar',
|
|
35
|
-
name: 'Google Calendar',
|
|
36
|
-
icon: 'Calendar',
|
|
37
|
-
version: '0.1.0',
|
|
38
|
-
scope: 'addon',
|
|
39
|
-
defaultEnabled: true,
|
|
40
|
-
dependencies: ['agenda'],
|
|
41
|
-
navigation: [],
|
|
42
|
-
routes: [],
|
|
43
|
-
connectors: [googleCalendarConnectorDef],
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
// Integration contract + capability test for the Google Calendar connector.
|
|
2
|
-
//
|
|
3
|
-
// TDD anchor: the connector contract, manifest integrity, and the pure
|
|
4
|
-
// booking↔event mapping are all proven HERE — before/without the deployed edge
|
|
5
|
-
// function. The data-plane behaviors that need a live Google account are written
|
|
6
|
-
// as it.todo specs so the intended contract is documented and pending, not
|
|
7
|
-
// silently missing. See docs/PLUGIN-PATTERNS.md → integration capability.
|
|
8
|
-
import { describe, it, expect } from 'vitest'
|
|
9
|
-
import { assertConnectorContract } from '@fayz-ai/core/testing'
|
|
10
|
-
import { googleCalendarConnector } from './connector'
|
|
11
|
-
import {
|
|
12
|
-
bookingToEvent,
|
|
13
|
-
eventToBookingPatch,
|
|
14
|
-
bookingSummary,
|
|
15
|
-
appointmentRowToEvent,
|
|
16
|
-
eventToAppointmentPatch,
|
|
17
|
-
eventTimes,
|
|
18
|
-
resolveTargetChannel,
|
|
19
|
-
channelRowToChannel,
|
|
20
|
-
} from './mapping'
|
|
21
|
-
import type { CalendarBooking } from '../../types'
|
|
22
|
-
import type { AppointmentRow, CalendarChannel } from './types'
|
|
23
|
-
|
|
24
|
-
function booking(overrides: Partial<CalendarBooking> = {}): CalendarBooking {
|
|
25
|
-
return {
|
|
26
|
-
id: 'b1', tenantId: 't1', kind: 'appointment',
|
|
27
|
-
startsAt: '2026-06-18T13:00:00-03:00', endsAt: '2026-06-18T14:00:00-03:00',
|
|
28
|
-
status: 'scheduled', notes: null, orderId: 'b1', locationId: null, metadata: {},
|
|
29
|
-
clientId: null, clientName: 'Sarah', clientPhone: null, clientEmail: 'sarah@x.com', clientAvatarUrl: null,
|
|
30
|
-
professionalId: null, professionalName: 'Ana', professionalAvatarUrl: null,
|
|
31
|
-
locationName: 'HQ', orderTotal: null, orderStatus: null,
|
|
32
|
-
services: [{ id: 's1', serviceId: 'svc1', name: 'Haircut', durationMinutes: 60, price: 100, assigneeId: null }] as any,
|
|
33
|
-
totalDurationMinutes: 60, createdAt: '', updatedAt: '',
|
|
34
|
-
...overrides,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('google-calendar · connector contract (integrity)', () => {
|
|
39
|
-
it('declares a valid connector descriptor', () => {
|
|
40
|
-
expect(() => assertConnectorContract(googleCalendarConnector)).not.toThrow()
|
|
41
|
-
expect(googleCalendarConnector.capabilities[0].direction).toBe('bidirectional')
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('google-calendar · booking↔event mapping (pure, TDD)', () => {
|
|
46
|
-
it('builds an event title from services + client', () => {
|
|
47
|
-
expect(bookingSummary(booking())).toBe('Haircut — Sarah')
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('maps a booking to a Google event input with start/end', () => {
|
|
51
|
-
const ev = bookingToEvent(booking())
|
|
52
|
-
expect(ev).not.toBeNull()
|
|
53
|
-
expect(ev!.summary).toBe('Haircut — Sarah')
|
|
54
|
-
expect(ev!.start.dateTime).toBe('2026-06-18T13:00:00-03:00')
|
|
55
|
-
expect(ev!.end.dateTime).toBe('2026-06-18T14:00:00-03:00')
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('refuses to map a booking with no end time', () => {
|
|
59
|
-
expect(bookingToEvent(booking({ endsAt: null }))).toBeNull()
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
it('maps an inbound reschedule to a booking patch', () => {
|
|
63
|
-
const patch = eventToBookingPatch({
|
|
64
|
-
id: 'e1', start: { dateTime: '2026-06-18T15:00:00-03:00' }, end: { dateTime: '2026-06-18T16:00:00-03:00' },
|
|
65
|
-
})
|
|
66
|
-
expect(patch.startsAt).toBe('2026-06-18T15:00:00-03:00')
|
|
67
|
-
expect(patch.endsAt).toBe('2026-06-18T16:00:00-03:00')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('maps an inbound cancellation to a cancelled status', () => {
|
|
71
|
-
const patch = eventToBookingPatch({ id: 'e1', status: 'cancelled', start: {}, end: {} })
|
|
72
|
-
expect(patch.status).toBe('cancelled')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('reads an all-day event window (date, no dateTime)', () => {
|
|
76
|
-
const t = eventTimes({ id: 'e1', start: { date: '2026-06-18' }, end: { date: '2026-06-19' } })
|
|
77
|
-
expect(t.allDay).toBe(true)
|
|
78
|
-
expect(t.startsAt).toBe('2026-06-18')
|
|
79
|
-
expect(t.endsAt).toBe('2026-06-19')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('reads a timed event window as not all-day', () => {
|
|
83
|
-
const t = eventTimes({ id: 'e1', start: { dateTime: '2026-06-18T13:00:00-03:00' }, end: { dateTime: '2026-06-18T14:00:00-03:00' } })
|
|
84
|
-
expect(t.allDay).toBe(false)
|
|
85
|
-
expect(t.startsAt).toBe('2026-06-18T13:00:00-03:00')
|
|
86
|
-
})
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
describe('google-calendar · appointment-row mapping (edge-function shape)', () => {
|
|
90
|
-
function row(overrides: Partial<AppointmentRow> = {}): AppointmentRow {
|
|
91
|
-
return { id: 'a1', tenant_id: 't1', starts_at: '2026-06-18T13:00:00-03:00', ends_at: '2026-06-18T14:00:00-03:00', ...overrides }
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
it('maps a minimal row to a Google event input', () => {
|
|
95
|
-
const ev = appointmentRowToEvent(row())
|
|
96
|
-
expect(ev.summary).toBe('Appointment')
|
|
97
|
-
expect(ev.start.dateTime).toBe('2026-06-18T13:00:00-03:00')
|
|
98
|
-
expect(ev.end.dateTime).toBe('2026-06-18T14:00:00-03:00')
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('builds the title from metadata service + contact names', () => {
|
|
102
|
-
const ev = appointmentRowToEvent(row({ metadata: { serviceNames: 'Cleaning', contactName: 'Sarah' }, notes: 'bring x-rays' }))
|
|
103
|
-
expect(ev.summary).toBe('Cleaning — Sarah')
|
|
104
|
-
expect(ev.description).toBe('bring x-rays')
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('falls back end→start when the row has no end', () => {
|
|
108
|
-
const ev = appointmentRowToEvent(row({ ends_at: null }))
|
|
109
|
-
expect(ev.end.dateTime).toBe('2026-06-18T13:00:00-03:00')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('maps an inbound reschedule to a snake_case appointment patch', () => {
|
|
113
|
-
const patch = eventToAppointmentPatch({
|
|
114
|
-
id: 'e1', summary: 'Moved',
|
|
115
|
-
start: { dateTime: '2026-06-18T15:00:00-03:00' }, end: { dateTime: '2026-06-18T16:00:00-03:00' },
|
|
116
|
-
})
|
|
117
|
-
expect(patch.cancelled).toBe(false)
|
|
118
|
-
expect(patch.starts_at).toBe('2026-06-18T15:00:00-03:00')
|
|
119
|
-
expect(patch.ends_at).toBe('2026-06-18T16:00:00-03:00')
|
|
120
|
-
expect(patch.summary).toBe('Moved')
|
|
121
|
-
expect(patch.allDay).toBe(false)
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
it('maps an inbound all-day event to an all-day patch', () => {
|
|
125
|
-
const patch = eventToAppointmentPatch({ id: 'e1', start: { date: '2026-06-18' }, end: { date: '2026-06-19' } })
|
|
126
|
-
expect(patch.allDay).toBe(true)
|
|
127
|
-
expect(patch.starts_at).toBe('2026-06-18')
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('maps an inbound cancellation to a cancelled patch', () => {
|
|
131
|
-
const patch = eventToAppointmentPatch({ id: 'e1', status: 'cancelled', start: {}, end: {} })
|
|
132
|
-
expect(patch.cancelled).toBe(true)
|
|
133
|
-
expect(patch.starts_at).toBeUndefined()
|
|
134
|
-
})
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
describe('google-calendar · channel resolution (outbound priority, pure)', () => {
|
|
138
|
-
function ch(overrides: Partial<CalendarChannel> = {}): CalendarChannel {
|
|
139
|
-
return {
|
|
140
|
-
id: 'c1', integrationId: 'i1', tenantId: 't1', googleCalendarId: 'cal@group',
|
|
141
|
-
summary: null, color: null, direction: 'bidirectional', targetKind: null, targetId: null,
|
|
142
|
-
importMode: 'appointment', syncToken: null, channelId: null, resourceId: null,
|
|
143
|
-
channelExpiresAt: null, isActive: true, createdAt: '', updatedAt: '',
|
|
144
|
-
...overrides,
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
it('prefers an assignee-targeted channel over service/location/whole-agenda', () => {
|
|
149
|
-
const channels = [
|
|
150
|
-
ch({ id: 'whole', googleCalendarId: 'whole@g', targetKind: null }),
|
|
151
|
-
ch({ id: 'loc', googleCalendarId: 'loc@g', targetKind: 'location', targetId: 'L1' }),
|
|
152
|
-
ch({ id: 'svc', googleCalendarId: 'svc@g', targetKind: 'service', targetId: 'S1' }),
|
|
153
|
-
ch({ id: 'ana', googleCalendarId: 'ana@g', targetKind: 'assignee', targetId: 'A1' }),
|
|
154
|
-
]
|
|
155
|
-
const match = resolveTargetChannel(channels, { assigneeId: 'A1', serviceIds: ['S1'], locationId: 'L1' })
|
|
156
|
-
expect(match?.id).toBe('ana')
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('falls through assignee → service → location → whole agenda', () => {
|
|
160
|
-
const svc = ch({ id: 'svc', targetKind: 'service', targetId: 'S9' })
|
|
161
|
-
const loc = ch({ id: 'loc', targetKind: 'location', targetId: 'L9' })
|
|
162
|
-
const whole = ch({ id: 'whole', targetKind: null })
|
|
163
|
-
expect(resolveTargetChannel([svc, loc, whole], { assigneeId: 'nope', serviceIds: ['S9'] })?.id).toBe('svc')
|
|
164
|
-
expect(resolveTargetChannel([loc, whole], { locationId: 'L9' })?.id).toBe('loc')
|
|
165
|
-
expect(resolveTargetChannel([whole], { assigneeId: 'x' })?.id).toBe('whole')
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('ignores inactive and non-outbound channels', () => {
|
|
169
|
-
const inbound = ch({ id: 'in', targetKind: 'assignee', targetId: 'A1', direction: 'inbound' })
|
|
170
|
-
const off = ch({ id: 'off', targetKind: 'assignee', targetId: 'A1', direction: 'off' })
|
|
171
|
-
const inactive = ch({ id: 'x', targetKind: null, isActive: false })
|
|
172
|
-
expect(resolveTargetChannel([inbound, off, inactive], { assigneeId: 'A1' })).toBeNull()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('returns null when nothing matches', () => {
|
|
176
|
-
const svc = ch({ id: 'svc', targetKind: 'service', targetId: 'S1' })
|
|
177
|
-
expect(resolveTargetChannel([svc], { assigneeId: 'A1', serviceIds: ['other'] })).toBeNull()
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('channelRowToChannel maps snake_case rows with defaults', () => {
|
|
181
|
-
const mapped = channelRowToChannel({
|
|
182
|
-
id: 'c1', integration_id: 'i1', tenant_id: 't1', google_calendar_id: 'cal@g', direction: 'inbound',
|
|
183
|
-
})
|
|
184
|
-
expect(mapped.googleCalendarId).toBe('cal@g')
|
|
185
|
-
expect(mapped.importMode).toBe('appointment')
|
|
186
|
-
expect(mapped.isActive).toBe(true)
|
|
187
|
-
expect(mapped.targetKind).toBeNull()
|
|
188
|
-
})
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// Data-plane behaviors — require a live Google account + the deployed
|
|
192
|
-
// google-calendar-sync edge function. Documented as the pending contract (TDD).
|
|
193
|
-
describe('google-calendar · sync data plane (pending — needs edge function)', () => {
|
|
194
|
-
it.todo('push_event: creating a booking creates a Google event and stores its id on bookings.metadata')
|
|
195
|
-
it.todo('push_event: updating a booking patches the linked Google event')
|
|
196
|
-
it.todo('push_event: cancelling a booking deletes the Google event')
|
|
197
|
-
it.todo('pull_events: a Google reschedule updates the linked booking; a cancel cancels it')
|
|
198
|
-
it.todo('pull_events: advances and persists the incremental syncToken')
|
|
199
|
-
})
|