From 49c49f409842c7cb5077de030c8bfa14a65dafac Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sun, 12 Jan 2025 12:52:47 +0100 Subject: [PATCH] feat: add client parser to get response from LLM --- internal/driver/parser/client.go | 91 +++++++++++++ internal/service/booking/service.go | 10 +- internal/service/booking/service_test.go | 4 +- internal/service/booking/sync.go | 155 +---------------------- internal/service/booking/sync_test.go | 6 +- main.go | 8 +- 6 files changed, 114 insertions(+), 160 deletions(-) create mode 100644 internal/driver/parser/client.go diff --git a/internal/driver/parser/client.go b/internal/driver/parser/client.go new file mode 100644 index 0000000..073c632 --- /dev/null +++ b/internal/driver/parser/client.go @@ -0,0 +1,91 @@ +package parser + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "time" + + "github.com/rjNemo/rentease/internal/service/booking" +) + +type BookingAgentParser struct { + baseUrl string +} + +func NewBookingAgentParser(baseUrl string) *BookingAgentParser { + return &BookingAgentParser{ + baseUrl: baseUrl, + } +} + +func (p *BookingAgentParser) Parse(rawContent string) (*booking.Booking, error) { + log.Println("sending request to booking agent parser") + + resp, err := http.Post(fmt.Sprintf("%s/sync?input=%s", p.baseUrl, url.QueryEscape(rawContent)), "application/json", nil) + if err != nil { + return nil, fmt.Errorf("error sending request to booking agent parser: %w", err) + } + defer resp.Body.Close() + + type ResponseData struct { + ArrivalDate string `json:"arrival_date"` + BookingFees json.Number `json:"booking_fees"` + BookingID string `json:"booking_id"` + DepartureDate string `json:"departure_date"` + StayLength int `json:"stay_length"` + GuestEmail string `json:"guest_email"` + GuestName string `json:"guest_name"` + GuestNumber int `json:"guest_number"` + GuestPhone string `json:"guest_phone"` + RoomBooked string `json:"room_booked"` + StandardRate json.Number `json:"standard_rate"` + SpecialRequests string `json:"special_requests"` + } + + type Response struct { + Data ResponseData `json:"data"` + } + + var r Response + err = json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + return nil, fmt.Errorf("error decoding response from booking agent parser: %w", err) + } + + var b booking.Booking + b.From, err = time.Parse("2006-01-02", r.Data.ArrivalDate) + if err != nil { + return nil, fmt.Errorf("error parsing arrival_date: %w", err) + } + b.To, err = time.Parse("2006-01-02", r.Data.DepartureDate) + if err != nil { + return nil, fmt.Errorf("error parsing departure_date: %w", err) + } + b.Name = r.Data.GuestName + b.PhoneNumber = r.Data.GuestPhone + b.Email = r.Data.GuestEmail + b.CustomerNumber = r.Data.GuestNumber + bookingFees, err := r.Data.BookingFees.Float64() + if err != nil { + return nil, fmt.Errorf("error parsing booking fees: %w", err) + } + b.Platform = "Booking" + b.PlatformFees = bookingFees + b.ExternalId = &r.Data.BookingID + + price, err := r.Data.StandardRate.Float64() + if err != nil { + return nil, fmt.Errorf("error parsing standard rate: %w", err) + } + b.Items = append(b.Items, booking.Item{ + Item: r.Data.RoomBooked, + Quantity: r.Data.StayLength, + Price: price, + PaymentMethod: "Card", + }) + + return &b, nil +} diff --git a/internal/service/booking/service.go b/internal/service/booking/service.go index 25d42ac..de41534 100644 --- a/internal/service/booking/service.go +++ b/internal/service/booking/service.go @@ -18,7 +18,7 @@ type Store interface { Create(b *Booking) error Update(b *Booking) error Cancel(id int) error - + // Item methods CreateItem(i *Item) error PayItem(id int) (*Item, error) @@ -26,15 +26,21 @@ type Store interface { UpdateItem(id int, item string, paymentMethod string, paymentStatus string, qty int, price float64) (*Item, error) } +type parserClient interface { + Parse(rawContent string) (*Booking, error) +} + type Service struct { store Store + parser parserClient calendar calendar.Client pdf pdf.Client } -func NewService(store Store, calendar calendar.Client, pdf pdf.Client) (*Service, error) { +func NewService(store Store, parser parserClient, calendar calendar.Client, pdf pdf.Client) (*Service, error) { return &Service{ store: store, + parser: parser, calendar: calendar, pdf: pdf, }, nil diff --git a/internal/service/booking/service_test.go b/internal/service/booking/service_test.go index 8a077c9..aa7a63b 100644 --- a/internal/service/booking/service_test.go +++ b/internal/service/booking/service_test.go @@ -52,7 +52,7 @@ func TestService_All(t *testing.T) { mockStore.On("All").Return(tt.mockData) // Create service with mocks - service, err := NewService(mockStore, mockCalendar, mockPDF) + service, err := NewService(mockStore, nil, mockCalendar, mockPDF) assert.NoError(t, err) // Call the method @@ -134,7 +134,7 @@ func TestService_Search(t *testing.T) { mockStore.On("Search", tt.searchQuery).Return(tt.expectedLines) // Create service with mocks - service, err := NewService(mockStore, mockCalendar, mockPDF) + service, err := NewService(mockStore, nil, mockCalendar, mockPDF) assert.NoError(t, err) // Call the method diff --git a/internal/service/booking/sync.go b/internal/service/booking/sync.go index f3c79c9..8d0b96c 100644 --- a/internal/service/booking/sync.go +++ b/internal/service/booking/sync.go @@ -1,170 +1,21 @@ package booking import ( - "fmt" - "log" - "regexp" - "strconv" - "strings" - "time" - "github.com/rjNemo/rentease/internal/config" ) func (bs Service) ParseFromApi(rawContent string) (*Booking, error) { - b, err := ParseBooking(rawContent) - log.Printf("parsed booking: %+v", b) + b, err := bs.parser.Parse(rawContent) if err != nil { return nil, err } - b = bs.Create(b.From, b.To, b.Name, b.PhoneNumber, b.Email, b.Platform, b.CustomerNumber, b.PlatformFees, b.ExternalId) itm := b.Items[0] + b = bs.Create(b.From, b.To, b.Name, b.PhoneNumber, b.Email, b.Platform, b.CustomerNumber, b.PlatformFees, b.ExternalId) + if item, ok := config.NewHost().Items[itm.Item]; ok { bs.CreateItem(b.Id, item, itm.Quantity, itm.Price, itm.PaymentMethod, b.CustomerNumber, b.Platform) } return b, nil } - -func ParseBooking(content string) (*Booking, error) { - content = strings.ReplaceAll(strings.TrimSpace(content), "\u00a0", " ") - - arrivalDate, err := extractDate(`Date d'arrivée `, content) - if err != nil { - return nil, err - } - - departureDate, err := extractDate(`Date de départ `, content) - if err != nil { - return nil, err - } - - stayLength, err := extractInt(`Durée de séjour : (\d+) nuits`, content) - if err != nil { - return nil, err - } - - customerName, err := extractString(`Nom du client : \n\s+([A-Za-z\s]+)`, content) - if err != nil { - return nil, err - } - - customerName = strings.SplitN(customerName, "\n", 2)[0] - customerEmail, err := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content) - if err != nil { - return nil, err - } - - customerNumber, err := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content) - if err != nil { - return nil, err - } - - commissionAmount, err := extractFloat(`Commission : € (\d+,\d+)`, content) - if err != nil { - return nil, err - } - - itemName, err := extractString(`Maison . Chambre(s?) \((T2|T3)`, content) - if err != nil { - return nil, err - } - - externalId, err := extractString(`Numéro de réservation : \n\s+(\d+)`, content) - if err != nil { - return nil, err - } - - standardRate, err := extractFloat(`Standard Rate\n\s+€ (\d+)`, content) - if err != nil { - return nil, err - } - - return &Booking{ - From: *formatDate(arrivalDate), - To: *formatDate(departureDate), - Name: customerName, - Email: customerEmail, - Platform: "Booking", - CustomerNumber: customerNumber, - PlatformFees: commissionAmount, - ExternalId: &externalId, - Items: []Item{ - { - Item: itemName, - Quantity: stayLength, - Price: standardRate, - PaymentMethod: "Card", - }, - }, - }, nil -} - -func extractDate(pattern, content string) (string, error) { - re := regexp.MustCompile(pattern + `(lun|mar|mer|jeu|ven|sam|dim)\. \d{1,2} (janv|févr|mars|avr|mai|juin|juil|août|sept|oct|nov|déc)\.? \d{4}`) - dateMatch := re.FindString(content) - - if dateMatch == "" { - return "", fmt.Errorf("date not found") - } - - // Regular expression to remove the prefix - rePrefix := regexp.MustCompile(pattern + `\w+\.\s*`) - dateString := rePrefix.ReplaceAllString(dateMatch, "") - return dateString, nil -} - -func extractInt(pattern, content string) (int, error) { - re := regexp.MustCompile(pattern) - match := re.FindStringSubmatch(content) - if len(match) > 1 { - val, err := strconv.Atoi(match[1]) - if err != nil { - return 0, err - } - return val, nil - } - return 0.0, fmt.Errorf("no match for %s", pattern) -} - -func extractFloat(pattern, content string) (float64, error) { - re := regexp.MustCompile(pattern) - match := re.FindStringSubmatch(content) - if len(match) > 1 { - val, err := strconv.ParseFloat(strings.ReplaceAll(match[1], ",", "."), 64) - if err != nil { - return 0, err - } - return val, nil - } - return 0.0, fmt.Errorf("no match for %s", pattern) -} - -func extractString(pattern, content string) (string, error) { - re := regexp.MustCompile(pattern) - match := re.FindStringSubmatch(content) - if len(match) > 1 { - return strings.TrimSpace(match[1]), nil - } else if len(match) > 0 { - return strings.TrimSpace(match[0]), nil - } - return "", fmt.Errorf("pattern %s not found", pattern) -} - -func formatDate(date string) *time.Time { - months := map[string]string{ - "janv.": "01", "févr.": "02", "mar.": "03", "avr.": "04", - "mai": "05", "juin": "06", "juil.": "07", "août": "08", - "sep.": "09", "oct.": "10", "nov.": "11", "déc.": "12", - } - parts := strings.Split(date, " ") - dateString := fmt.Sprintf("%s-%02s-%02s", parts[2], months[parts[1]], parts[0]) - - t, err := time.Parse(time.DateOnly, dateString) - if err != nil { - log.Println(err) - return nil - } - return &t -} diff --git a/internal/service/booking/sync_test.go b/internal/service/booking/sync_test.go index c28beb9..8d549a5 100644 --- a/internal/service/booking/sync_test.go +++ b/internal/service/booking/sync_test.go @@ -9,7 +9,7 @@ import ( "github.com/rjNemo/rentease/internal/service/booking" ) -const content = " Date d'arrivée jeu. 3 avr. 2025 Date de départ dim. 6 avr. 2025 Durée de séjour : 3 nuits Nombre de personnes : \n 2\n Nombre d'hébergements \n 1\n Montant total € 186 Nom du client : \n Olga Korovina\n \n ru\n \n okorov.905387@guest.booking.com\n Contactez vos clients ! Indiquez-leur l'heure à laquelle vous souhaitez les accueillir ou l'endroit où ils récupéreront leurs clés. Un simple appel suffit : Afficher le numéro de téléphone Vous pouvez également leur envoyer un e-mail ou un message. Langue préférée \n russe\n Canal : Booking.com Code IATA/TIDS : \n PC029090\n Numéro de réservation : \n 4453602306\n Montant soumis à commission : € 177 Reçu jeu. 2 janv. 2025 Commission : € 31,86 Bloc-notes (usage interne) Ajoutez une note ici \n\n Maison 1 Chambre (T2 - VillaFleurie au bourg du Gosier)\n € 186 jeu. 3 avr. 2025 dim. 6 avr. 2025 Nom du client \n Olga Korovina\n Occupation maximum 2 adultes, 2 enfants (3 personnes max.) Photo de l'hébergement Date Tarif Tarif par nuit \n 03 - 04 avril\n \n Standard Rate\n € 59\n 04 - 05 avril\n \n Standard Rate\n € 59\n 05 - 06 avril\n \n Standard Rate\n € 59Sous-total € 177\n Taxe de séjour\n € 1.50 par personne et par nuit € 9Tarif total de l'hébergement € 186 Le tarif comprend 8.9 % de TVA Conversation avec le client \n Aucun message\n \n Les conversations avec vos clients apparaîtront ici.\n Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions " +const content = " Date d'arrivée jeu. 3 avr. 2025 Date de départ dim. 6 avr. 2025 Durée de séjour : 3 nuits Nombre de personnes : \n 2\n Nombre d'hébergements \n 1\n Montant total € 186 Nom du client : \n Olga Korovina\n \n ru\n \n okorov.905387@guest.booking.com\n Contactez vos clients ! Indiquez-leur l'heure à laquelle vous souhaitez les accueillir ou l'endroit où ils récupéreront leurs clés. Un simple appel suffit : Afficher le numéro de téléphone Vous pouvez également leur envoyer un e-mail ou un message. Langue préférée \n russe\n Canal : Booking.com Code IATA/TIDS : \n PC029090\n Numéro de réservation : \n 4453602306\n Montant soumis à commission : € 177 Reçu jeu. 2 janv. 2025 Commission : € 31,86 Bloc-notes (usage interne) Ajoutez une note ici \n\n Maison 1 Chambre T2 - VillaFleurie au bourg du Gosier)\n € 186 jeu. 3 avr. 2025 dim. 6 avr. 2025 Nom du client \n Olga Korovina\n Occupation maximum 2 adultes, 2 enfants (3 personnes max.) Photo de l'hébergement Date Tarif Tarif par nuit \n 03 - 04 avril\n \n Standard Rate\n € 59\n 04 - 05 avril\n \n Standard Rate\n € 59\n 05 - 06 avril\n \n Standard Rate\n € 59Sous-total € 177\n Taxe de séjour\n € 1.50 par personne et par nuit € 9Tarif total de l'hébergement € 186 Le tarif comprend 8.9 % de TVA Conversation avec le client \n Aucun message\n \n Les conversations avec vos clients apparaîtront ici.\n Booking.com reçoit tous les messages écrits ici et les traite selon sa Charte de confidentialité et informations sur les cookies Conditions " func TestParseFromApi(t *testing.T) { externalId := "4453602306" @@ -47,7 +47,9 @@ func TestParseFromApi(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() - actual, err := booking.ParseBooking(tt.rawContent) + bs, _ := booking.NewService(&booking.MockStore{}, nil, nil, nil) + + actual, err := bs.ParseFromApi(tt.rawContent) if err != nil { t.Errorf("unexpected error: %v", err) } diff --git a/main.go b/main.go index 17c9e18..3a0b655 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/rjNemo/rentease/internal/config" "github.com/rjNemo/rentease/internal/driver/calendar" "github.com/rjNemo/rentease/internal/driver/database" + "github.com/rjNemo/rentease/internal/driver/parser" "github.com/rjNemo/rentease/internal/driver/pdf" bookingRepo "github.com/rjNemo/rentease/internal/repository/booking" "github.com/rjNemo/rentease/internal/server" @@ -56,6 +57,8 @@ func run(c context.Context, getEnv func(string) string) error { return fmt.Errorf("error migrating the database %w", err) } + bookingStore := bookingRepo.NewPgStore(db) + gc, err := calendar.NewGoogleClient(ctx, getEnv("CALENDAR_CREDENTIALS")) if err != nil { return fmt.Errorf("error building calendar client %w", err) @@ -72,8 +75,9 @@ func run(c context.Context, getEnv func(string) string) error { return fmt.Errorf("error starting pdf client %w", err) } - bookingStore := bookingRepo.NewPgStore(db) - bookingService, err := booking.NewService(bookingStore, gc, pc) + parsingClient := parser.NewBookingAgentParser(getEnv("PARSER_BASE_URL")) + + bookingService, err := booking.NewService(bookingStore, parsingClient, gc, pc) if err != nil { return fmt.Errorf("error creating booking service: %w", err) }