From a2ce00329978b0890a5773743cd1d3230b80ea7d Mon Sep 17 00:00:00 2001 From: Ruidy Date: Sun, 5 Jan 2025 16:04:44 +0100 Subject: [PATCH] parsing (#28) * test: add test for booking parsing * improve error handling * fix: booking parsing fix: guest number fix: item name fix: parsing * refactor tests * test: more --- go.mod | 6 +- go.sum | 7 ++ internal/service/booking/service_test.go | 40 +++---- internal/service/booking/sync.go | 133 +++++++++++++++++------ internal/service/booking/sync_test.go | 57 ++++++++++ 5 files changed, 188 insertions(+), 55 deletions(-) create mode 100644 internal/service/booking/sync_test.go diff --git a/go.mod b/go.mod index 537d0ed..6528ddf 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/labstack/gommon v0.4.2 github.com/rjNemo/underscore v0.7.0 github.com/stretchr/testify v1.10.0 - golang.org/x/oauth2 v0.24.0 + golang.org/x/oauth2 v0.25.0 google.golang.org/api v0.214.0 gorm.io/driver/postgres v1.5.11 gorm.io/gorm v1.25.12 @@ -58,9 +58,9 @@ require ( golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.8.0 // indirect + golang.org/x/time v0.9.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect google.golang.org/grpc v1.69.2 // indirect google.golang.org/protobuf v1.36.1 // indirect diff --git a/go.sum b/go.sum index f0ea9bb..3cca4f3 100644 --- a/go.sum +++ b/go.sum @@ -142,6 +142,7 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= @@ -194,6 +195,8 @@ golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -207,12 +210,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/internal/service/booking/service_test.go b/internal/service/booking/service_test.go index dbd2636..8a077c9 100644 --- a/internal/service/booking/service_test.go +++ b/internal/service/booking/service_test.go @@ -22,20 +22,20 @@ func TestService_All(t *testing.T) { { Id: 1, CustomerName: "John Doe", - From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - To: time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC), - Platform: "Airbnb", - Total: 500.0, - Canceled: false, + From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC), + Platform: "Airbnb", + Total: 500.0, + Canceled: false, }, { Id: 2, CustomerName: "Jane Smith", - From: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), - To: time.Date(2024, 2, 3, 0, 0, 0, 0, time.UTC), - Platform: "Booking.com", - Total: 300.0, - Canceled: true, + From: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 2, 3, 0, 0, 0, 0, time.UTC), + Platform: "Booking.com", + Total: 300.0, + Canceled: true, }, }, }, @@ -70,20 +70,20 @@ func TestService_Search(t *testing.T) { { Id: 1, CustomerName: "John Doe", - From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), - To: time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC), - Platform: "Airbnb", - Total: 500.0, - Canceled: false, + From: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC), + Platform: "Airbnb", + Total: 500.0, + Canceled: false, }, { Id: 2, CustomerName: "Jane Smith", - From: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), - To: time.Date(2024, 2, 3, 0, 0, 0, 0, time.UTC), - Platform: "Booking.com", - Total: 300.0, - Canceled: true, + From: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2024, 2, 3, 0, 0, 0, 0, time.UTC), + Platform: "Booking.com", + Total: 300.0, + Canceled: true, }, } diff --git a/internal/service/booking/sync.go b/internal/service/booking/sync.go index 8cf7858..b0d69fc 100644 --- a/internal/service/booking/sync.go +++ b/internal/service/booking/sync.go @@ -12,79 +12,148 @@ import ( ) func (bs Service) ParseFromApi(rawContent string) (*Booking, error) { - content := strings.ReplaceAll(strings.TrimSpace(rawContent), "\u00a0", " ") + b, err := ParseBooking(rawContent) + if err != nil { + return nil, err + } - arrivalDate := extractDate(`Date d'arrivée `, content) - departureDate := extractDate(`Date de départ `, content) - stayLength := extractInt(`Durée de séjour : (\d+) nuits`, content) - customerName := extractString(`Nom du client : \n\s+([\w\s]+)`, content) - customerName = strings.SplitN(customerName, "\n", 2)[0] - customerEmail := extractString(`[\w\.\-]+@[\w\.\-]+\.\w+`, content) - customerNumber := extractInt(`Nombre de personnes : \s*\n\s*(\d+)`, content) - commissionAmount := extractFloat(`Commission : € (\d+,\d+)`, content) - itemName := extractString(`Maison . Chambre. \((T2|T3) -`, content) - externalId := extractString(`Numéro de réservation : \n\s+(\d+)`, content) - standardRate := extractFloat(`Standard Rate\n\s+€ (\d+)`, content) - - b := bs.Create(*formatDate(arrivalDate), *formatDate(departureDate), customerName, "", customerEmail, "Booking", customerNumber, commissionAmount, &externalId) - if item, ok := config.NewHost().Items[itemName]; ok { - bs.CreateItem(b.Id, item, stayLength, standardRate, "Card", customerNumber, b.Platform) + 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] + 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 extractDate(pattern, content string) string { +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.\((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 == "" { - log.Println("date not found") - return "" + return "", fmt.Errorf("date not found") } // Regular expression to remove the prefix rePrefix := regexp.MustCompile(pattern + `\w+\.\s*`) dateString := rePrefix.ReplaceAllString(dateMatch, "") - return dateString + return dateString, nil } -func extractInt(pattern, content string) int { +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 val + if err != nil { + return 0, err } + return val, nil } - return 0 + return 0.0, fmt.Errorf("no match for %s", pattern) } -func extractFloat(pattern, content string) float64 { +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 val + if err != nil { + return 0, err } + return val, nil } - return 0.0 + return 0.0, fmt.Errorf("no match for %s", pattern) } -func extractString(pattern, content string) string { +func extractString(pattern, content string) (string, error) { re := regexp.MustCompile(pattern) match := re.FindStringSubmatch(content) if len(match) > 1 { - return strings.TrimSpace(match[1]) + return strings.TrimSpace(match[1]), nil + } else if len(match) > 0 { + return strings.TrimSpace(match[0]), nil } - return strings.TrimSpace(match[0]) + 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", + "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", } diff --git a/internal/service/booking/sync_test.go b/internal/service/booking/sync_test.go new file mode 100644 index 0000000..7881cbc --- /dev/null +++ b/internal/service/booking/sync_test.go @@ -0,0 +1,57 @@ +package booking_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "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 ` + +func TestParseFromApi(t *testing.T) { + externalId := "4453602306" + tests := []struct { + name string + rawContent string + expected *booking.Booking + }{ + { + name: "parse booking from raw content", + rawContent: content, + expected: &booking.Booking{ + From: time.Date(2025, time.April, 3, 0, 0, 0, 0, time.UTC), + To: time.Date(2025, time.April, 6, 0, 0, 0, 0, time.UTC), + Name: "Olga Korovina", + Email: "okorov.905387@guest.booking.com", + Platform: "Booking", + CustomerNumber: 2, + PlatformFees: 31.86, + ExternalId: &externalId, + Canceled: false, + Items: []booking.Item{ + { + BookingId: 0, + Item: "T2", + Quantity: 3, + Price: 59.0, + PaymentMethod: "Card", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + actual, err := booking.ParseBooking(tt.rawContent) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + assert.Equal(t, tt.expected, actual, "expected %v, got %v", tt.expected, actual) + }) + } +}