Browse Source

SVI начало

SVI 8 months ago
commit
cc5932b61c
3 changed files with 194 additions and 0 deletions
  1. 62 0
      README.md
  2. 3 0
      go.mod
  3. 129 0
      main.go

+ 62 - 0
README.md

@@ -0,0 +1,62 @@
+# Application design
+
+В коде представлен прототип сервиса бронирования номеров в отелях,
+в котором реализована возможность забронировать свободный номер в отеле.
+
+Сервис будет развиваться, например:
+
+- появится отправка письма-подтверждения о бронировании
+- появятся скидки, промокоды, программы лояльности
+- появится возможность бронирования нескольких номеров
+
+## Задание
+
+Провести рефакторинг структуры и кода приложения, исправить существующие
+проблемы в логике. Персистентное хранение реализовывать не требуется,
+все данные храним в памяти сервиса.
+
+В результате выполнения задания ожидается структурированный код сервиса,
+с корректно работающей логикой сценариев бронирования номеров в отелях.
+
+Чеклист:
+
+- код реорганизован и выделены слои
+- выделены абстракций и интерфейсы
+- техническе и логические ошибки исправлены
+
+Ограничения:
+
+- ожидаем реализацию, которая управляет состоянием в памяти приложения,
+ но которую легко заменить на внешнее хранилище
+- если у тебя есть опыт с Go: для решения надо использовать только
+ стандартную библиотеку Go + роутер (например chi)
+- если у тебя нет опыта с Go: можно реализовать решение на своем
+ любимом стеке технологий
+
+## Что будет на встрече
+
+На встрече ожидаем что ты продемонстрируешь экран и презентуешь свое решение:
+расскажешь какими проблемами обладает исходный код и как они решены в твоем варианте.
+Мы будем задавать вопросы о том почему было решено разделить ответственность между
+компонентами тем или иным образом, какими принципами ты при этом руководствуешься.
+Спросим что будет если продакт решит добавить какую-то новую фичу — как она ляжет
+на предложенную тобой структуру. Также можем поговорить и о более технических вещах:
+о значениях и указателях, многопоточности, интерфейсах, каналах.
+
+## Например
+
+```sh
+go run main.go
+```
+
+```sh
+curl --location --request POST 'localhost:8080/orders' \
+--header 'Content-Type: application/json' \
+--data-raw '{
+    "hotel_id": "reddison",
+    "room_id": "lux",
+    "email": "guest@mail.ru",
+    "from": "2024-01-02T00:00:00Z",
+    "to": "2024-01-04T00:00:00Z"
+}'
+```

+ 3 - 0
go.mod

@@ -0,0 +1,3 @@
+module applicationDesignTest
+
+go 1.21.6

+ 129 - 0
main.go

@@ -0,0 +1,129 @@
+// Ниже реализован сервис бронирования номеров в отеле. В предметной области
+// выделены два понятия: Order — заказ, который включает в себя даты бронирования
+// и контакты пользователя, и RoomAvailability — количество свободных номеров на
+// конкретный день.
+//
+// Задание:
+// - провести рефакторинг кода с выделением слоев и абстракций
+// - применить best-practices там где это имеет смысл
+// - исправить имеющиеся в реализации логические и технические ошибки и неточности
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+type Order struct {
+	HotelID   string    `json:"hotel_id"`
+	RoomID    string    `json:"room_id"`
+	UserEmail string    `json:"email"`
+	From      time.Time `json:"from"`
+	To        time.Time `json:"to"`
+}
+
+var Orders = []Order{}
+
+type RoomAvailability struct {
+	HotelID string    `json:"hotel_id"`
+	RoomID  string    `json:"room_id"`
+	Date    time.Time `json:"date"`
+	Quota   int       `json:"quota"`
+}
+
+var Availability = []RoomAvailability{
+	{"reddison", "lux", date(2024, 1, 1), 1},
+	{"reddison", "lux", date(2024, 1, 2), 1},
+	{"reddison", "lux", date(2024, 1, 3), 1},
+	{"reddison", "lux", date(2024, 1, 4), 1},
+	{"reddison", "lux", date(2024, 1, 5), 0},
+}
+
+func main() {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/orders", createOrder)
+
+	LogInfo("Server listening on localhost:8080")
+	err := http.ListenAndServe(":8080", mux)
+	if errors.Is(err, http.ErrServerClosed) {
+		LogInfo("Server closed")
+	} else if err != nil {
+		LogErrorf("Server failed: %s", err)
+		os.Exit(1)
+	}
+}
+
+func createOrder(w http.ResponseWriter, r *http.Request) {
+	var newOrder Order
+	json.NewDecoder(r.Body).Decode(&newOrder)
+
+	daysToBook := daysBetween(newOrder.From, newOrder.To)
+
+	unavailableDays := make(map[time.Time]struct{})
+	for _, day := range daysToBook {
+		unavailableDays[day] = struct{}{}
+	}
+
+	for _, dayToBook := range daysToBook {
+		for i, availability := range Availability {
+			if !availability.Date.Equal(dayToBook) || availability.Quota < 1 {
+				continue
+			}
+			availability.Quota -= 1
+			Availability[i] = availability
+			delete(unavailableDays, dayToBook)
+		}
+	}
+
+	if len(unavailableDays) != 0 {
+		http.Error(w, "Hotel room is not available for selected dates", http.StatusInternalServerError)
+		LogErrorf("Hotel room is not available for selected dates:\n%v\n%v", newOrder, unavailableDays)
+		return
+	}
+
+	Orders = append(Orders, newOrder)
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusCreated)
+	json.NewEncoder(w).Encode(newOrder)
+
+	LogInfo("Order successfully created: %v", newOrder)
+}
+
+func daysBetween(from time.Time, to time.Time) []time.Time {
+	if from.After(to) {
+		return nil
+	}
+
+	days := make([]time.Time, 0)
+	for d := toDay(from); !d.After(toDay(to)); d = d.AddDate(0, 0, 1) {
+		days = append(days, d)
+	}
+
+	return days
+}
+
+func toDay(timestamp time.Time) time.Time {
+	return time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, time.UTC)
+}
+
+func date(year, month, day int) time.Time {
+	return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
+}
+
+var logger = log.Default()
+
+func LogErrorf(format string, v ...any) {
+	msg := fmt.Sprintf(format, v...)
+	logger.Printf("[Error]: %s\n", msg)
+}
+
+func LogInfo(format string, v ...any) {
+	msg := fmt.Sprintf(format, v...)
+	logger.Printf("[Info]: %s\n", msg)
+}