33import { revalidatePath } from "next/cache" ;
44import { getSupabaseAdmin } from "@/lib/supabase/admin" ;
55import { parseSupabaseError } from "@/lib/error-handler" ;
6- import { Appointment , SaleStatus } from "@/types" ;
6+ import { Appointment , SaleStatus , AppointmentStatus } from "@/types" ;
77import { supabaseAppointmentToAppointment } from "@/lib/utils/mapping" ;
8- import { createSaleAction } from "./finance" ;
98
109/**
11- * Creates a new appointment.
10+ * Creates a new appointment atomically with its sale using an RPC.
11+ * Resolves Rule 1 (Atomicity) and Rule 2 (Commission Tracking).
1212 */
1313export async function createAppointmentAction (
1414 appointment : Omit < Appointment , "id" | "created_at" > ,
1515) {
1616 try {
1717 const supabase = getSupabaseAdmin ( ) ;
1818
19- // 1. Create the appointment
20- const payload = {
21- client_id : parseInt ( appointment . clientId ) ,
22- professional_id : appointment . professionalId ,
23- start_time : appointment . startTime ,
24- end_time : appointment . endTime ,
25- status : appointment . status ,
26- notes : appointment . notes || null ,
19+ const rpcPayload = {
20+ p_client_id : parseInt ( appointment . clientId ) ,
21+ p_professional_id : appointment . professionalId ,
22+ p_start_time : appointment . startTime ,
23+ p_end_time : appointment . endTime ,
24+ p_notes : appointment . notes || null ,
25+ p_service_variants :
26+ appointment . serviceVariants ?. map ( ( sv ) => ( {
27+ service_variant_id : parseInt ( sv . serviceVariantId ) ,
28+ quantity : sv . quantity ,
29+ } ) ) || [ ] ,
2730 } ;
2831
29- const { data : appointmentData , error : appointmentError } = await supabase
30- . from ( "appointments" )
31- . insert ( [ payload ] )
32- . select ( `*, clients(full_name)` )
33- . single ( ) ;
32+ // Rule 1: Use atomic RPC to prevent "ghost appointments"
33+ const { data , error } = await supabase . rpc (
34+ "create_appointment_with_sale" ,
35+ rpcPayload ,
36+ ) ;
3437
35- if ( appointmentError ) {
38+ if ( error ) {
3639 return {
3740 success : false ,
38- error : parseSupabaseError ( appointmentError ) . description ,
41+ error : parseSupabaseError ( error ) . description ,
3942 } ;
4043 }
4144
42- const appointmentId = appointmentData . id ;
43-
44- // 2. Create appointment services and calculate total price
45- let totalAmount = 0 ;
46- const items : Array < {
47- serviceVariantId : string ;
48- quantity : number ;
49- unitPrice : number ;
50- subtotal : number ;
51- professionalId : string ;
52- } > = [ ] ;
53-
54- if ( appointment . serviceVariants && appointment . serviceVariants . length > 0 ) {
55- for ( const sv of appointment . serviceVariants ) {
56- // Fetch price for each variant
57- const { data : variantData } = await supabase
58- . from ( "service_variants" )
59- . select ( "price" )
60- . eq ( "id" , parseInt ( sv . serviceVariantId ) )
61- . single ( ) ;
62-
63- const unitPrice = variantData ?. price || 0 ;
64- const subtotal = unitPrice * sv . quantity ;
65- totalAmount += subtotal ;
66-
67- items . push ( {
68- serviceVariantId : sv . serviceVariantId ,
69- quantity : sv . quantity ,
70- unitPrice : unitPrice ,
71- subtotal : subtotal ,
72- professionalId : appointment . professionalId ,
73- } ) ;
74-
75- // Insert into appointment_services
76- await supabase . from ( "appointment_services" ) . insert ( [
77- {
78- appointment_id : appointmentId ,
79- service_variant_id : parseInt ( sv . serviceVariantId ) ,
80- quantity : sv . quantity ,
81- } ,
82- ] ) ;
83- }
84-
85- // 3. Create a pending sale automatically
86- await createSaleAction ( {
87- clientId : appointment . clientId ,
88- appointmentId : String ( appointmentId ) ,
89- items : items ,
90- totalAmount : totalAmount ,
91- status : SaleStatus . PENDING ,
92- notes : `Gerada automaticamente do agendamento #${ appointmentId } ` ,
93- } ) ;
94- }
95-
9645 revalidatePath ( "/agenda" ) ;
9746 revalidatePath ( "/financeiro" ) ;
9847
9948 return {
10049 success : true ,
101- data : supabaseAppointmentToAppointment ( appointmentData ) ,
50+ data : supabaseAppointmentToAppointment ( data ) ,
10251 } ;
10352 } catch ( error : unknown ) {
10453 console . error ( "Error in createAppointmentAction:" , error ) ;
@@ -107,7 +56,8 @@ export async function createAppointmentAction(
10756}
10857
10958/**
110- * Updates an existing appointment.
59+ * Updates an existing appointment and syncs related sale status.
60+ * Resolves Rule 3 (End-of-day reconciliation).
11161 */
11262export async function updateAppointmentAction (
11363 id : string ,
@@ -146,7 +96,19 @@ export async function updateAppointmentAction(
14696 return { success : false , error : parseSupabaseError ( error ) . description } ;
14797 }
14898
99+ // Rule 3 Sync: If appointment is cancelled, cancel the associated sale
100+ if ( appointment . status === AppointmentStatus . CANCELLED ) {
101+ await supabase
102+ . from ( "sales" )
103+ . update ( {
104+ status : SaleStatus . CANCELLED ,
105+ updated_at : new Date ( ) . toISOString ( ) ,
106+ } )
107+ . eq ( "appointment_id" , parseInt ( id ) ) ;
108+ }
109+
149110 revalidatePath ( "/agenda" ) ;
111+ revalidatePath ( "/financeiro" ) ;
150112 return { success : true , data : supabaseAppointmentToAppointment ( data ) } ;
151113 } catch ( error : unknown ) {
152114 console . error ( "Error in updateAppointmentAction:" , error ) ;
0 commit comments