Эх сурвалжийг харах

feat: add OrderService with full state machine tests

b 1 сар өмнө
parent
commit
6263b3a666
4 өөрчлөгдсөн 255 нэмэгдсэн , 0 устгасан
  1. 2 0
      go.mod
  2. 2 0
      go.sum
  3. 83 0
      service/order.go
  4. 168 0
      service/order_test.go

+ 2 - 0
go.mod

@@ -1,3 +1,5 @@
 module job-cheng-xing
 
 go 1.25.0
+
+require github.com/google/uuid v1.6.0 // indirect

+ 2 - 0
go.sum

@@ -0,0 +1,2 @@
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

+ 83 - 0
service/order.go

@@ -0,0 +1,83 @@
+package service
+
+import (
+	"time"
+
+	"job-cheng-xing/model"
+	"job-cheng-xing/repository"
+
+	"github.com/google/uuid"
+)
+
+type OrderService struct {
+	repo repository.OrderRepository
+}
+
+func NewOrderService(repo repository.OrderRepository) *OrderService {
+	return &OrderService{repo: repo}
+}
+
+func (s *OrderService) CreateOrder(req model.CreateOrderRequest) (*model.Order, error) {
+	now := time.Now()
+	order := &model.Order{
+		ID:          "ord_" + uuid.NewString()[:8],
+		Status:      model.StatusPending,
+		ServiceTime: req.ServiceTime,
+		Duration:    req.Duration,
+		Address:     req.Address,
+		ProviderID:  nil,
+		CreatedAt:   now,
+		UpdatedAt:   now,
+	}
+	if err := s.repo.Create(order); err != nil {
+		return nil, err
+	}
+	return order, nil
+}
+
+func (s *OrderService) AcceptOrder(id, providerID string) (*model.Order, error) {
+	order, err := s.repo.FindByID(id)
+	if err != nil {
+		return nil, err
+	}
+
+	if order.Status == model.StatusCanceled {
+		return nil, model.ErrInvalidState("接单")
+	}
+	if order.Status != model.StatusPending {
+		return nil, model.ErrConflict
+	}
+
+	pid := providerID
+	updated := &model.Order{
+		Status:     model.StatusAccepted,
+		ProviderID: &pid,
+		UpdatedAt:  time.Now(),
+	}
+	if err := s.repo.Update(id, model.StatusPending, updated); err != nil {
+		return nil, err
+	}
+
+	return s.repo.FindByID(id)
+}
+
+func (s *OrderService) CancelOrder(id string) (*model.Order, error) {
+	order, err := s.repo.FindByID(id)
+	if err != nil {
+		return nil, err
+	}
+
+	if order.Status == model.StatusCanceled {
+		return nil, model.ErrInvalidState("取消")
+	}
+
+	updated := &model.Order{
+		Status:    model.StatusCanceled,
+		UpdatedAt: time.Now(),
+	}
+	if err := s.repo.Update(id, order.Status, updated); err != nil {
+		return nil, err
+	}
+
+	return s.repo.FindByID(id)
+}

+ 168 - 0
service/order_test.go

@@ -0,0 +1,168 @@
+package service
+
+import (
+	"testing"
+	"time"
+
+	"job-cheng-xing/model"
+	"job-cheng-xing/repository"
+)
+
+func TestCreateOrder(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	req := model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳区xxx路xxx号",
+	}
+
+	order, err := svc.CreateOrder(req)
+	if err != nil {
+		t.Fatalf("CreateOrder failed: %v", err)
+	}
+	if order.ID == "" {
+		t.Error("expected non-empty ID")
+	}
+	if order.Status != model.StatusPending {
+		t.Errorf("expected pending, got %s", order.Status)
+	}
+	if order.ProviderID != nil {
+		t.Error("expected nil ProviderID")
+	}
+}
+
+func TestAcceptOrder_Success(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+
+	result, err := svc.AcceptOrder(order.ID, "prov_001")
+	if err != nil {
+		t.Fatalf("AcceptOrder failed: %v", err)
+	}
+	if result.Status != model.StatusAccepted {
+		t.Errorf("expected accepted, got %s", result.Status)
+	}
+	if *result.ProviderID != "prov_001" {
+		t.Errorf("expected prov_001, got %s", *result.ProviderID)
+	}
+}
+
+func TestAcceptOrder_AlreadyAccepted(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+	svc.AcceptOrder(order.ID, "prov_001")
+
+	_, err := svc.AcceptOrder(order.ID, "prov_002")
+	if err == nil {
+		t.Fatal("expected error")
+	}
+	appErr, ok := err.(*model.AppError)
+	if !ok || appErr.Code != "already_accepted" {
+		t.Errorf("expected already_accepted error, got %v", err)
+	}
+}
+
+func TestAcceptOrder_OrderNotFound(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	_, err := svc.AcceptOrder("nonexistent", "prov_001")
+	if err != model.ErrNotFound {
+		t.Errorf("expected ErrNotFound, got %v", err)
+	}
+}
+
+func TestCancelOrder_FromPending(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+
+	result, err := svc.CancelOrder(order.ID)
+	if err != nil {
+		t.Fatalf("CancelOrder failed: %v", err)
+	}
+	if result.Status != model.StatusCanceled {
+		t.Errorf("expected canceled, got %s", result.Status)
+	}
+}
+
+func TestCancelOrder_FromAccepted(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+	svc.AcceptOrder(order.ID, "prov_001")
+
+	result, err := svc.CancelOrder(order.ID)
+	if err != nil {
+		t.Fatalf("CancelOrder failed: %v", err)
+	}
+	if result.Status != model.StatusCanceled {
+		t.Errorf("expected canceled, got %s", result.Status)
+	}
+}
+
+func TestCancelOrder_AlreadyCanceled(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+	svc.CancelOrder(order.ID)
+
+	_, err := svc.CancelOrder(order.ID)
+	if err == nil {
+		t.Fatal("expected error")
+	}
+	appErr, ok := err.(*model.AppError)
+	if !ok || appErr.Code != "invalid_state" {
+		t.Errorf("expected invalid_state error, got %v", err)
+	}
+}
+
+func TestAcceptOrder_AfterCanceled(t *testing.T) {
+	repo := repository.NewMemoryRepo()
+	svc := NewOrderService(repo)
+
+	order, _ := svc.CreateOrder(model.CreateOrderRequest{
+		ServiceTime: time.Now().Add(24 * time.Hour),
+		Duration:    120,
+		Address:     "北京朝阳",
+	})
+	svc.CancelOrder(order.ID)
+
+	_, err := svc.AcceptOrder(order.ID, "prov_001")
+	if err == nil {
+		t.Fatal("expected error")
+	}
+	appErr, ok := err.(*model.AppError)
+	if !ok || appErr.Code != "invalid_state" {
+		t.Errorf("expected invalid_state error, got %v", err)
+	}
+}