@fayz-ai/plugin-agenda 0.2.1 → 0.3.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.
Files changed (83) hide show
  1. package/dist/chunk-DRZF6CV2.js +421 -0
  2. package/dist/chunk-DRZF6CV2.js.map +1 -0
  3. package/dist/chunk-LFUELUIT.cjs +423 -0
  4. package/dist/chunk-LFUELUIT.cjs.map +1 -0
  5. package/dist/index.cjs +741 -10
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +741 -10
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrations/index.d.ts +2 -0
  11. package/dist/migrations/index.d.ts.map +1 -0
  12. package/dist/public/BookingWidget.d.ts +4 -2
  13. package/dist/public/BookingWidget.d.ts.map +1 -1
  14. package/dist/public/components/BrandHeader.d.ts +6 -0
  15. package/dist/public/components/BrandHeader.d.ts.map +1 -0
  16. package/dist/public/components/ServiceCard.d.ts +5 -0
  17. package/dist/public/components/ServiceCard.d.ts.map +1 -0
  18. package/dist/public/components/SidebarExtras.d.ts +4 -0
  19. package/dist/public/components/SidebarExtras.d.ts.map +1 -0
  20. package/dist/public/components/StepRailItem.d.ts +4 -0
  21. package/dist/public/components/StepRailItem.d.ts.map +1 -0
  22. package/dist/public/components/SuccessPanel.d.ts +4 -0
  23. package/dist/public/components/SuccessPanel.d.ts.map +1 -0
  24. package/dist/public/components/SummaryRail.d.ts +27 -0
  25. package/dist/public/components/SummaryRail.d.ts.map +1 -0
  26. package/dist/public/components/defaults.d.ts +3 -0
  27. package/dist/public/components/defaults.d.ts.map +1 -0
  28. package/dist/public/context.d.ts +22 -8
  29. package/dist/public/context.d.ts.map +1 -1
  30. package/dist/public/createPublicBookingPlugin.d.ts +27 -10
  31. package/dist/public/createPublicBookingPlugin.d.ts.map +1 -1
  32. package/dist/public/data.d.ts +37 -0
  33. package/dist/public/data.d.ts.map +1 -0
  34. package/dist/public/data.supabase.d.ts +7 -0
  35. package/dist/public/data.supabase.d.ts.map +1 -0
  36. package/dist/public/format.d.ts +18 -0
  37. package/dist/public/format.d.ts.map +1 -0
  38. package/dist/public/hooks.d.ts +13 -11
  39. package/dist/public/hooks.d.ts.map +1 -1
  40. package/dist/public/index.cjs +1016 -613
  41. package/dist/public/index.cjs.map +1 -1
  42. package/dist/public/index.d.ts +15 -4
  43. package/dist/public/index.d.ts.map +1 -1
  44. package/dist/public/index.js +1006 -616
  45. package/dist/public/index.js.map +1 -1
  46. package/dist/public/steps/ContactStep.d.ts +30 -0
  47. package/dist/public/steps/ContactStep.d.ts.map +1 -0
  48. package/dist/public/steps/DateTimeStep.d.ts +23 -0
  49. package/dist/public/steps/DateTimeStep.d.ts.map +1 -0
  50. package/dist/public/steps/OverviewStep.d.ts +16 -0
  51. package/dist/public/steps/OverviewStep.d.ts.map +1 -0
  52. package/dist/public/steps/PaymentStep.d.ts +20 -0
  53. package/dist/public/steps/PaymentStep.d.ts.map +1 -0
  54. package/dist/public/types.d.ts +134 -0
  55. package/dist/public/types.d.ts.map +1 -1
  56. package/package.json +12 -12
  57. package/src/index.ts +11 -0
  58. package/src/migrations/001_public_booking.sql +278 -0
  59. package/src/migrations/index.ts +282 -0
  60. package/src/public/BookingWidget.tsx +136 -713
  61. package/src/public/components/BrandHeader.tsx +27 -0
  62. package/src/public/components/ServiceCard.tsx +39 -0
  63. package/src/public/components/SidebarExtras.tsx +30 -0
  64. package/src/public/components/StepRailItem.tsx +27 -0
  65. package/src/public/components/SuccessPanel.tsx +27 -0
  66. package/src/public/components/SummaryRail.tsx +90 -0
  67. package/src/public/components/defaults.ts +14 -0
  68. package/src/public/context.tsx +64 -7
  69. package/src/public/createPublicBookingPlugin.ts +131 -21
  70. package/src/public/data.supabase.ts +94 -0
  71. package/src/public/data.ts +99 -0
  72. package/src/public/format.ts +61 -0
  73. package/src/public/hooks.ts +22 -34
  74. package/src/public/index.ts +57 -4
  75. package/src/public/steps/ContactStep.tsx +179 -0
  76. package/src/public/steps/DateTimeStep.tsx +138 -0
  77. package/src/public/steps/OverviewStep.tsx +81 -0
  78. package/src/public/steps/PaymentStep.tsx +180 -0
  79. package/src/public/types.ts +169 -3
  80. package/dist/chunk-GAJVIEXM.cjs +0 -870
  81. package/dist/chunk-GAJVIEXM.cjs.map +0 -1
  82. package/dist/chunk-U6VTLOHN.js +0 -867
  83. package/dist/chunk-U6VTLOHN.js.map +0 -1
@@ -0,0 +1,30 @@
1
+ import { type CountryDef } from '@fayz-ai/core';
2
+ import type { PublicBookingLabels, ResolvedPayment } from '../types';
3
+ export interface ContactDraft {
4
+ name: string;
5
+ phone: string;
6
+ email: string;
7
+ notes: string;
8
+ }
9
+ interface ContactStepProps {
10
+ labels: PublicBookingLabels;
11
+ contact: ContactDraft;
12
+ onContactChange: (contact: ContactDraft) => void;
13
+ contactStep: 'phone' | 'code' | 'details';
14
+ country: CountryDef;
15
+ onCountryChange: (iso2: string) => void;
16
+ code: string;
17
+ onCodeChange: (code: string) => void;
18
+ codeError: boolean;
19
+ onSendCode: () => void;
20
+ onConfirmCode: () => void;
21
+ onBackToPhone: () => void;
22
+ onSubmitDetails: () => void;
23
+ payment: ResolvedPayment | null;
24
+ submitting: boolean;
25
+ error: Error | null;
26
+ }
27
+ /** Contact step — phone verification first (Quaddro-style), then details. */
28
+ export declare function ContactStep({ labels, contact, onContactChange, contactStep, country, onCountryChange, code, onCodeChange, codeError, onSendCode, onConfirmCode, onBackToPhone, onSubmitDetails, payment, submitting, error, }: ContactStepProps): import("react").JSX.Element;
29
+ export {};
30
+ //# sourceMappingURL=ContactStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/ContactStep.tsx"],"names":[],"mappings":"AACA,OAAO,EAAqC,KAAK,UAAU,EAAE,MAAM,eAAe,CAAA;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAEpE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,OAAO,EAAE,YAAY,CAAA;IACrB,eAAe,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAA;IAChD,WAAW,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;IACzC,OAAO,EAAE,UAAU,CAAA;IACnB,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,IAAI,EAAE,MAAM,CAAA;IACZ,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,SAAS,EAAE,OAAO,CAAA;IAClB,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,aAAa,EAAE,MAAM,IAAI,CAAA;IACzB,eAAe,EAAE,MAAM,IAAI,CAAA;IAC3B,OAAO,EAAE,eAAe,GAAG,IAAI,CAAA;IAC/B,UAAU,EAAE,OAAO,CAAA;IACnB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;CACpB;AAED,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,EACvE,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EACvE,eAAe,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,GAC5C,EAAE,gBAAgB,+BA+IlB"}
@@ -0,0 +1,23 @@
1
+ import type { TimeSlot } from '../../types';
2
+ import type { DayCell } from '../format';
3
+ import type { BookingFormatters, PublicBookingLabels, WorkingHours } from '../types';
4
+ interface DateTimeStepProps {
5
+ labels: PublicBookingLabels;
6
+ fmt: BookingFormatters;
7
+ week: DayCell[];
8
+ weekOffset: number;
9
+ maxWeekOffset: number;
10
+ onWeekOffset: (updater: (w: number) => number) => void;
11
+ businessHours: WorkingHours;
12
+ date: string | null;
13
+ onSelectDate: (iso: string) => void;
14
+ slots: TimeSlot[];
15
+ slotsLoading: boolean;
16
+ slotStart: string | null;
17
+ onSelectSlot: (startIso: string) => void;
18
+ onContinue: () => void;
19
+ }
20
+ /** Week strip + available-slot grid. */
21
+ export declare function DateTimeStep({ labels, fmt, week, weekOffset, maxWeekOffset, onWeekOffset, businessHours, date, onSelectDate, slots, slotsLoading, slotStart, onSelectSlot, onContinue, }: DateTimeStepProps): import("react").JSX.Element;
22
+ export {};
23
+ //# sourceMappingURL=DateTimeStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DateTimeStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/DateTimeStep.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAEpF,UAAU,iBAAiB;IACzB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;IACtB,IAAI,EAAE,OAAO,EAAE,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,KAAK,IAAI,CAAA;IACtD,aAAa,EAAE,YAAY,CAAA;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,YAAY,EAAE,OAAO,CAAA;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACxC,UAAU,EAAE,MAAM,IAAI,CAAA;CACvB;AAED,wCAAwC;AACxC,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EACzE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,GAC7E,EAAE,iBAAiB,+BA+GnB"}
@@ -0,0 +1,16 @@
1
+ import type { BookingComponents, BookingFormatters, PublicBookingLabels, PublicService, ResolvedBookingBrand, WorkingHours } from '../types';
2
+ interface OverviewStepProps {
3
+ services: PublicService[];
4
+ servicesLoading: boolean;
5
+ businessHours: WorkingHours;
6
+ professionalName: string;
7
+ labels: PublicBookingLabels;
8
+ brand: ResolvedBookingBrand;
9
+ fmt: BookingFormatters;
10
+ components: BookingComponents;
11
+ onStartBooking: (serviceId?: string) => void;
12
+ }
13
+ /** Default view: services + opening hours + sidebar CTA (Quaddro-style). */
14
+ export declare function OverviewStep({ services, servicesLoading, businessHours, professionalName, labels, brand, fmt, components: C, onStartBooking, }: OverviewStepProps): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=OverviewStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OverviewStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/OverviewStep.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5I,UAAU,iBAAiB;IACzB,QAAQ,EAAE,aAAa,EAAE,CAAA;IACzB,eAAe,EAAE,OAAO,CAAA;IACxB,aAAa,EAAE,YAAY,CAAA;IAC3B,gBAAgB,EAAE,MAAM,CAAA;IACxB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,KAAK,EAAE,oBAAoB,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;IACtB,UAAU,EAAE,iBAAiB,CAAA;IAC7B,cAAc,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAC7C;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EAAE,eAAe,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,EAAE,cAAc,GAC9G,EAAE,iBAAiB,+BA6DnB"}
@@ -0,0 +1,20 @@
1
+ import type { PixCharge } from '@fayz-ai/core';
2
+ import type { BookingFormatters, PublicService, ResolvedPayment } from '../types';
3
+ declare const PIX_PAYLOAD = "00020126840014br.gov.bcb.pix2562qrcode.fayz.ai/pix/v2/9d8f7a6b-agendamento5204000053039865802BR5904FAYZ6009SAO PAULO62070503***6304AB12";
4
+ interface PaymentStepProps {
5
+ service: PublicService;
6
+ payment: ResolvedPayment;
7
+ paymentStep: 'choose' | 'pix';
8
+ charge: PixCharge | null;
9
+ busy: boolean;
10
+ error: Error | null;
11
+ copied: boolean;
12
+ fmt: BookingFormatters;
13
+ onPayWithPix: (fee: number) => void;
14
+ onCopy: () => void;
15
+ onSimulatePaid: () => void;
16
+ }
17
+ /** Payment step: order summary + Pix method → QR/copia-e-cola while polling. */
18
+ export declare function PaymentStep({ service, payment, paymentStep, charge, busy, error, copied, fmt, onPayWithPix, onCopy, onSimulatePaid, }: PaymentStepProps): import("react").JSX.Element;
19
+ export { PIX_PAYLOAD };
20
+ //# sourceMappingURL=PaymentStep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PaymentStep.d.ts","sourceRoot":"","sources":["../../../src/public/steps/PaymentStep.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAIjF,QAAA,MAAM,WAAW,4IAC0H,CAAA;AAqC3I,UAAU,gBAAgB;IACxB,OAAO,EAAE,aAAa,CAAA;IACtB,OAAO,EAAE,eAAe,CAAA;IACxB,WAAW,EAAE,QAAQ,GAAG,KAAK,CAAA;IAC7B,MAAM,EAAE,SAAS,GAAG,IAAI,CAAA;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;IACnB,MAAM,EAAE,OAAO,CAAA;IACf,GAAG,EAAE,iBAAiB,CAAA;IACtB,YAAY,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,cAAc,EAAE,MAAM,IAAI,CAAA;CAC3B;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,cAAc,GACtG,EAAE,gBAAgB,+BAoHlB;AAED,OAAO,EAAE,WAAW,EAAE,CAAA"}
@@ -1,3 +1,4 @@
1
+ import type { ComponentType, ReactNode } from 'react';
1
2
  export interface PublicService {
2
3
  id: string;
3
4
  name: string;
@@ -39,6 +40,98 @@ export interface ResolvedPayment {
39
40
  reservationFeePercent: number;
40
41
  method: 'pix';
41
42
  }
43
+ /** One icon+text bullet on the overview sidebar. */
44
+ export interface BookingHighlight {
45
+ icon?: ComponentType<{
46
+ className?: string;
47
+ }>;
48
+ text: string;
49
+ }
50
+ /** Brand/text-level personalization — no components needed for these. */
51
+ export interface BookingBrand {
52
+ /** e.g. "HempDent · Consulta Canábica". */
53
+ name?: string;
54
+ /** Avatar letter; defaults to name.charAt(0). Ignored when logoUrl is set. */
55
+ initials?: string;
56
+ logoUrl?: string;
57
+ /** Subheader line under the name. `null` hides the row. */
58
+ tagline?: string | null;
59
+ /** Icon on service rows. Default: lucide Video. */
60
+ serviceIcon?: ComponentType<{
61
+ className?: string;
62
+ }>;
63
+ /** Chip after the duration ("Online", "Presencial"…). `null` hides. */
64
+ serviceMeta?: string | null;
65
+ /** Badge on the first service. `null` disables. Default 'Mais procurado'. */
66
+ featuredBadge?: string | null;
67
+ /** Overview sidebar bullets (replaces the default generic ones). */
68
+ highlights?: BookingHighlight[];
69
+ }
70
+ export interface ResolvedBookingBrand {
71
+ name: string;
72
+ initials: string;
73
+ logoUrl?: string;
74
+ tagline: string | null;
75
+ serviceIcon: ComponentType<{
76
+ className?: string;
77
+ }>;
78
+ serviceMeta: string | null;
79
+ featuredBadge: string | null;
80
+ highlights: BookingHighlight[];
81
+ }
82
+ export interface BookingFormatters {
83
+ price: Intl.NumberFormat;
84
+ time: Intl.DateTimeFormat;
85
+ weekday: Intl.DateTimeFormat;
86
+ dayNum: Intl.DateTimeFormat;
87
+ longDay: Intl.DateTimeFormat;
88
+ dateShort: Intl.DateTimeFormat;
89
+ month: Intl.DateTimeFormat;
90
+ }
91
+ export type StepState = 'done' | 'active' | 'todo';
92
+ export interface BrandHeaderProps {
93
+ brand: ResolvedBookingBrand;
94
+ }
95
+ export interface ServiceCardProps {
96
+ service: PublicService;
97
+ featured: boolean;
98
+ onSelect: () => void;
99
+ fmt: BookingFormatters;
100
+ brand: ResolvedBookingBrand;
101
+ }
102
+ export interface SidebarExtrasProps {
103
+ professionalName: string;
104
+ brand: ResolvedBookingBrand;
105
+ /** Renders the primary CTA — replacements can call it from their own button. */
106
+ onStartBooking: () => void;
107
+ labels: PublicBookingLabels;
108
+ }
109
+ export interface SuccessPanelProps {
110
+ service: PublicService | null;
111
+ /** ISO datetime of the booked slot. */
112
+ slotStart: string | null;
113
+ professionalName: string;
114
+ labels: PublicBookingLabels;
115
+ fmt: BookingFormatters;
116
+ }
117
+ export interface StepRailItemProps {
118
+ n: number;
119
+ label: string;
120
+ state: StepState;
121
+ children?: ReactNode;
122
+ }
123
+ /**
124
+ * Host-overridable subcomponents. Flow-critical parts (week strip, slot grid,
125
+ * contact forms, payment panel) stay internal — these are the brand-visible
126
+ * seams. Defaults are exported so hosts can wrap instead of rewriting.
127
+ */
128
+ export interface BookingComponents {
129
+ BrandHeader: ComponentType<BrandHeaderProps>;
130
+ ServiceCard: ComponentType<ServiceCardProps>;
131
+ SidebarExtras: ComponentType<SidebarExtrasProps>;
132
+ SuccessPanel: ComponentType<SuccessPanelProps>;
133
+ StepRailItem: ComponentType<StepRailItemProps>;
134
+ }
42
135
  export interface PublicBookingLabels {
43
136
  title: string;
44
137
  serviceStep: string;
@@ -47,5 +140,46 @@ export interface PublicBookingLabels {
47
140
  confirmCta: string;
48
141
  successTitle: string;
49
142
  successBody: string;
143
+ servicesHeading: string;
144
+ aboutHeading: string;
145
+ openingHoursHeading: string;
146
+ closedLabel: string;
147
+ bookNowCta: string;
148
+ timezoneNote: string;
149
+ slotsHeading: string;
150
+ noSlots: string;
151
+ continueCta: string;
152
+ backToServices: string;
153
+ back: string;
154
+ prevWeek: string;
155
+ nextWeek: string;
156
+ contactIntro: string;
157
+ phoneLabel: string;
158
+ phoneHint: string;
159
+ sendCodeCta: string;
160
+ codeTitle: string;
161
+ codeBody: string;
162
+ codeInvalid: string;
163
+ codeConfirmCta: string;
164
+ codeResendHint: string;
165
+ phoneVerified: string;
166
+ changePhone: string;
167
+ nameLabel: string;
168
+ namePlaceholder: string;
169
+ emailLabel: string;
170
+ emailPlaceholder: string;
171
+ emailHint: string;
172
+ notesLabel: string;
173
+ notesPlaceholder: string;
174
+ saveAndContinueCta: string;
175
+ submitError: string;
176
+ noChargeNote: string;
177
+ railTitle: string;
178
+ railWaitingPayment: string;
179
+ railService: string;
180
+ railProfessional: string;
181
+ railDatetime: string;
182
+ railContact: string;
183
+ railPayment: string;
50
184
  }
51
185
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/public/types.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,cAAc;IACd,KAAK,EAAE,MAAM,CAAA;IACb,cAAc;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAA;IACvB,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAA;IACtB,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kFAAkF;IAClF,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,wCAAwC;IACxC,MAAM,CAAC,EAAE,KAAK,CAAA;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,MAAM,EAAE,KAAK,CAAA;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;CACpB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/public/types.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,qBAAqB;IACrB,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,cAAc;IACd,KAAK,EAAE,MAAM,CAAA;IACb,cAAc;IACd,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,aAAa;IAC5B,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAA;IACvB,kDAAkD;IAClD,cAAc,EAAE,MAAM,CAAA;IACtB,mCAAmC;IACnC,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,kFAAkF;IAClF,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,wCAAwC;IACxC,MAAM,CAAC,EAAE,KAAK,CAAA;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,qBAAqB,EAAE,MAAM,CAAA;IAC7B,MAAM,EAAE,KAAK,CAAA;CACd;AAMD,oDAAoD;AACpD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5C,IAAI,EAAE,MAAM,CAAA;CACb;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,2CAA2C;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,mDAAmD;IACnD,WAAW,CAAC,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACnD,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,oEAAoE;IACpE,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAA;CAChC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,WAAW,EAAE,aAAa,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClD,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,UAAU,EAAE,gBAAgB,EAAE,CAAA;CAC/B;AAMD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,IAAI,CAAC,YAAY,CAAA;IACxB,IAAI,EAAE,IAAI,CAAC,cAAc,CAAA;IACzB,OAAO,EAAE,IAAI,CAAC,cAAc,CAAA;IAC5B,MAAM,EAAE,IAAI,CAAC,cAAc,CAAA;IAC3B,OAAO,EAAE,IAAI,CAAC,cAAc,CAAA;IAC5B,SAAS,EAAE,IAAI,CAAC,cAAc,CAAA;IAC9B,KAAK,EAAE,IAAI,CAAC,cAAc,CAAA;CAC3B;AAMD,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAElD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,oBAAoB,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,CAAA;IACtB,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,GAAG,EAAE,iBAAiB,CAAA;IACtB,KAAK,EAAE,oBAAoB,CAAA;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,oBAAoB,CAAA;IAC3B,gFAAgF;IAChF,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,MAAM,EAAE,mBAAmB,CAAA;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,aAAa,GAAG,IAAI,CAAA;IAC7B,uCAAuC;IACvC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,gBAAgB,EAAE,MAAM,CAAA;IACxB,MAAM,EAAE,mBAAmB,CAAA;IAC3B,GAAG,EAAE,iBAAiB,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,SAAS,CAAA;IAChB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAA;IAC5C,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAA;IAC5C,aAAa,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAA;IAChD,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;IAC9C,YAAY,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAA;CAC/C;AAMD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IAEnB,eAAe,EAAE,MAAM,CAAA;IACvB,YAAY,EAAE,MAAM,CAAA;IACpB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;IAElB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAEhB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,eAAe,EAAE,MAAM,CAAA;IACvB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IAEpB,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,MAAM,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fayz-ai/plugin-agenda",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Fayz SDK — plugin-agenda plugin",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -29,29 +29,29 @@
29
29
  "react-dom": "^18.0.0 || ^19.0.0"
30
30
  },
31
31
  "dependencies": {
32
- "lucide-react": "^0.400.0",
33
- "zustand": "^4.5.0",
34
32
  "@fullcalendar/core": "^6.1.20",
35
33
  "@fullcalendar/daygrid": "^6.1.20",
36
- "@fullcalendar/timegrid": "^6.1.20",
37
- "@fullcalendar/react": "^6.1.20",
38
34
  "@fullcalendar/interaction": "^6.1.20",
39
- "@tanstack/react-table": "^8.20.0",
40
- "@fullcalendar/resource-timegrid": "^6.1.0",
41
35
  "@fullcalendar/list": "^6.1.0",
36
+ "@fullcalendar/react": "^6.1.20",
37
+ "@fullcalendar/resource-timegrid": "^6.1.0",
38
+ "@fullcalendar/timegrid": "^6.1.20",
39
+ "@tanstack/react-table": "^8.20.0",
40
+ "lucide-react": "^0.400.0",
41
+ "zustand": "^4.5.0",
42
42
  "@fayz-ai/plugin-financial": "^0.2.0",
43
+ "@fayz-ai/saas": "^0.7.0",
43
44
  "@fayz-ai/ui": "^0.7.0",
44
- "@fayz-ai/core": "^0.7.1",
45
- "@fayz-ai/saas": "^0.7.0"
45
+ "@fayz-ai/core": "^0.7.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/react": "^18.3.0",
49
+ "@types/react-dom": "^18.3.0",
49
50
  "react": "^18.3.0",
51
+ "react-dom": "^18.3.0",
50
52
  "tsup": "^8.2.0",
51
53
  "typescript": "^5.5.0",
52
- "@types/react-dom": "^18.3.0",
53
- "vitest": "^2.1.9",
54
- "react-dom": "^18.3.0"
54
+ "vitest": "^2.1.9"
55
55
  },
56
56
  "license": "MIT",
57
57
  "keywords": [
package/src/index.ts CHANGED
@@ -11,6 +11,7 @@ import { createAgendaStore } from './store'
11
11
  import { agendaRegistries } from './registries'
12
12
  import { AgendaGeneralSettings } from './components/AgendaGeneralSettings'
13
13
  import { agendaLocales } from './locales'
14
+ import { MIGRATION_001_PUBLIC_BOOKING } from './migrations'
14
15
  import { setScheduleBlockConfig, getScheduleBlockConfig, PluginSettingsPanel } from '@fayz-ai/saas'
15
16
 
16
17
  // ---------------------------------------------------------------------------
@@ -179,6 +180,16 @@ export function createAgendaPlugin(options?: AgendaPluginOptions): PluginManifes
179
180
  permission: { feature: 'appointments', action: 'read' as const },
180
181
  },
181
182
  ],
