mirror of
https://github.com/rjNemo/rentease.git
synced 2026-06-09 20:26:51 +00:00
feat: add client parser to get response from LLM
This commit is contained in:
parent
b4ffff3c67
commit
49c49f4098
6 changed files with 114 additions and 160 deletions
91
internal/driver/parser/client.go
Normal file
91
internal/driver/parser/client.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
8
main.go
8
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue