@fayz-ai/plugin-agenda 0.9.0 → 0.10.0
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-ISLJXP7O.js} +7 -2
- package/dist/chunk-ISLJXP7O.js.map +1 -0
- package/dist/components/AppointmentModal.d.ts.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/mock.d.ts.map +1 -1
- package/dist/data/realtime.test.d.ts +2 -0
- package/dist/data/realtime.test.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +7 -0
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2515 -187
- package/dist/index.js.map +1 -1
- package/dist/integrations/google-calendar/conflict.d.ts +25 -0
- package/dist/integrations/google-calendar/conflict.d.ts.map +1 -0
- package/dist/integrations/google-calendar/connectorDef.d.ts +12 -0
- package/dist/integrations/google-calendar/connectorDef.d.ts.map +1 -1
- package/dist/integrations/google-calendar/credentials.d.ts +27 -0
- package/dist/integrations/google-calendar/credentials.d.ts.map +1 -0
- package/dist/integrations/google-calendar/data/supabase.d.ts +10 -2
- package/dist/integrations/google-calendar/data/supabase.d.ts.map +1 -1
- package/dist/integrations/google-calendar/data/tables.d.ts +2 -0
- package/dist/integrations/google-calendar/data/tables.d.ts.map +1 -1
- package/dist/integrations/google-calendar/health.d.ts +23 -0
- package/dist/integrations/google-calendar/health.d.ts.map +1 -0
- package/dist/integrations/google-calendar/health.test.d.ts +2 -0
- package/dist/integrations/google-calendar/health.test.d.ts.map +1 -0
- package/dist/integrations/google-calendar/index.d.ts +6 -0
- package/dist/integrations/google-calendar/index.d.ts.map +1 -1
- package/dist/integrations/google-calendar/migrations/index.d.ts +12 -0
- package/dist/integrations/google-calendar/migrations/index.d.ts.map +1 -0
- package/dist/integrations/google-calendar/panel-data.d.ts +26 -0
- package/dist/integrations/google-calendar/panel-data.d.ts.map +1 -0
- package/dist/integrations/google-calendar/panel-data.test.d.ts +2 -0
- package/dist/integrations/google-calendar/panel-data.test.d.ts.map +1 -0
- package/dist/integrations/google-calendar/types.d.ts +17 -0
- package/dist/integrations/google-calendar/types.d.ts.map +1 -1
- package/dist/lib/booking-events.d.ts +5 -0
- package/dist/lib/booking-events.d.ts.map +1 -0
- package/dist/lib/calendar-events.d.ts +8 -0
- package/dist/lib/calendar-events.d.ts.map +1 -0
- package/dist/lib/calendar-events.test.d.ts +2 -0
- package/dist/lib/calendar-events.test.d.ts.map +1 -0
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- 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/dist/views/CalendarView.d.ts.map +1 -1
- package/package.json +9 -11
- 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/chunk-Q2PAWBPR.js.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/integrations/google-calendar/GoogleCalendarSettings.d.ts +0 -3
- package/dist/integrations/google-calendar/GoogleCalendarSettings.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,517 +0,0 @@
|
|
|
1
|
-
-- ---------------------------------------------------------------------------
|
|
2
|
-
-- Google Calendar SMART SYNC — multi-calendar de-para + loop-safe two-way sync
|
|
3
|
-
-- ---------------------------------------------------------------------------
|
|
4
|
-
-- Builds on 001_google_calendar.sql. Where 001 modelled a single calendar per
|
|
5
|
-
-- tenant (plg_calendar_integrations, one outbound trigger, a pull template), 002
|
|
6
|
-
-- makes the sync vertical-agnostic and bidirectional-safe:
|
|
7
|
-
--
|
|
8
|
-
-- §1 public.plg_calendar_channels — de-para: one Google calendar → one sync
|
|
9
|
-
-- target (whole tenant / assignee / service
|
|
10
|
-
-- / location), per direction + import mode
|
|
11
|
-
-- §2 fn_gcal_push_booking (REPLACE)— loop-prevention guard + correlation id
|
|
12
|
-
-- §3 plg_calendar_sync_log — correlation_id + channel_id_ref columns
|
|
13
|
-
-- §4 inbound write functions — the ONLY path Google→booking writes take
|
|
14
|
-
-- (gcal_import_event / gcal_apply_event_patch
|
|
15
|
-
-- / gcal_unlink_channel), SECURITY DEFINER,
|
|
16
|
-
-- service_role-only
|
|
17
|
-
-- §5 get_available_slots (REPLACE) — tenant-wide imported blocks now also
|
|
18
|
-
-- remove slots for every professional
|
|
19
|
-
-- §6 pg_cron template — per-channel pull
|
|
20
|
-
--
|
|
21
|
-
-- Idempotent / re-runnable: IF NOT EXISTS, CREATE OR REPLACE, DROP … IF EXISTS.
|
|
22
|
-
-- Written against the canonical public names (appointments uses starts_at/ends_at,
|
|
23
|
-
-- metadata jsonb, party_id/assignee_id — see packages/db 004_archetypes.sql).
|
|
24
|
-
-- ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
-- ===========================================================================
|
|
27
|
-
-- §1 — plg_calendar_channels: multi-calendar de-para (vertical-agnostic)
|
|
28
|
-
-- target_kind NULL → whole tenant agenda (agency-os): every professional
|
|
29
|
-
-- target_kind 'assignee' → calendar maps to one professional (school: teacher)
|
|
30
|
-
-- target_kind 'service' | 'location' → clinic-style mapping
|
|
31
|
-
-- import_mode 'block' → inbound events become opaque busy blocks
|
|
32
|
-
-- import_mode 'appointment' → inbound events become real appointments
|
|
33
|
-
-- ===========================================================================
|
|
34
|
-
CREATE TABLE IF NOT EXISTS public.plg_calendar_channels (
|
|
35
|
-
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
36
|
-
integration_id uuid NOT NULL REFERENCES public.plg_calendar_integrations(id) ON DELETE CASCADE,
|
|
37
|
-
tenant_id uuid NOT NULL,
|
|
38
|
-
google_calendar_id text NOT NULL,
|
|
39
|
-
summary text,
|
|
40
|
-
color text,
|
|
41
|
-
direction text NOT NULL DEFAULT 'bidirectional'
|
|
42
|
-
CHECK (direction IN ('inbound','outbound','bidirectional','off')),
|
|
43
|
-
target_kind text CHECK (target_kind IN ('assignee','service','location')),
|
|
44
|
-
target_id uuid,
|
|
45
|
-
import_mode text NOT NULL DEFAULT 'block'
|
|
46
|
-
CHECK (import_mode IN ('block','appointment')),
|
|
47
|
-
sync_token text,
|
|
48
|
-
channel_id text,
|
|
49
|
-
resource_id text,
|
|
50
|
-
channel_expires_at timestamptz,
|
|
51
|
-
is_active boolean NOT NULL DEFAULT true,
|
|
52
|
-
created_at timestamptz NOT NULL DEFAULT now(),
|
|
53
|
-
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
54
|
-
UNIQUE (integration_id, google_calendar_id)
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
CREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_tenant ON public.plg_calendar_channels(tenant_id);
|
|
58
|
-
CREATE INDEX IF NOT EXISTS idx_plg_calendar_channels_integration ON public.plg_calendar_channels(integration_id);
|
|
59
|
-
|
|
60
|
-
ALTER TABLE public.plg_calendar_channels ENABLE ROW LEVEL SECURITY;
|
|
61
|
-
GRANT SELECT, INSERT, UPDATE, DELETE ON public.plg_calendar_channels TO authenticated;
|
|
62
|
-
-- service_role bypasses RLS, but this table is born after 004's blanket grant,
|
|
63
|
-
-- so grant it explicitly (the inbound functions run as definer/service_role).
|
|
64
|
-
GRANT ALL ON public.plg_calendar_channels TO service_role;
|
|
65
|
-
|
|
66
|
-
DROP TRIGGER IF EXISTS plg_calendar_channels_updated_at ON public.plg_calendar_channels;
|
|
67
|
-
CREATE TRIGGER plg_calendar_channels_updated_at BEFORE UPDATE ON public.plg_calendar_channels
|
|
68
|
-
FOR EACH ROW EXECUTE FUNCTION public.handle_updated_at();
|
|
69
|
-
|
|
70
|
-
-- RLS: tenant isolation (padrão 001) + explicit service_role FOR ALL policy
|
|
71
|
-
-- (matches the vigente service_role policy pattern from 001_core payment_events).
|
|
72
|
-
DROP POLICY IF EXISTS plg_calendar_channels_rls ON public.plg_calendar_channels;
|
|
73
|
-
CREATE POLICY plg_calendar_channels_rls ON public.plg_calendar_channels
|
|
74
|
-
FOR ALL TO authenticated
|
|
75
|
-
USING (tenant_id IN (SELECT public.user_tenant_ids()))
|
|
76
|
-
WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));
|
|
77
|
-
|
|
78
|
-
DROP POLICY IF EXISTS plg_calendar_channels_service ON public.plg_calendar_channels;
|
|
79
|
-
CREATE POLICY plg_calendar_channels_service ON public.plg_calendar_channels
|
|
80
|
-
FOR ALL TO service_role USING (true) WITH CHECK (true);
|
|
81
|
-
|
|
82
|
-
-- ===========================================================================
|
|
83
|
-
-- §2 — Outbound trigger with loop-prevention + correlation id.
|
|
84
|
-
-- CREATE OR REPLACE of 001's fn_gcal_push_booking. Same pg_net → edge function
|
|
85
|
-
-- payload (app.gcal_sync_url / app.gcal_service_key), but now:
|
|
86
|
-
-- * SKIP the push when the write itself came from the Google sync, detected by
|
|
87
|
-
-- the TRANSACTION-LOCAL GUC app.booking_origin = 'google' — set by every §4
|
|
88
|
-
-- writer via set_config(..., true) and gone when the transaction ends.
|
|
89
|
-
-- This is what stops the Google→booking→Google→… echo loop.
|
|
90
|
-
-- Deliberately NOT a persistent metadata check: a sticky marker
|
|
91
|
-
-- (metadata.syncOrigin) would permanently mute pushes for imported events,
|
|
92
|
-
-- so a later admin edit would never reach Google. metadata.syncOrigin is
|
|
93
|
-
-- kept by §4 as provenance only — this trigger does not read it.
|
|
94
|
-
-- * carry a correlation_id so both directions can be stitched in the sync log
|
|
95
|
-
-- (from GUC app.booking_correlation_id, else a fresh uuid — design ported
|
|
96
|
-
-- from PR #7).
|
|
97
|
-
-- ===========================================================================
|
|
98
|
-
CREATE OR REPLACE FUNCTION public.fn_gcal_push_booking() RETURNS trigger
|
|
99
|
-
LANGUAGE plpgsql SECURITY DEFINER AS $$
|
|
100
|
-
DECLARE
|
|
101
|
-
url text := current_setting('app.gcal_sync_url', true);
|
|
102
|
-
key text := current_setting('app.gcal_service_key', true);
|
|
103
|
-
rec record := COALESCE(NEW, OLD);
|
|
104
|
-
op text := lower(TG_OP);
|
|
105
|
-
corr text := NULLIF(current_setting('app.booking_correlation_id', true), '');
|
|
106
|
-
BEGIN
|
|
107
|
-
-- Loop guard: never bounce a Google-originated write back to Google.
|
|
108
|
-
-- Transaction-local GUC only (see §2 header for why not persistent metadata).
|
|
109
|
-
IF current_setting('app.booking_origin', true) = 'google' THEN
|
|
110
|
-
RETURN COALESCE(NEW, OLD);
|
|
111
|
-
END IF;
|
|
112
|
-
|
|
113
|
-
IF url IS NULL OR url = '' THEN RETURN COALESCE(NEW, OLD); END IF;
|
|
114
|
-
|
|
115
|
-
PERFORM net.http_post(
|
|
116
|
-
url := url,
|
|
117
|
-
headers := jsonb_build_object('Content-Type', 'application/json',
|
|
118
|
-
'Authorization', 'Bearer ' || COALESCE(key, '')),
|
|
119
|
-
body := jsonb_build_object(
|
|
120
|
-
'action', 'push_event',
|
|
121
|
-
'op', op,
|
|
122
|
-
'bookingId', rec.id,
|
|
123
|
-
'tenantId', rec.tenant_id,
|
|
124
|
-
'correlationId', COALESCE(corr, gen_random_uuid()::text)
|
|
125
|
-
)
|
|
126
|
-
);
|
|
127
|
-
RETURN COALESCE(NEW, OLD);
|
|
128
|
-
END $$;
|
|
129
|
-
|
|
130
|
-
-- Re-assert the trigger (idempotent; unchanged from 001).
|
|
131
|
-
DROP TRIGGER IF EXISTS trg_gcal_push ON public.appointments;
|
|
132
|
-
CREATE TRIGGER trg_gcal_push
|
|
133
|
-
AFTER INSERT OR UPDATE OR DELETE ON public.appointments
|
|
134
|
-
FOR EACH ROW EXECUTE FUNCTION public.fn_gcal_push_booking();
|
|
135
|
-
|
|
136
|
-
-- ===========================================================================
|
|
137
|
-
-- §3 — sync log: correlate the two directions + reference the source channel.
|
|
138
|
-
-- ===========================================================================
|
|
139
|
-
ALTER TABLE public.plg_calendar_sync_log
|
|
140
|
-
ADD COLUMN IF NOT EXISTS correlation_id uuid;
|
|
141
|
-
ALTER TABLE public.plg_calendar_sync_log
|
|
142
|
-
ADD COLUMN IF NOT EXISTS channel_id_ref uuid
|
|
143
|
-
REFERENCES public.plg_calendar_channels(id) ON DELETE SET NULL;
|
|
144
|
-
-- Unlinked future events seen during a pull (import-wizard candidates), so the
|
|
145
|
-
-- log can show "N eventos aguardando import" without re-hitting Google.
|
|
146
|
-
ALTER TABLE public.plg_calendar_sync_log
|
|
147
|
-
ADD COLUMN IF NOT EXISTS discovered integer;
|
|
148
|
-
|
|
149
|
-
-- ===========================================================================
|
|
150
|
-
-- §4 — Inbound write path (Google → booking). THE single entry point used by
|
|
151
|
-
-- the edge function (scheduled pull, webhook, and the import wizard). All three
|
|
152
|
-
-- functions are SECURITY DEFINER with SET search_path = public and are granted
|
|
153
|
-
-- to service_role ONLY (anon/authenticated explicitly revoked) — they bypass the
|
|
154
|
-
-- tenant RLS on purpose and must never be reachable from the anon/authenticated
|
|
155
|
-
-- PostgREST surface. Every writer sets the TRANSACTION-LOCAL GUC
|
|
156
|
-
-- app.booking_origin='google' (set_config ..., true) so §2's trigger suppresses
|
|
157
|
-
-- the echo for THIS write only; metadata.syncOrigin is provenance, not a guard.
|
|
158
|
-
-- ===========================================================================
|
|
159
|
-
|
|
160
|
-
-- gcal_import_event: create (or idempotently update) a booking from a Google
|
|
161
|
-
-- event, honouring the channel's import_mode + target mapping.
|
|
162
|
-
-- kind = 'block' | 'appointment' (from channel.import_mode)
|
|
163
|
-
-- assignee_id = channel.target_id when target_kind='assignee', else NULL
|
|
164
|
-
-- (NULL = tenant-wide → blocks every professional, see §5)
|
|
165
|
-
-- location_id = channel.target_id when target_kind='location', else NULL
|
|
166
|
-
-- status = 'scheduled'
|
|
167
|
-
-- No order is created — an external import is not a sale. Idempotent on
|
|
168
|
-
-- (tenant_id, metadata.googleCalendarEventId): update in place if seen before.
|
|
169
|
-
CREATE OR REPLACE FUNCTION public.gcal_import_event(
|
|
170
|
-
p_tenant_id uuid,
|
|
171
|
-
p_channel_id uuid,
|
|
172
|
-
p_event_id text,
|
|
173
|
-
p_etag text,
|
|
174
|
-
p_summary text,
|
|
175
|
-
p_starts_at timestamptz,
|
|
176
|
-
p_ends_at timestamptz,
|
|
177
|
-
p_all_day boolean,
|
|
178
|
-
p_description text DEFAULT NULL
|
|
179
|
-
) RETURNS uuid
|
|
180
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
181
|
-
SET search_path = public
|
|
182
|
-
AS $$
|
|
183
|
-
DECLARE
|
|
184
|
-
v_ch record;
|
|
185
|
-
v_kind text;
|
|
186
|
-
v_assignee uuid;
|
|
187
|
-
v_location uuid;
|
|
188
|
-
v_ends timestamptz;
|
|
189
|
-
v_meta jsonb;
|
|
190
|
-
v_existing uuid;
|
|
191
|
-
v_appt uuid;
|
|
192
|
-
BEGIN
|
|
193
|
-
IF p_tenant_id IS NULL OR p_event_id IS NULL OR p_starts_at IS NULL THEN
|
|
194
|
-
RAISE EXCEPTION 'gcal_import_event: tenant, event id and start are required';
|
|
195
|
-
END IF;
|
|
196
|
-
|
|
197
|
-
-- Transaction-local: §2's trigger skips the echo for this write only.
|
|
198
|
-
PERFORM set_config('app.booking_origin', 'google', true);
|
|
199
|
-
|
|
200
|
-
-- Resolve the channel mapping (must belong to the same tenant).
|
|
201
|
-
SELECT c.import_mode, c.target_kind, c.target_id
|
|
202
|
-
INTO v_ch
|
|
203
|
-
FROM plg_calendar_channels c
|
|
204
|
-
WHERE c.id = p_channel_id AND c.tenant_id = p_tenant_id;
|
|
205
|
-
IF NOT FOUND THEN RAISE EXCEPTION 'gcal_import_event: unknown channel for tenant'; END IF;
|
|
206
|
-
|
|
207
|
-
v_kind := CASE WHEN v_ch.import_mode = 'appointment' THEN 'appointment' ELSE 'block' END;
|
|
208
|
-
v_assignee := CASE WHEN v_ch.target_kind = 'assignee' THEN v_ch.target_id ELSE NULL END;
|
|
209
|
-
v_location := CASE WHEN v_ch.target_kind = 'location' THEN v_ch.target_id ELSE NULL END;
|
|
210
|
-
|
|
211
|
-
-- All-day events without an explicit end block the whole day so §5 excludes it.
|
|
212
|
-
v_ends := COALESCE(
|
|
213
|
-
p_ends_at,
|
|
214
|
-
CASE WHEN COALESCE(p_all_day, false) THEN p_starts_at + interval '1 day' ELSE p_starts_at END
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
v_meta := jsonb_build_object(
|
|
218
|
-
'syncOrigin', 'google',
|
|
219
|
-
'googleCalendarEventId', p_event_id,
|
|
220
|
-
'gcalEtag', p_etag,
|
|
221
|
-
'gcalChannelId', p_channel_id::text,
|
|
222
|
-
'allDay', COALESCE(p_all_day, false),
|
|
223
|
-
'title', p_summary,
|
|
224
|
-
'source', 'google_import'
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
-- Idempotency: has this event already been imported for the tenant?
|
|
228
|
-
SELECT b.id INTO v_existing
|
|
229
|
-
FROM appointments b
|
|
230
|
-
WHERE b.tenant_id = p_tenant_id
|
|
231
|
-
AND b.metadata->>'googleCalendarEventId' = p_event_id
|
|
232
|
-
LIMIT 1;
|
|
233
|
-
|
|
234
|
-
IF v_existing IS NOT NULL THEN
|
|
235
|
-
UPDATE appointments b
|
|
236
|
-
SET kind = v_kind,
|
|
237
|
-
assignee_id = v_assignee,
|
|
238
|
-
location_id = v_location,
|
|
239
|
-
starts_at = p_starts_at,
|
|
240
|
-
ends_at = v_ends,
|
|
241
|
-
status = 'scheduled',
|
|
242
|
-
notes = COALESCE(p_description, p_summary),
|
|
243
|
-
metadata = COALESCE(b.metadata, '{}'::jsonb) || v_meta,
|
|
244
|
-
updated_at = now()
|
|
245
|
-
WHERE b.id = v_existing
|
|
246
|
-
RETURNING b.id INTO v_appt;
|
|
247
|
-
ELSE
|
|
248
|
-
INSERT INTO appointments (tenant_id, kind, assignee_id, location_id,
|
|
249
|
-
starts_at, ends_at, status, notes, metadata)
|
|
250
|
-
VALUES (p_tenant_id, v_kind, v_assignee, v_location,
|
|
251
|
-
p_starts_at, v_ends, 'scheduled', COALESCE(p_description, p_summary), v_meta)
|
|
252
|
-
RETURNING id INTO v_appt;
|
|
253
|
-
END IF;
|
|
254
|
-
|
|
255
|
-
RETURN v_appt;
|
|
256
|
-
END $$;
|
|
257
|
-
|
|
258
|
-
-- gcal_apply_event_patch: apply an inbound reschedule/cancel to an already-linked
|
|
259
|
-
-- booking. Located by metadata.googleCalendarEventId. Echo suppression: if the
|
|
260
|
-
-- stored gcalEtag already equals the incoming etag, this is our own write coming
|
|
261
|
-
-- back — skip and return NULL. Every real write re-stamps syncOrigin='google'.
|
|
262
|
-
CREATE OR REPLACE FUNCTION public.gcal_apply_event_patch(
|
|
263
|
-
p_tenant_id uuid,
|
|
264
|
-
p_event_id text,
|
|
265
|
-
p_etag text,
|
|
266
|
-
p_starts_at timestamptz,
|
|
267
|
-
p_ends_at timestamptz,
|
|
268
|
-
p_summary text,
|
|
269
|
-
p_cancelled boolean
|
|
270
|
-
) RETURNS uuid
|
|
271
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
272
|
-
SET search_path = public
|
|
273
|
-
AS $$
|
|
274
|
-
DECLARE
|
|
275
|
-
v_appt uuid;
|
|
276
|
-
v_curr_etag text;
|
|
277
|
-
BEGIN
|
|
278
|
-
IF p_tenant_id IS NULL OR p_event_id IS NULL THEN
|
|
279
|
-
RAISE EXCEPTION 'gcal_apply_event_patch: tenant and event id are required';
|
|
280
|
-
END IF;
|
|
281
|
-
|
|
282
|
-
-- Transaction-local: §2's trigger skips the echo for this write only.
|
|
283
|
-
PERFORM set_config('app.booking_origin', 'google', true);
|
|
284
|
-
|
|
285
|
-
SELECT b.id, b.metadata->>'gcalEtag'
|
|
286
|
-
INTO v_appt, v_curr_etag
|
|
287
|
-
FROM appointments b
|
|
288
|
-
WHERE b.tenant_id = p_tenant_id
|
|
289
|
-
AND b.metadata->>'googleCalendarEventId' = p_event_id
|
|
290
|
-
LIMIT 1;
|
|
291
|
-
|
|
292
|
-
IF v_appt IS NULL THEN RETURN NULL; END IF; -- not linked here
|
|
293
|
-
IF p_etag IS NOT NULL AND p_etag = v_curr_etag THEN
|
|
294
|
-
RETURN NULL; -- echo: nothing changed
|
|
295
|
-
END IF;
|
|
296
|
-
|
|
297
|
-
IF COALESCE(p_cancelled, false) THEN
|
|
298
|
-
UPDATE appointments b
|
|
299
|
-
SET status = 'cancelled',
|
|
300
|
-
metadata = COALESCE(b.metadata, '{}'::jsonb)
|
|
301
|
-
|| jsonb_build_object('syncOrigin', 'google', 'gcalEtag', p_etag),
|
|
302
|
-
updated_at = now()
|
|
303
|
-
WHERE b.id = v_appt;
|
|
304
|
-
ELSE
|
|
305
|
-
UPDATE appointments b
|
|
306
|
-
SET starts_at = COALESCE(p_starts_at, b.starts_at),
|
|
307
|
-
ends_at = COALESCE(p_ends_at, b.ends_at),
|
|
308
|
-
notes = COALESCE(p_summary, b.notes),
|
|
309
|
-
metadata = COALESCE(b.metadata, '{}'::jsonb)
|
|
310
|
-
|| jsonb_build_object('syncOrigin', 'google',
|
|
311
|
-
'gcalEtag', p_etag,
|
|
312
|
-
'title', COALESCE(p_summary, b.metadata->>'title')),
|
|
313
|
-
updated_at = now()
|
|
314
|
-
WHERE b.id = v_appt;
|
|
315
|
-
END IF;
|
|
316
|
-
|
|
317
|
-
RETURN v_appt;
|
|
318
|
-
END $$;
|
|
319
|
-
|
|
320
|
-
-- gcal_stamp_outbound: after the edge function creates the Google event for an
|
|
321
|
-
-- outbound push, it stamps the fresh event id/etag back onto the appointment.
|
|
322
|
-
-- Bookkeeping only — must not re-fire §2's trigger, hence the GUC. Does NOT set
|
|
323
|
-
-- metadata.syncOrigin (the booking's provenance is still the app/site).
|
|
324
|
-
CREATE OR REPLACE FUNCTION public.gcal_stamp_outbound(
|
|
325
|
-
p_tenant_id uuid,
|
|
326
|
-
p_booking_id uuid,
|
|
327
|
-
p_event_id text,
|
|
328
|
-
p_etag text DEFAULT NULL,
|
|
329
|
-
p_calendar_id text DEFAULT NULL
|
|
330
|
-
) RETURNS uuid
|
|
331
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
332
|
-
SET search_path = public
|
|
333
|
-
AS $$
|
|
334
|
-
DECLARE
|
|
335
|
-
v_id uuid;
|
|
336
|
-
BEGIN
|
|
337
|
-
IF p_tenant_id IS NULL OR p_booking_id IS NULL OR p_event_id IS NULL THEN
|
|
338
|
-
RAISE EXCEPTION 'gcal_stamp_outbound: tenant, booking and event id are required';
|
|
339
|
-
END IF;
|
|
340
|
-
|
|
341
|
-
-- Transaction-local: §2's trigger skips the echo for this write only.
|
|
342
|
-
PERFORM set_config('app.booking_origin', 'google', true);
|
|
343
|
-
|
|
344
|
-
UPDATE appointments b
|
|
345
|
-
SET metadata = COALESCE(b.metadata, '{}'::jsonb)
|
|
346
|
-
|| jsonb_build_object('googleCalendarEventId', p_event_id,
|
|
347
|
-
'gcalEtag', p_etag,
|
|
348
|
-
'gcalSyncedAt', now())
|
|
349
|
-
|| CASE WHEN p_calendar_id IS NULL THEN '{}'::jsonb
|
|
350
|
-
ELSE jsonb_build_object('gcalCalendarId', p_calendar_id) END,
|
|
351
|
-
updated_at = now()
|
|
352
|
-
WHERE b.id = p_booking_id AND b.tenant_id = p_tenant_id
|
|
353
|
-
RETURNING b.id INTO v_id;
|
|
354
|
-
RETURN v_id;
|
|
355
|
-
END $$;
|
|
356
|
-
|
|
357
|
-
-- gcal_unlink_channel: forget the incremental cursor + Google watch bookkeeping
|
|
358
|
-
-- for a channel (used on disconnect / re-watch). Leaves the mapping row in place.
|
|
359
|
-
CREATE OR REPLACE FUNCTION public.gcal_unlink_channel(
|
|
360
|
-
p_channel_id uuid
|
|
361
|
-
) RETURNS uuid
|
|
362
|
-
LANGUAGE plpgsql SECURITY DEFINER
|
|
363
|
-
SET search_path = public
|
|
364
|
-
AS $$
|
|
365
|
-
DECLARE
|
|
366
|
-
v_id uuid;
|
|
367
|
-
BEGIN
|
|
368
|
-
UPDATE plg_calendar_channels c
|
|
369
|
-
SET sync_token = NULL,
|
|
370
|
-
channel_id = NULL,
|
|
371
|
-
resource_id = NULL,
|
|
372
|
-
channel_expires_at = NULL,
|
|
373
|
-
updated_at = now()
|
|
374
|
-
WHERE c.id = p_channel_id
|
|
375
|
-
RETURNING c.id INTO v_id;
|
|
376
|
-
RETURN v_id;
|
|
377
|
-
END $$;
|
|
378
|
-
|
|
379
|
-
-- Lock the inbound writers to service_role only.
|
|
380
|
-
DO $$
|
|
381
|
-
DECLARE fn text;
|
|
382
|
-
BEGIN
|
|
383
|
-
FOR fn IN SELECT unnest(ARRAY[
|
|
384
|
-
'public.gcal_import_event(uuid, uuid, text, text, text, timestamptz, timestamptz, boolean, text)',
|
|
385
|
-
'public.gcal_apply_event_patch(uuid, text, text, timestamptz, timestamptz, text, boolean)',
|
|
386
|
-
'public.gcal_stamp_outbound(uuid, uuid, text, text, text)',
|
|
387
|
-
'public.gcal_unlink_channel(uuid)'
|
|
388
|
-
])
|
|
389
|
-
LOOP
|
|
390
|
-
EXECUTE format('REVOKE ALL ON FUNCTION %s FROM public, anon, authenticated', fn);
|
|
391
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO service_role', fn);
|
|
392
|
-
END LOOP;
|
|
393
|
-
END $$;
|
|
394
|
-
|
|
395
|
-
-- ===========================================================================
|
|
396
|
-
-- §5 — get_available_slots: imported Google blocks must remove availability.
|
|
397
|
-
-- CREATE OR REPLACE keeping the EXACT signature (anon + admin both call it, and
|
|
398
|
-
-- it is graded on the same shape as plugin-agenda/src/migrations/001_public_booking.sql).
|
|
399
|
-
--
|
|
400
|
-
-- Conceptual diff vs 001 — ONE condition in the overlap NOT EXISTS:
|
|
401
|
-
-- before: AND b.assignee_id = c.assignee_id
|
|
402
|
-
-- after: AND (b.assignee_id = c.assignee_id
|
|
403
|
-
-- OR (b.assignee_id IS NULL AND b.kind = 'block'))
|
|
404
|
-
-- Rationale: a tenant-wide BLOCK imported from Google (target_kind NULL →
|
|
405
|
-
-- assignee_id IS NULL, kind 'block', see §4) must busy-out the slot for EVERY
|
|
406
|
-
-- professional. Restricted to kind='block' so an unassigned admin appointment
|
|
407
|
-
-- keeps the 001 behaviour (blocks nobody). Per-assignee rows keep matching on
|
|
408
|
-
-- assignee_id.
|
|
409
|
-
-- Everything else is byte-identical to 001: the status filter already covers
|
|
410
|
-
-- "any non-cancelled kind" (no_show also excluded), and the p_assignee_id-NULL
|
|
411
|
-
-- fan-out across staff via day_schedules is preserved untouched.
|
|
412
|
-
-- ===========================================================================
|
|
413
|
-
CREATE OR REPLACE FUNCTION public.get_available_slots(
|
|
414
|
-
p_tenant_id uuid,
|
|
415
|
-
p_date date,
|
|
416
|
-
p_duration_minutes integer,
|
|
417
|
-
p_assignee_id uuid DEFAULT NULL,
|
|
418
|
-
p_slot_interval integer DEFAULT 30
|
|
419
|
-
) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)
|
|
420
|
-
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
421
|
-
SET search_path = public
|
|
422
|
-
AS $$
|
|
423
|
-
DECLARE
|
|
424
|
-
v_tz text;
|
|
425
|
-
BEGIN
|
|
426
|
-
-- tight validation: this function is anon-callable
|
|
427
|
-
IF p_duration_minutes IS NULL OR p_duration_minutes NOT BETWEEN 5 AND 480 THEN
|
|
428
|
-
RAISE EXCEPTION 'invalid duration';
|
|
429
|
-
END IF;
|
|
430
|
-
IF p_slot_interval IS NULL OR p_slot_interval NOT BETWEEN 5 AND 240 THEN
|
|
431
|
-
RAISE EXCEPTION 'invalid interval';
|
|
432
|
-
END IF;
|
|
433
|
-
IF p_date IS NULL OR p_date < current_date OR p_date > current_date + 90 THEN
|
|
434
|
-
RETURN;
|
|
435
|
-
END IF;
|
|
436
|
-
|
|
437
|
-
SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz
|
|
438
|
-
FROM tenants t WHERE t.id = p_tenant_id;
|
|
439
|
-
IF v_tz IS NULL THEN RETURN; END IF; -- unknown tenant → empty, not an error
|
|
440
|
-
|
|
441
|
-
RETURN QUERY
|
|
442
|
-
WITH day_schedules AS (
|
|
443
|
-
SELECT sc.assignee_id, sc.starts_at AS win_start, sc.ends_at AS win_end
|
|
444
|
-
FROM schedules sc
|
|
445
|
-
WHERE sc.tenant_id = p_tenant_id
|
|
446
|
-
AND sc.kind = 'working_hours'
|
|
447
|
-
AND sc.is_active
|
|
448
|
-
AND (p_assignee_id IS NULL OR sc.assignee_id = p_assignee_id)
|
|
449
|
-
AND (
|
|
450
|
-
sc.specific_date = p_date
|
|
451
|
-
OR (sc.specific_date IS NULL AND sc.day_of_week = EXTRACT(dow FROM p_date)::smallint)
|
|
452
|
-
)
|
|
453
|
-
),
|
|
454
|
-
candidate AS (
|
|
455
|
-
SELECT
|
|
456
|
-
ds.assignee_id,
|
|
457
|
-
gs AS s_start,
|
|
458
|
-
gs + make_interval(mins => p_duration_minutes) AS s_end
|
|
459
|
-
FROM day_schedules ds
|
|
460
|
-
CROSS JOIN LATERAL generate_series(
|
|
461
|
-
(p_date + ds.win_start) AT TIME ZONE v_tz,
|
|
462
|
-
((p_date + ds.win_end) AT TIME ZONE v_tz) - make_interval(mins => p_duration_minutes),
|
|
463
|
-
make_interval(mins => p_slot_interval)
|
|
464
|
-
) AS gs
|
|
465
|
-
)
|
|
466
|
-
SELECT DISTINCT c.s_start, c.s_end
|
|
467
|
-
FROM candidate c
|
|
468
|
-
WHERE NOT EXISTS (
|
|
469
|
-
SELECT 1 FROM appointments b
|
|
470
|
-
WHERE b.tenant_id = p_tenant_id
|
|
471
|
-
-- SMART SYNC: match this professional's own bookings OR any tenant-wide
|
|
472
|
-
-- BLOCK (assignee_id IS NULL AND kind='block', e.g. imported from Google
|
|
473
|
-
-- with a whole-tenant channel) — the latter busies the slot for every
|
|
474
|
-
-- professional. Assignee-less rows of other kinds (an unassigned admin
|
|
475
|
-
-- appointment) keep the 001 behaviour and do not block anyone.
|
|
476
|
-
AND (b.assignee_id = c.assignee_id
|
|
477
|
-
OR (b.assignee_id IS NULL AND b.kind = 'block'))
|
|
478
|
-
AND b.status NOT IN ('cancelled', 'no_show')
|
|
479
|
-
AND b.starts_at < c.s_end
|
|
480
|
-
AND COALESCE(b.ends_at, b.starts_at) > c.s_start
|
|
481
|
-
)
|
|
482
|
-
ORDER BY 1;
|
|
483
|
-
END;
|
|
484
|
-
$$;
|
|
485
|
-
|
|
486
|
-
REVOKE ALL ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer) FROM public;
|
|
487
|
-
GRANT EXECUTE ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer)
|
|
488
|
-
TO anon, authenticated, service_role;
|
|
489
|
-
|
|
490
|
-
-- ===========================================================================
|
|
491
|
-
-- §6 — Inbound schedule (optional) — per-channel incremental pull.
|
|
492
|
-
-- Supersedes the single-calendar template in 001. Enable once per project after
|
|
493
|
-
-- setting app.gcal_sync_url / app.gcal_service_key (see 001 header). The edge
|
|
494
|
-
-- function fans out over active channels, using each channel's sync_token cursor:
|
|
495
|
-
--
|
|
496
|
-
-- SELECT cron.schedule('gcal-pull', '*/10 * * * *', $CRON$
|
|
497
|
-
-- SELECT net.http_post(
|
|
498
|
-
-- url := current_setting('app.gcal_sync_url'),
|
|
499
|
-
-- headers := jsonb_build_object(
|
|
500
|
-
-- 'Content-Type','application/json',
|
|
501
|
-
-- 'Authorization','Bearer '||current_setting('app.gcal_service_key')),
|
|
502
|
-
-- body := jsonb_build_object('action','pull_events'))
|
|
503
|
-
-- $CRON$);
|
|
504
|
-
--
|
|
505
|
-
-- Or drive one row per channel from SQL (tighter scoping / per-channel cadence):
|
|
506
|
-
--
|
|
507
|
-
-- SELECT cron.schedule('gcal-pull-'||ch.id::text, '*/10 * * * *', format($CRON$
|
|
508
|
-
-- SELECT net.http_post(
|
|
509
|
-
-- url := current_setting('app.gcal_sync_url'),
|
|
510
|
-
-- headers := jsonb_build_object(
|
|
511
|
-
-- 'Content-Type','application/json',
|
|
512
|
-
-- 'Authorization','Bearer '||current_setting('app.gcal_service_key')),
|
|
513
|
-
-- body := jsonb_build_object('action','pull_events','channelId',%L))
|
|
514
|
-
-- $CRON$, ch.id))
|
|
515
|
-
-- FROM public.plg_calendar_channels ch
|
|
516
|
-
-- WHERE ch.is_active AND ch.direction IN ('inbound','bidirectional');
|
|
517
|
-
-- ===========================================================================
|