183
+ migrations: [
184
+ {
185
+ id: 'agenda-001-public-booking',
186
+ version: '0.3.0',
187
+ sql: MIGRATION_001_PUBLIC_BOOKING,
188
+ description:
189
+ 'Booking read model (v_bookings) + anon public booking surface (v_public_services, get_available_slots, create_public_booking) + order_items duration/assignee columns',
190
+ },
191
+ ],
192
+
182
193
  locales: agendaLocales,
183
194
  }
184
195
  }
@@ -0,0 +1,278 @@
1
+ -- ============================================================================
2
+ -- plugin-agenda 001: booking read model + public (anon) booking surface
3
+ --
4
+ -- Ships the objects the agenda providers reference but the spine (packages/db
5
+ -- migrations 001–008) does not define:
6
+ -- §1 order_items columns the admin provider writes (duration/assignee)
7
+ -- §2 public.v_bookings — admin read model (authenticated, RLS applies)
8
+ -- §3 public.v_public_services — anon-readable service catalog
9
+ -- §4 public.get_available_slots — anon RPC (mirrors the mock algorithm)
10
+ -- §5 public.create_public_booking — anon RPC (validated, race-safe write)
11
+ --
12
+ -- Anon-access design: public booking pages run with the publishable key and no
13
+ -- session, so the canonical tenant RLS yields nothing. Anon access is granted
14
+ -- ONLY via the column-whitelisted view (§3) and SECURITY DEFINER RPCs (§4, §5)
15
+ -- with tight input validation and hardcoded statuses.
16
+ -- ============================================================================
17
+
18
+ -- §1 — columns the agenda provider writes but 004_archetypes lacks
19
+ ALTER TABLE saas_core.order_items
20
+ ADD COLUMN IF NOT EXISTS duration_minutes integer,
21
+ ADD COLUMN IF NOT EXISTS assignee_id uuid REFERENCES saas_core.persons(id) ON DELETE SET NULL;
22
+
23
+ -- Upsert key for public customers (per tenant, by normalized phone)
24
+ CREATE UNIQUE INDEX IF NOT EXISTS persons_tenant_customer_phone
25
+ ON saas_core.persons(tenant_id, phone)
26
+ WHERE kind = 'customer' AND phone IS NOT NULL;
27
+
28
+ -- Slot-conflict scan
29
+ CREATE INDEX IF NOT EXISTS bookings_assignee_time
30
+ ON saas_core.bookings(assignee_id, starts_at, ends_at)
31
+ WHERE status NOT IN ('cancelled', 'no_show');
32
+
33
+ -- §2 — admin read model (security_invoker → underlying RLS applies; anon sees 0 rows)
34
+ CREATE OR REPLACE VIEW public.v_bookings
35
+ WITH (security_invoker = true) AS
36
+ SELECT
37
+ b.id, b.tenant_id, b.kind, b.starts_at, b.ends_at, b.status, b.notes,
38
+ b.order_id, b.location_id, b.metadata, b.created_at, b.updated_at,
39
+ b.party_id AS client_id,
40
+ COALESCE(pc.name, b.metadata->>'title') AS client_name,
41
+ pc.phone AS client_phone,
42
+ pc.email AS client_email,
43
+ pc.avatar_url AS client_avatar_url,
44
+ b.assignee_id AS professional_id,
45
+ ps.name AS professional_name,
46
+ ps.avatar_url AS professional_avatar_url,
47
+ l.name AS location_name,
48
+ o.total AS order_total,
49
+ o.status AS order_status,
50
+ (EXTRACT(EPOCH FROM (COALESCE(b.ends_at, b.starts_at) - b.starts_at))::int / 60)
51
+ AS total_duration_minutes
52
+ FROM saas_core.bookings b
53
+ LEFT JOIN saas_core.persons pc ON pc.id = b.party_id
54
+ LEFT JOIN saas_core.persons ps ON ps.id = b.assignee_id
55
+ LEFT JOIN saas_core.locations l ON l.id = b.location_id
56
+ LEFT JOIN saas_core.orders o ON o.id = b.order_id;
57
+
58
+ GRANT SELECT ON public.v_bookings TO authenticated, service_role;
59
+ REVOKE SELECT ON public.v_bookings FROM anon;
60
+
61
+ -- §3 — anon-readable public catalog (owner-rights view = deliberate, whitelisted)
62
+ CREATE OR REPLACE VIEW public.v_public_services AS
63
+ SELECT
64
+ s.id, s.tenant_id, s.name, s.description, s.price, s.currency,
65
+ COALESCE(s.duration_minutes, 30) AS duration_minutes,
66
+ s.image_url,
67
+ COALESCE((s.metadata->>'sort_order')::int, 0) AS sort_order
68
+ FROM saas_core.services s
69
+ WHERE s.is_active AND s.status = 'active';
70
+
71
+ GRANT SELECT ON public.v_public_services TO anon, authenticated, service_role;
72
+
73
+ -- §4 — availability RPC (mirrors plugin-agenda/src/data/mock.ts getAvailableSlots)
74
+ CREATE OR REPLACE FUNCTION public.get_available_slots(
75
+ p_tenant_id uuid,
76
+ p_date date,
77
+ p_duration_minutes integer,
78
+ p_assignee_id uuid DEFAULT NULL,
79
+ p_slot_interval integer DEFAULT 30
80
+ ) RETURNS TABLE (slot_start timestamptz, slot_end timestamptz)
81
+ LANGUAGE plpgsql STABLE SECURITY DEFINER
82
+ SET search_path = saas_core, public
83
+ AS $$
84
+ DECLARE
85
+ v_tz text;
86
+ BEGIN
87
+ -- tight validation: this function is anon-callable
88
+ IF p_duration_minutes IS NULL OR p_duration_minutes NOT BETWEEN 5 AND 480 THEN
89
+ RAISE EXCEPTION 'invalid duration';
90
+ END IF;
91
+ IF p_slot_interval IS NULL OR p_slot_interval NOT BETWEEN 5 AND 240 THEN
92
+ RAISE EXCEPTION 'invalid interval';
93
+ END IF;
94
+ IF p_date IS NULL OR p_date < current_date OR p_date > current_date + 90 THEN
95
+ RETURN;
96
+ END IF;
97
+
98
+ SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz
99
+ FROM tenants t WHERE t.id = p_tenant_id;
100
+ IF v_tz IS NULL THEN RETURN; END IF; -- unknown tenant → empty, not an error
101
+
102
+ RETURN QUERY
103
+ WITH day_schedules AS (
104
+ SELECT sc.assignee_id, sc.starts_at AS win_start, sc.ends_at AS win_end
105
+ FROM schedules sc
106
+ WHERE sc.tenant_id = p_tenant_id
107
+ AND sc.kind = 'working_hours'
108
+ AND sc.is_active
109
+ AND (p_assignee_id IS NULL OR sc.assignee_id = p_assignee_id)
110
+ AND (
111
+ sc.specific_date = p_date
112
+ OR (sc.specific_date IS NULL AND sc.day_of_week = EXTRACT(dow FROM p_date)::smallint)
113
+ )
114
+ ),
115
+ candidate AS (
116
+ SELECT
117
+ ds.assignee_id,
118
+ gs AS s_start,
119
+ gs + make_interval(mins => p_duration_minutes) AS s_end
120
+ FROM day_schedules ds
121
+ CROSS JOIN LATERAL generate_series(
122
+ (p_date + ds.win_start) AT TIME ZONE v_tz,
123
+ ((p_date + ds.win_end) AT TIME ZONE v_tz) - make_interval(mins => p_duration_minutes),
124
+ make_interval(mins => p_slot_interval)
125
+ ) AS gs
126
+ )
127
+ SELECT DISTINCT c.s_start, c.s_end
128
+ FROM candidate c
129
+ WHERE NOT EXISTS (
130
+ SELECT 1 FROM bookings b
131
+ WHERE b.tenant_id = p_tenant_id
132
+ AND b.assignee_id = c.assignee_id
133
+ AND b.status NOT IN ('cancelled', 'no_show')
134
+ AND b.starts_at < c.s_end
135
+ AND COALESCE(b.ends_at, b.starts_at) > c.s_start
136
+ )
137
+ ORDER BY 1;
138
+ END;
139
+ $$;
140
+
141
+ REVOKE ALL ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer) FROM public;
142
+ GRANT EXECUTE ON FUNCTION public.get_available_slots(uuid, date, integer, uuid, integer)
143
+ TO anon, authenticated, service_role;
144
+
145
+ -- §5 — public booking write RPC (validated, race-safe, hardcoded statuses)
146
+ CREATE OR REPLACE FUNCTION public.create_public_booking(
147
+ p_tenant_id uuid,
148
+ p_service_id uuid,
149
+ p_starts_at timestamptz,
150
+ p_name text,
151
+ p_phone text,
152
+ p_email text DEFAULT NULL,
153
+ p_notes text DEFAULT NULL
154
+ ) RETURNS TABLE (booking_id uuid, starts_at timestamptz, ends_at timestamptz)
155
+ LANGUAGE plpgsql SECURITY DEFINER
156
+ SET search_path = saas_core, public
157
+ AS $$
158
+ DECLARE
159
+ v_service record;
160
+ v_person_id uuid;
161
+ v_assignee uuid;
162
+ v_ends timestamptz;
163
+ v_order uuid;
164
+ v_booking uuid;
165
+ v_phone text;
166
+ v_tz text;
167
+ v_notes text;
168
+ BEGIN
169
+ -- validation: anon-callable, everything tight
170
+ IF p_name IS NULL OR length(trim(p_name)) NOT BETWEEN 2 AND 120 THEN
171
+ RAISE EXCEPTION 'invalid name';
172
+ END IF;
173
+ v_phone := regexp_replace(COALESCE(p_phone, ''), '\D', '', 'g');
174
+ IF length(v_phone) NOT BETWEEN 8 AND 15 THEN
175
+ RAISE EXCEPTION 'invalid phone';
176
+ END IF;
177
+ IF p_email IS NOT NULL AND (length(p_email) > 254 OR p_email !~ '^[^@\s]+@[^@\s]+\.[^@\s]+$') THEN
178
+ RAISE EXCEPTION 'invalid email';
179
+ END IF;
180
+ v_notes := left(p_notes, 500);
181
+ IF p_starts_at IS NULL OR p_starts_at <= now() OR p_starts_at > now() + interval '90 days' THEN
182
+ RAISE EXCEPTION 'invalid start';
183
+ END IF;
184
+
185
+ SELECT s.id, s.name, s.price, COALESCE(s.duration_minutes, 30) AS duration_minutes
186
+ INTO v_service
187
+ FROM services s
188
+ WHERE s.id = p_service_id AND s.tenant_id = p_tenant_id
189
+ AND s.is_active AND s.status = 'active';
190
+ IF NOT FOUND THEN RAISE EXCEPTION 'unknown service'; END IF;
191
+
192
+ v_ends := p_starts_at + make_interval(mins => v_service.duration_minutes);
193
+ SELECT COALESCE(t.settings->>'timezone', 'America/Sao_Paulo') INTO v_tz
194
+ FROM tenants t WHERE t.id = p_tenant_id;
195
+ IF v_tz IS NULL THEN RAISE EXCEPTION 'unknown tenant'; END IF;
196
+
197
+ -- pick the assignee whose working hours cover the slot
198
+ SELECT sc.assignee_id INTO v_assignee
199
+ FROM schedules sc
200
+ WHERE sc.tenant_id = p_tenant_id
201
+ AND sc.kind = 'working_hours'
202
+ AND sc.is_active
203
+ AND (
204
+ sc.specific_date = (p_starts_at AT TIME ZONE v_tz)::date
205
+ OR (sc.specific_date IS NULL
206
+ AND sc.day_of_week = EXTRACT(dow FROM p_starts_at AT TIME ZONE v_tz)::smallint)
207
+ )
208
+ AND (p_starts_at AT TIME ZONE v_tz)::time >= sc.starts_at
209
+ AND (v_ends AT TIME ZONE v_tz)::time <= sc.ends_at
210
+ ORDER BY sc.assignee_id
211
+ LIMIT 1;
212
+ IF v_assignee IS NULL THEN RAISE EXCEPTION 'slot outside working hours'; END IF;
213
+
214
+ -- serialize per (tenant, assignee) to kill the double-book race
215
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_tenant_id::text || v_assignee::text, 0));
216
+
217
+ IF EXISTS (
218
+ SELECT 1 FROM bookings b
219
+ WHERE b.tenant_id = p_tenant_id
220
+ AND b.assignee_id = v_assignee
221
+ AND b.status NOT IN ('cancelled', 'no_show')
222
+ AND b.starts_at < v_ends
223
+ AND COALESCE(b.ends_at, b.starts_at) > p_starts_at
224
+ ) THEN
225
+ RAISE EXCEPTION 'slot no longer available';
226
+ END IF;
227
+
228
+ -- anti-spam: cap open future public bookings per phone per tenant
229
+ IF (
230
+ SELECT count(*) FROM bookings b
231
+ JOIN persons p ON p.id = b.party_id
232
+ WHERE b.tenant_id = p_tenant_id
233
+ AND p.phone = v_phone
234
+ AND b.starts_at > now()
235
+ AND b.status = 'scheduled'
236
+ ) >= 3 THEN
237
+ RAISE EXCEPTION 'too many open bookings for this phone';
238
+ END IF;
239
+
240
+ -- upsert the customer by (tenant, phone) — matches persons_tenant_customer_phone
241
+ INSERT INTO persons (tenant_id, kind, name, phone, email, metadata)
242
+ VALUES (p_tenant_id, 'customer', trim(p_name), v_phone, p_email,
243
+ jsonb_build_object('source', 'public_booking'))
244
+ ON CONFLICT (tenant_id, phone) WHERE kind = 'customer' AND phone IS NOT NULL
245
+ DO UPDATE SET
246
+ name = EXCLUDED.name,
247
+ email = COALESCE(EXCLUDED.email, persons.email),
248
+ updated_at = now()
249
+ RETURNING id INTO v_person_id;
250
+
251
+ -- order + booking + order_item (statuses hardcoded — caller cannot set)
252
+ INSERT INTO orders (tenant_id, kind, status, party_id, assignee_id, subtotal, total, notes, metadata)
253
+ VALUES (p_tenant_id, 'appointment', 'scheduled', v_person_id, v_assignee,
254
+ v_service.price, v_service.price, v_notes,
255
+ jsonb_build_object('source', 'public_booking',
256
+ 'serviceNames', v_service.name,
257
+ 'contactName', trim(p_name)))
258
+ RETURNING id INTO v_order;
259
+
260
+ INSERT INTO bookings (tenant_id, kind, party_id, assignee_id, order_id,
261
+ starts_at, ends_at, status, notes, metadata)
262
+ VALUES (p_tenant_id, 'appointment', v_person_id, v_assignee, v_order,
263
+ p_starts_at, v_ends, 'scheduled', v_notes,
264
+ jsonb_build_object('source', 'public_booking', 'serviceNames', v_service.name))
265
+ RETURNING id INTO v_booking;
266
+
267
+ INSERT INTO order_items (order_id, service_id, name, quantity, unit_price, total,
268
+ sort_order, duration_minutes, assignee_id)
269
+ VALUES (v_order, v_service.id, v_service.name, 1, v_service.price, v_service.price,
270
+ 0, v_service.duration_minutes, v_assignee);
271
+
272
+ RETURN QUERY SELECT v_booking, p_starts_at, v_ends;
273
+ END;
274
+ $$;
275
+
276
+ REVOKE ALL ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text) FROM public;
277
+ GRANT EXECUTE ON FUNCTION public.create_public_booking(uuid, uuid, timestamptz, text, text, text, text)
278
+ TO anon, authenticated, service_role;