* test: add test for booking parsing

* improve error handling

* fix: booking parsing

fix: guest number

fix: item name

fix: parsing

* refactor tests

* test: more
This commit is contained in:
Ruidy 2025-01-05 16:04:44 +01:00 committed by GitHub
parent 9f0b6e71cb
commit a2ce003299
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 188 additions and 55 deletions

6
go.mod
View file

@ -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

7
go.sum
View file

@ -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=

View file

@ -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,
},
}

View file

@ -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",
}

View file

@ -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)
})
}
}