@fayz-ai/plugin-agenda 0.3.0 → 0.8.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 +2 -0
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/index.cjs +77 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +77 -65
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +8 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
- package/dist/public/index.cjs +16 -16
- package/dist/public/index.cjs.map +1 -1
- package/dist/public/index.js +17 -17
- package/dist/public/index.js.map +1 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -14
- package/src/bridges/create-financial-bridge.ts +6 -8
- package/src/components/AppointmentModal.tsx +4 -4
- package/src/config.ts +2 -2
- package/src/data/supabase.ts +26 -25
- package/src/index.ts +1 -1
- package/src/integrations/google-calendar/index.ts +1 -1
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +6 -6
- package/src/migrations/000b_v_bookings_rename.sql +21 -0
- package/src/migrations/000c_dedupe_customer_phones.sql +43 -0
- package/src/migrations/001_public_booking.sql +40 -27
- package/src/migrations/002_v_bookings_compat.sql +14 -0
- package/src/migrations/index.ts +134 -29
- package/src/public/createPublicBookingPlugin.ts +23 -20
- package/src/public/data.supabase.ts +4 -4
- package/src/registries.ts +1 -2
- package/src/types.ts +4 -4
package/dist/index.js
CHANGED
|
@@ -1721,7 +1721,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1721
1721
|
for (const [k, v] of Object.entries(data)) {
|
|
1722
1722
|
dbData[k.replace(/[A-Z]/g, (m) => "_" + m.toLowerCase())] = v;
|
|
1723
1723
|
}
|
|
1724
|
-
await supabase.
|
|
1724
|
+
await supabase.from("people").update(dbData).eq("id", clientId);
|
|
1725
1725
|
setClientData({ ...clientData, ...data });
|
|
1726
1726
|
setClientEditing(false);
|
|
1727
1727
|
} catch {
|
|
@@ -1881,7 +1881,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1881
1881
|
const supabase = getSupabaseClientOptional4();
|
|
1882
1882
|
const tenantId = getAgendaTenantId2();
|
|
1883
1883
|
if (!supabase || !tenantId) throw new Error("Not initialized");
|
|
1884
|
-
const { data: personRow, error: personError } = await supabase.
|
|
1884
|
+
const { data: personRow, error: personError } = await supabase.from("people").insert({
|
|
1885
1885
|
tenant_id: tenantId,
|
|
1886
1886
|
kind: config.clientKind,
|
|
1887
1887
|
name: newClientName.trim(),
|
|
@@ -1894,7 +1894,7 @@ function AppointmentModal({ open, mode, bookingId, prefill, initialTab, external
|
|
|
1894
1894
|
tenant_id: tenantId
|
|
1895
1895
|
});
|
|
1896
1896
|
if (clientError) {
|
|
1897
|
-
await supabase.
|
|
1897
|
+
await supabase.from("people").delete().eq("id", personRow.id);
|
|
1898
1898
|
throw clientError;
|
|
1899
1899
|
}
|
|
1900
1900
|
setClientId(personRow.id);
|
|
@@ -3474,7 +3474,7 @@ function snakeToCamel(obj) {
|
|
|
3474
3474
|
function getClients() {
|
|
3475
3475
|
const supabase = getSupabaseClientOptional();
|
|
3476
3476
|
if (!supabase) throw new Error("Supabase not initialized");
|
|
3477
|
-
return { core: supabase
|
|
3477
|
+
return { core: supabase, pub: supabase };
|
|
3478
3478
|
}
|
|
3479
3479
|
function mapBooking(row) {
|
|
3480
3480
|
return snakeToCamel(row);
|
|
@@ -3531,7 +3531,7 @@ async function hydrateCanonicalBookings(core, bookings) {
|
|
|
3531
3531
|
{ data: orders, error: ordersError },
|
|
3532
3532
|
{ data: items, error: itemsError }
|
|
3533
3533
|
] = await Promise.all([
|
|
3534
|
-
personIds.length ? core.from("
|
|
3534
|
+
personIds.length ? core.from("people").select("id, name, phone, email, avatar_url").in("id", personIds) : Promise.resolve({ data: [], error: null }),
|
|
3535
3535
|
locationIds.length ? core.from("locations").select("id, name").in("id", locationIds) : Promise.resolve({ data: [], error: null }),
|
|
3536
3536
|
orderIds.length ? core.from("orders").select("id, total, status").in("id", orderIds) : Promise.resolve({ data: [], error: null }),
|
|
3537
3537
|
orderIds.length ? core.from("order_items").select("id, order_id, service_id, name, duration_minutes, unit_price, assignee_id, sort_order").in("order_id", orderIds) : Promise.resolve({ data: [], error: null })
|
|
@@ -3589,7 +3589,7 @@ async function hydrateCanonicalBookings(core, bookings) {
|
|
|
3589
3589
|
});
|
|
3590
3590
|
}
|
|
3591
3591
|
async function getCanonicalBookings(core, query) {
|
|
3592
|
-
let q = core.from("
|
|
3592
|
+
let q = core.from("appointments").select("id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
|
|
3593
3593
|
if (query.kind) q = q.eq("kind", query.kind);
|
|
3594
3594
|
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3595
3595
|
if (query.professionalIds?.length) q = q.in("assignee_id", query.professionalIds);
|
|
@@ -3600,10 +3600,10 @@ async function getCanonicalBookings(core, query) {
|
|
|
3600
3600
|
}
|
|
3601
3601
|
async function getCanonicalBookingById(core, id) {
|
|
3602
3602
|
const columns = "id, tenant_id, kind, starts_at, ends_at, status, notes, order_id, location_id, metadata, party_id, assignee_id, created_at, updated_at";
|
|
3603
|
-
let { data, error } = await core.from("
|
|
3603
|
+
let { data, error } = await core.from("appointments").select(columns).eq("id", id).maybeSingle();
|
|
3604
3604
|
if (error) throw error;
|
|
3605
3605
|
if (!data) {
|
|
3606
|
-
const fallback = await core.from("
|
|
3606
|
+
const fallback = await core.from("appointments").select(columns).eq("order_id", id).maybeSingle();
|
|
3607
3607
|
if (fallback.error) throw fallback.error;
|
|
3608
3608
|
data = fallback.data;
|
|
3609
3609
|
}
|
|
@@ -3613,10 +3613,10 @@ async function getCanonicalBookingById(core, id) {
|
|
|
3613
3613
|
}
|
|
3614
3614
|
function createSupabaseAgendaProvider() {
|
|
3615
3615
|
return {
|
|
3616
|
-
//
|
|
3616
|
+
// v_appointments is in public schema (PostgREST default)
|
|
3617
3617
|
async getBookings(query) {
|
|
3618
3618
|
const { core, pub } = getClients();
|
|
3619
|
-
let q = pub.from("
|
|
3619
|
+
let q = pub.from("v_appointments").select("*").gte("starts_at", query.dateRange.start).lte("starts_at", query.dateRange.end);
|
|
3620
3620
|
if (query.kind) q = q.eq("kind", query.kind);
|
|
3621
3621
|
if (query.statuses?.length) q = q.in("status", query.statuses);
|
|
3622
3622
|
if (query.professionalIds?.length) q = q.in("professional_id", query.professionalIds);
|
|
@@ -3630,7 +3630,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3630
3630
|
},
|
|
3631
3631
|
async getBookingById(id) {
|
|
3632
3632
|
const { core, pub } = getClients();
|
|
3633
|
-
const { data, error } = await pub.from("
|
|
3633
|
+
const { data, error } = await pub.from("v_appointments").select("*").eq("id", id).maybeSingle();
|
|
3634
3634
|
if (error) throw error;
|
|
3635
3635
|
if (data) return mapBooking(data);
|
|
3636
3636
|
return getCanonicalBookingById(core, id);
|
|
@@ -3646,7 +3646,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3646
3646
|
const serviceNames = input.services.map((s) => s.name).join(", ");
|
|
3647
3647
|
let contactName;
|
|
3648
3648
|
if (input.clientId) {
|
|
3649
|
-
const { data: person } = await core.from("
|
|
3649
|
+
const { data: person } = await core.from("people").select("name").eq("id", input.clientId).single();
|
|
3650
3650
|
contactName = person?.name;
|
|
3651
3651
|
}
|
|
3652
3652
|
const eventTitle = input.title?.trim() || void 0;
|
|
@@ -3668,7 +3668,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3668
3668
|
}
|
|
3669
3669
|
}).select("id").single();
|
|
3670
3670
|
if (orderErr) throw orderErr;
|
|
3671
|
-
const { data: booking, error: bookingErr } = await core.from("
|
|
3671
|
+
const { data: booking, error: bookingErr } = await core.from("appointments").insert({
|
|
3672
3672
|
tenant_id: tenantId,
|
|
3673
3673
|
kind: input.kind ?? "appointment",
|
|
3674
3674
|
party_id: input.clientId || null,
|
|
@@ -3708,7 +3708,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3708
3708
|
},
|
|
3709
3709
|
async updateBooking(id, data) {
|
|
3710
3710
|
const { core } = getClients();
|
|
3711
|
-
const { data: existing, error: existingErr } = await core.from("
|
|
3711
|
+
const { data: existing, error: existingErr } = await core.from("appointments").select("id, order_id, starts_at, assignee_id, metadata").eq("id", id).single();
|
|
3712
3712
|
if (existingErr) throw existingErr;
|
|
3713
3713
|
const updates = {};
|
|
3714
3714
|
if (data.startsAt) updates.starts_at = data.startsAt;
|
|
@@ -3733,7 +3733,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3733
3733
|
}
|
|
3734
3734
|
}
|
|
3735
3735
|
const effectiveAssigneeId = data.professionalId ?? existing?.assignee_id ?? null;
|
|
3736
|
-
const { error } = await core.from("
|
|
3736
|
+
const { error } = await core.from("appointments").update(updates).eq("id", id);
|
|
3737
3737
|
if (error) throw error;
|
|
3738
3738
|
const orderId = existing?.order_id;
|
|
3739
3739
|
if (orderId) {
|
|
@@ -3774,21 +3774,21 @@ function createSupabaseAgendaProvider() {
|
|
|
3774
3774
|
},
|
|
3775
3775
|
async deleteBooking(id) {
|
|
3776
3776
|
const { core, pub } = getClients();
|
|
3777
|
-
const { data: booking, error: bookingErr } = await core.from("
|
|
3777
|
+
const { data: booking, error: bookingErr } = await core.from("appointments").select("order_id").eq("id", id).single();
|
|
3778
3778
|
if (bookingErr) throw bookingErr;
|
|
3779
3779
|
const orderId = booking?.order_id;
|
|
3780
|
-
const { count, error: movementErr } = orderId ? await pub.from("
|
|
3780
|
+
const { count, error: movementErr } = orderId ? await pub.from("plg_financial_movements").select("id", { count: "exact", head: true }).eq("invoice_id", orderId) : { count: 0, error: null };
|
|
3781
3781
|
if (movementErr) throw movementErr;
|
|
3782
3782
|
if (orderId && (count ?? 0) > 0) {
|
|
3783
|
-
await pub.from("
|
|
3783
|
+
await pub.from("plg_financial_movements").update({ status: "cancelled" }).eq("invoice_id", orderId);
|
|
3784
3784
|
await core.from("orders").update({ status: "cancelled" }).eq("id", orderId);
|
|
3785
|
-
const { error } = await core.from("
|
|
3785
|
+
const { error } = await core.from("appointments").update({ status: "cancelled" }).eq("id", id);
|
|
3786
3786
|
if (error) throw error;
|
|
3787
3787
|
} else {
|
|
3788
3788
|
if (orderId) {
|
|
3789
|
-
await pub.from("
|
|
3789
|
+
await pub.from("plg_financial_movements").delete().eq("invoice_id", orderId);
|
|
3790
3790
|
}
|
|
3791
|
-
const { error } = await core.from("
|
|
3791
|
+
const { error } = await core.from("appointments").delete().eq("id", id);
|
|
3792
3792
|
if (error) throw error;
|
|
3793
3793
|
if (orderId) {
|
|
3794
3794
|
const { error: orderErr } = await core.from("orders").delete().eq("id", orderId);
|
|
@@ -3801,7 +3801,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3801
3801
|
},
|
|
3802
3802
|
async checkConflict(params) {
|
|
3803
3803
|
const { core } = getClients();
|
|
3804
|
-
let q = core.from("
|
|
3804
|
+
let q = core.from("appointments").select("id", { count: "exact", head: true }).eq("assignee_id", params.assigneeId).eq("kind", "appointment").not("status", "in", '("cancelled","no_show")').not("starts_at", "is", null).lt("starts_at", params.endsAt).gt("ends_at", params.startsAt);
|
|
3805
3805
|
if (params.excludeBookingId) {
|
|
3806
3806
|
q = q.neq("id", params.excludeBookingId);
|
|
3807
3807
|
}
|
|
@@ -3856,7 +3856,7 @@ function createSupabaseAgendaProvider() {
|
|
|
3856
3856
|
},
|
|
3857
3857
|
async getProfessionals() {
|
|
3858
3858
|
const { core } = getClients();
|
|
3859
|
-
const { data, error } = await core.from("
|
|
3859
|
+
const { data, error } = await core.from("people").select("id, name, avatar_url, is_active").eq("kind", "staff").eq("is_active", true).order("name");
|
|
3860
3860
|
if (error) throw error;
|
|
3861
3861
|
return (data ?? []).map((p) => ({
|
|
3862
3862
|
id: p.id,
|
|
@@ -3878,23 +3878,23 @@ function createSupabaseAgendaProvider() {
|
|
|
3878
3878
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3879
3879
|
const cutoff = /* @__PURE__ */ new Date();
|
|
3880
3880
|
cutoff.setDate(cutoff.getDate() + daysAhead);
|
|
3881
|
-
const { data, error } = await pub.from("
|
|
3881
|
+
const { data, error } = await pub.from("v_appointments").select("*").eq("status", "scheduled").gte("starts_at", now).lte("starts_at", cutoff.toISOString()).order("starts_at");
|
|
3882
3882
|
if (error) throw error;
|
|
3883
3883
|
return (data ?? []).map(mapBooking);
|
|
3884
3884
|
},
|
|
3885
3885
|
async sendConfirmation(bookingId, channel) {
|
|
3886
3886
|
const { core } = getClients();
|
|
3887
|
-
const { data: booking } = await core.from("
|
|
3887
|
+
const { data: booking } = await core.from("appointments").select("metadata").eq("id", bookingId).single();
|
|
3888
3888
|
const metadata = booking?.metadata ?? {};
|
|
3889
3889
|
const confirmations = metadata.confirmations ?? [];
|
|
3890
3890
|
confirmations.push({ channel, sentAt: (/* @__PURE__ */ new Date()).toISOString(), status: "sent" });
|
|
3891
|
-
await core.from("
|
|
3891
|
+
await core.from("appointments").update({ metadata: { ...metadata, confirmations } }).eq("id", bookingId);
|
|
3892
3892
|
},
|
|
3893
3893
|
async completeBooking(bookingId) {
|
|
3894
3894
|
const { core } = getClients();
|
|
3895
|
-
const { data: existing, error: existingErr } = await core.from("
|
|
3895
|
+
const { data: existing, error: existingErr } = await core.from("appointments").select("order_id").eq("id", bookingId).single();
|
|
3896
3896
|
if (existingErr) throw existingErr;
|
|
3897
|
-
const { error } = await core.from("
|
|
3897
|
+
const { error } = await core.from("appointments").update({ status: "completed" }).eq("id", bookingId);
|
|
3898
3898
|
if (error) throw error;
|
|
3899
3899
|
const orderId = existing?.order_id;
|
|
3900
3900
|
const booking = await this.getBookingById(bookingId);
|
|
@@ -4123,7 +4123,6 @@ var holidayEntity = {
|
|
|
4123
4123
|
],
|
|
4124
4124
|
data: {
|
|
4125
4125
|
table: "holidays",
|
|
4126
|
-
schema: "saas_core",
|
|
4127
4126
|
tenantScoped: true
|
|
4128
4127
|
}
|
|
4129
4128
|
};
|
|
@@ -4657,36 +4656,49 @@ var MIGRATION_001_PUBLIC_BOOKING = `-- =========================================
|
|
|
4657
4656
|
-- plugin-agenda 001: booking read model + public (anon) booking surface
|
|
4658
4657
|
--
|
|
4659
4658
|
-- Ships the objects the agenda providers reference but the spine (packages/db
|
|
4660
|
-
-- migrations 001\
|
|
4659
|
+
-- migrations 001\u2013010) does not define:
|
|
4660
|
+
-- \xA70 converted-pool cleanup \u2014 drop the pre-rename v_bookings view / indexes
|
|
4661
4661
|
-- \xA71 order_items columns the admin provider writes (duration/assignee)
|
|
4662
|
-
-- \xA72 public.
|
|
4662
|
+
-- \xA72 public.v_appointments \u2014 admin read model (authenticated, RLS applies)
|
|
4663
4663
|
-- \xA73 public.v_public_services \u2014 anon-readable service catalog
|
|
4664
4664
|
-- \xA74 public.get_available_slots \u2014 anon RPC (mirrors the mock algorithm)
|
|
4665
4665
|
-- \xA75 public.create_public_booking \u2014 anon RPC (validated, race-safe write)
|
|
4666
4666
|
--
|
|
4667
|
+
-- Core v1 lives directly in PUBLIC (no separate core schema): people (was
|
|
4668
|
+
-- persons), appointments (was bookings). This migration is written against public
|
|
4669
|
+
-- names and is idempotent + safe on both fresh and converted pools.
|
|
4670
|
+
--
|
|
4667
4671
|
-- Anon-access design: public booking pages run with the publishable key and no
|
|
4668
4672
|
-- session, so the canonical tenant RLS yields nothing. Anon access is granted
|
|
4669
4673
|
-- ONLY via the column-whitelisted view (\xA73) and SECURITY DEFINER RPCs (\xA74, \xA75)
|
|
4670
4674
|
-- with tight input validation and hardcoded statuses.
|
|
4671
4675
|
-- ============================================================================
|
|
4672
4676
|
|
|
4677
|
+
-- \xA70 \u2014 converted-pool cleanup. The pre-industry-pool shape shipped a v_bookings
|
|
4678
|
+
-- view and persons_/bookings_-named indexes; drop them so this file re-emits the
|
|
4679
|
+
-- canonical v_appointments view + people_/appointments_ indexes without leaving
|
|
4680
|
+
-- duplicates. All guarded with IF EXISTS \u2192 no-ops on a fresh pool.
|
|
4681
|
+
DROP VIEW IF EXISTS public.v_bookings;
|
|
4682
|
+
DROP INDEX IF EXISTS public.persons_tenant_customer_phone;
|
|
4683
|
+
DROP INDEX IF EXISTS public.bookings_assignee_time;
|
|
4684
|
+
|
|
4673
4685
|
-- \xA71 \u2014 columns the agenda provider writes but 004_archetypes lacks
|
|
4674
|
-
ALTER TABLE
|
|
4686
|
+
ALTER TABLE public.order_items
|
|
4675
4687
|
ADD COLUMN IF NOT EXISTS duration_minutes integer,
|
|
4676
|
-
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES
|
|
4688
|
+
ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES public.people(id) ON DELETE SET NULL;
|
|
4677
4689
|
|
|
4678
4690
|
-- Upsert key for public customers (per tenant, by normalized phone)
|
|
4679
|
-
CREATE UNIQUE INDEX IF NOT EXISTS
|
|
4680
|
-
ON
|
|
4691
|
+
CREATE UNIQUE INDEX IF NOT EXISTS people_tenant_customer_phone
|
|
4692
|
+
ON public.people(tenant_id, phone)
|
|
4681
4693
|
WHERE kind = 'customer' AND phone IS NOT NULL;
|
|
4682
4694
|
|
|
4683
4695
|
-- Slot-conflict scan
|
|
4684
|
-
CREATE INDEX IF NOT EXISTS
|
|
4685
|
-
ON
|
|
4696
|
+
CREATE INDEX IF NOT EXISTS appointments_assignee_time
|
|
4697
|
+
ON public.appointments(assignee_id, starts_at, ends_at)
|
|
4686
4698
|
WHERE status NOT IN ('cancelled', 'no_show');
|
|
4687
4699
|
|
|
4688
4700
|
-- \xA72 \u2014 admin read model (security_invoker \u2192 underlying RLS applies; anon sees 0 rows)
|
|
4689
|
-
CREATE OR REPLACE VIEW public.
|
|
4701
|
+
CREATE OR REPLACE VIEW public.v_appointments
|
|
4690
4702
|
WITH (security_invoker = true) AS
|
|
4691
4703
|
SELECT
|
|
4692
4704
|
b.id, b.tenant_id, b.kind, b.starts_at, b.ends_at, b.status, b.notes,
|
|
@@ -4704,14 +4716,14 @@ SELECT
|
|
|
4704
4716
|
o.status AS order_status,
|
|
4705
4717
|
(EXTRACT(EPOCH FROM (COALESCE(b.ends_at, b.starts_at) - b.starts_at))::int / 60)
|
|
4706
4718
|
AS total_duration_minutes
|
|
4707
|
-
FROM
|
|
4708
|
-
LEFT JOIN
|
|
4709
|
-
LEFT JOIN
|
|
4710
|
-
LEFT JOIN
|
|
4711
|
-
LEFT JOIN
|
|
4719
|
+
FROM public.appointments b
|
|
4720
|
+
LEFT JOIN public.people pc ON pc.id = b.party_id
|
|
4721
|
+
LEFT JOIN public.people ps ON ps.id = b.assignee_id
|
|
4722
|
+
LEFT JOIN public.locations l ON l.id = b.location_id
|
|
4723
|
+
LEFT JOIN public.orders o ON o.id = b.order_id;
|
|
4712
4724
|
|
|
4713
|
-
GRANT SELECT ON public.
|
|
4714
|
-
REVOKE SELECT ON public.
|
|
4725
|
+
GRANT SELECT ON public.v_appointments TO authenticated, service_role;
|
|
4726
|
+
REVOKE SELECT ON public.v_appointments FROM anon;
|
|
4715
4727
|
|
|
4716
4728
|
-- \xA73 \u2014 anon-readable public catalog (owner-rights view = deliberate, whitelisted)
|
|
4717
4729
|
CREATE OR REPLACE VIEW public.v_public_services AS
|
|
@@ -4720,7 +4732,7 @@ SELECT
|
|
|
4720
4732
|
COALESCE(s.duration_minutes, 30) AS duration_minutes,
|
|
4721
4733
|
s.image_url,
|
|
4722
4734
|
COALESCE((s.metadata->>'sort_order')::int, 0) AS sort_order
|
|
4723
|
-
FROM
|
|
4735
|
+
FROM public.services s
|
|
4724
4736
|
WHERE s.is_active AND s.status = 'active';
|
|
4725
4737
|
|
|
4726
4738
|
GRANT SELECT ON public.v_public_services TO anon, authenticated, service_role;
|
|
@@ -4734,7 +4746,7 @@ CREATE OR REPLACE FUNCTION public.get_available_slots(
|
|
|
4734
4746
|
p_slot_interval integer DEFAULT 30
|
|
4735
4747
|
) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)
|
|
4736
4748
|
LANGUAGE plpgsql STABLE SECURITY DEFINER
|
|
4737
|
-
SET search_path =
|
|
4749
|
+
SET search_path = public
|
|
4738
4750
|
AS $$
|
|
4739
4751
|
DECLARE
|
|
4740
4752
|
v_tz text;
|
|
@@ -4782,7 +4794,7 @@ BEGIN
|
|
|
4782
4794
|
SELECT DISTINCT c.s_start, c.s_end
|
|
4783
4795
|
FROM candidate c
|
|
4784
4796
|
WHERE NOT EXISTS (
|
|
4785
|
-
SELECT 1 FROM
|
|
4797
|
+
SELECT 1 FROM appointments b
|
|
4786
4798
|
WHERE b.tenant_id = p_tenant_id
|
|
4787
4799
|
AND b.assignee_id = c.assignee_id
|
|
4788
4800
|
AND b.status NOT IN ('cancelled', 'no_show')
|
|
@@ -4808,7 +4820,7 @@ CREATE OR REPLACE FUNCTION public.create_public_booking(
|
|
|
4808
4820
|
p_notes text DEFAULT NULL
|
|
4809
4821
|
) RETURNS TABLE (booking_id uuid, starts_at timestamptz, ends_at timestamptz)
|
|
4810
4822
|
LANGUAGE plpgsql SECURITY DEFINER
|
|
4811
|
-
SET search_path =
|
|
4823
|
+
SET search_path = public
|
|
4812
4824
|
AS $$
|
|
4813
4825
|
DECLARE
|
|
4814
4826
|
v_service record;
|
|
@@ -4870,7 +4882,7 @@ BEGIN
|
|
|
4870
4882
|
PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || v_assignee::text, 0));
|
|
4871
4883
|
|
|
4872
4884
|
IF EXISTS (
|
|
4873
|
-
SELECT 1 FROM
|
|
4885
|
+
SELECT 1 FROM appointments b
|
|
4874
4886
|
WHERE b.tenant_id = p_tenant_id
|
|
4875
4887
|
AND b.assignee_id = v_assignee
|
|
4876
4888
|
AND b.status NOT IN ('cancelled', 'no_show')
|
|
@@ -4882,8 +4894,8 @@ BEGIN
|
|
|
4882
4894
|
|
|
4883
4895
|
-- anti-spam: cap open future public bookings per phone per tenant
|
|
4884
4896
|
IF (
|
|
4885
|
-
SELECT count(*) FROM
|
|
4886
|
-
JOIN
|
|
4897
|
+
SELECT count(*) FROM appointments b
|
|
4898
|
+
JOIN people p ON p.id = b.party_id
|
|
4887
4899
|
WHERE b.tenant_id = p_tenant_id
|
|
4888
4900
|
AND p.phone = v_phone
|
|
4889
4901
|
AND b.starts_at > now()
|
|
@@ -4892,14 +4904,14 @@ BEGIN
|
|
|
4892
4904
|
RAISE EXCEPTION 'too many open bookings for this phone';
|
|
4893
4905
|
END IF;
|
|
4894
4906
|
|
|
4895
|
-
-- upsert the customer by (tenant, phone) \u2014 matches
|
|
4896
|
-
INSERT INTO
|
|
4907
|
+
-- upsert the customer by (tenant, phone) \u2014 matches people_tenant_customer_phone
|
|
4908
|
+
INSERT INTO people (tenant_id, kind, name, phone, email, metadata)
|
|
4897
4909
|
VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
|
|
4898
4910
|
jsonb_build_object('source', 'public_booking'))
|
|
4899
4911
|
ON CONFLICT (tenant_id, phone) WHERE kind = 'customer' AND phone IS NOT NULL
|
|
4900
4912
|
DO UPDATE SET
|
|
4901
4913
|
name = EXCLUDED.name,
|
|
4902
|
-
email = COALESCE(EXCLUDED.email,
|
|
4914
|
+
email = COALESCE(EXCLUDED.email, people.email),
|
|
4903
4915
|
updated_at = now()
|
|
4904
4916
|
RETURNING id INTO v_person_id;
|
|
4905
4917
|
|
|
@@ -4912,7 +4924,7 @@ BEGIN
|
|
|
4912
4924
|
'contactName', trim(p_name)))
|
|
4913
4925
|
RETURNING id INTO v_order;
|
|
4914
4926
|
|
|
4915
|
-
INSERT INTO
|
|
4927
|
+
INSERT INTO appointments (tenant_id, kind, party_id, assignee_id, order_id,
|
|
4916
4928
|
starts_at, ends_at, status, notes, metadata)
|
|
4917
4929
|
VALUES (p_tenant_id, 'appointment', v_person_id, v_assignee, v_order,
|
|
4918
4930
|
p_starts_at, v_ends, 'scheduled', v_notes,
|
|
@@ -4959,8 +4971,8 @@ function createFinancialBridge(provider2) {
|
|
|
4959
4971
|
if (!supabase) return result;
|
|
4960
4972
|
try {
|
|
4961
4973
|
const [{ data: orders }, { data: movements }] = await Promise.all([
|
|
4962
|
-
supabase.
|
|
4963
|
-
supabase.from("
|
|
4974
|
+
supabase.from("orders").select("id, kind, status, total, reference_number, metadata").in("id", orderIds),
|
|
4975
|
+
supabase.from("plg_financial_movements").select("invoice_id, status, paid_amount").in("invoice_id", orderIds)
|
|
4964
4976
|
]);
|
|
4965
4977
|
const movementsByOrderId = /* @__PURE__ */ new Map();
|
|
4966
4978
|
for (const movement of movements ?? []) {
|
|
@@ -4988,8 +5000,8 @@ function createFinancialBridge(provider2) {
|
|
|
4988
5000
|
const supabase = getSupabaseClientOptional();
|
|
4989
5001
|
if (!supabase) return null;
|
|
4990
5002
|
const [orderRes, movRes] = await Promise.all([
|
|
4991
|
-
supabase.
|
|
4992
|
-
supabase.from("
|
|
5003
|
+
supabase.from("orders").select("id, status, total, reference_number, metadata").eq("id", orderId).single(),
|
|
5004
|
+
supabase.from("plg_financial_movements").select("*").eq("invoice_id", orderId).order("due_date")
|
|
4993
5005
|
]);
|
|
4994
5006
|
const order = orderRes.data;
|
|
4995
5007
|
if (!order) return null;
|
|
@@ -4997,7 +5009,7 @@ function createFinancialBridge(provider2) {
|
|
|
4997
5009
|
const typeIds = [...new Set((movRes.data ?? []).map((m) => m.payment_method_type_id).filter(Boolean))];
|
|
4998
5010
|
let typeNames = {};
|
|
4999
5011
|
if (typeIds.length > 0) {
|
|
5000
|
-
const { data: types } = await supabase.from("
|
|
5012
|
+
const { data: types } = await supabase.from("plg_financial_payment_method_types").select("id, name").in("id", typeIds);
|
|
5001
5013
|
typeNames = Object.fromEntries((types ?? []).map((t) => [t.id, t.name]));
|
|
5002
5014
|
}
|
|
5003
5015
|
const movements = (movRes.data ?? []).map((m) => ({
|
|
@@ -5049,17 +5061,17 @@ function createFinancialBridge(provider2) {
|
|
|
5049
5061
|
async createCommissionMovement(orderId, options) {
|
|
5050
5062
|
const supabase = getSupabaseClientOptional();
|
|
5051
5063
|
if (!supabase) return null;
|
|
5052
|
-
const { data: order } = await supabase.
|
|
5064
|
+
const { data: order } = await supabase.from("orders").select("id, tenant_id, assignee_id, total").eq("id", orderId).single();
|
|
5053
5065
|
if (!order || !order.assignee_id) return null;
|
|
5054
5066
|
const { data: staff } = await supabase.from("v_staff").select("commission_rate").eq("id", order.assignee_id).single();
|
|
5055
5067
|
const rate = Number(staff?.commission_rate) || 0;
|
|
5056
5068
|
const base = Number(order.total) || 0;
|
|
5057
5069
|
const amount = computeCommissionAmount(base, rate);
|
|
5058
5070
|
if (amount <= 0) return null;
|
|
5059
|
-
const { data: existing } = await supabase.from("
|
|
5071
|
+
const { data: existing } = await supabase.from("plg_financial_movements").select("id").eq("invoice_id", orderId).eq("movement_kind", "commission").limit(1);
|
|
5060
5072
|
if (existing && existing.length > 0) return existing[0].id;
|
|
5061
5073
|
const dueDate = options?.dueDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5062
|
-
const { data: inserted } = await supabase.from("
|
|
5074
|
+
const { data: inserted } = await supabase.from("plg_financial_movements").insert({
|
|
5063
5075
|
tenant_id: order.tenant_id,
|
|
5064
5076
|
invoice_id: orderId,
|
|
5065
5077
|
direction: "debit",
|
|
@@ -5496,7 +5508,7 @@ function createAgendaPlugin(options) {
|
|
|
5496
5508
|
id: "agenda-001-public-booking",
|
|
5497
5509
|
version: "0.3.0",
|
|
5498
5510
|
sql: MIGRATION_001_PUBLIC_BOOKING,
|
|
5499
|
-
description: "Booking read model (
|
|
5511
|
+
description: "Booking read model (v_appointments) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns"
|
|
5500
5512
|
}
|
|
5501
5513
|
],
|
|
5502
5514
|
locales: agendaLocales
|