# Models Layer Agent Instructions

## Overview

Models contain **core business logic**. This is the heaviest layer - focus on reusability and testability.

## Package Structure

```
models/<feature>/
├── interface.go       # Interface definitions
├── impl/              # Implementations
│   ├── <feature>.go
│   └── <feature>_test.go
├── mock/              # Auto-generated mocks (DO NOT EDIT)
└── domain/            # Shared domain types (in models/domain/)
```

## Interface Pattern

**Define in `interface.go`:**
```go
package backup

import (
    "context"
    "github.com/tidbcloud/dataflow-service/app/models/domain"
    "github.com/tidbcloud/pkgv2/bizerr"
)

//nolint:interfacebloat  // Use for large interfaces
type BackupModel interface {
    Create(ctx context.Context, req *domain.CreateBackupReq) (*domain.CreateBackupResp, bizerr.BizError)
    Get(ctx context.Context, id uint64) (*domain.Backup, bizerr.BizError)
    List(ctx context.Context, req *domain.ListBackupsReq) (*domain.ListBackupsResp, bizerr.BizError)
    Delete(ctx context.Context, id uint64) bizerr.BizError
}
```

**Key rules:**
- First param is always `context.Context`
- Return `bizerr.BizError` not `error`
- Use `domain.*` types, NOT gRPC/entity types

## Implementation Pattern

**Create in `impl/<feature>.go`:**
```go
package impl

import backup "github.com/tidbcloud/dataflow-service/app/models/backup"

// Compile-time interface verification
var _ backup.BackupModel = &backupModelImpl{}

type backupModelImpl struct {
    transProvider db.TransProvider
    backupRepo    backup_repo.BackupRepo
    coreCli       grpcli.CoreGrpcClient
    workflow      workflow.Workflow
}

// Constructor returns interface type
func NewBackupModel(
    transProvider db.TransProvider,
    backupRepo backup_repo.BackupRepo,
    coreCli grpcli.CoreGrpcClient,
    workflow workflow.Workflow,
) backup.BackupModel {
    return &backupModelImpl{
        transProvider: transProvider,
        backupRepo:    backupRepo,
        coreCli:       coreCli,
        workflow:      workflow,
    }
}
```

## Method Implementation

```go
func (m *backupModelImpl) Create(
    ctx context.Context, req *domain.CreateBackupReq,
) (*domain.CreateBackupResp, bizerr.BizError) {
    // 1. Setup logging
    ctx = log.WithTraceID(ctx)
    logger := log.GetLogger(ctx)
    
    // 2. Validation / permission checks
    cluster, err := m.coreCli.GetClusterByID(ctx, req.ClusterID)
    if err != nil {
        return nil, bizerr_wrapper.NewInternalErr("get cluster failed")
    }
    
    // 3. Business logic with transactions
    var backupID uint64
    transErr := m.transProvider.Transaction(ctx, func(ctx context.Context) error {
        entity := &entity.Backup{...}
        if err := m.backupRepo.Create(ctx, entity); err != nil {
            return err.Chain("create backup entity")
        }
        backupID = entity.ID.Uint64()
        return nil
    })
    if transErr != nil {
        return nil, wrapTransactionError(transErr)
    }
    
    // 4. Post-transaction operations (workflows, async tasks)
    if _, err := m.workflow.RunDag(workflow.DagCreateBackup, map[string]string{
        workflow.KeyBackupID: strconv.FormatUint(backupID, 10),
    }); err != nil {
        logger.Error("failed to start workflow", zap.Error(err))
    }
    
    return &domain.CreateBackupResp{BackupID: backupID}, nil
}
```

## Error Handling

```go
// Create business errors
return bizerr.New(config.ErrCodeNotFound, "backup %d not found", id)

// Use wrapper for common cases
return bizerr_wrapper.NewInternalErr("operation failed")
return bizerr_wrapper.NewNotFoundErr("backup not found")
return bizerr_wrapper.NewInvalidInputError("invalid parameter")

// Chain errors for context
bizErr, err := m.repo.Load(ctx, id)
if err != nil {
    return err.Chain("load backup %d", id)
}

// Check specific error codes
if err.ErrCode() == bizerr.ErrCodeRecordNotFound {
    // Handle not found case
}
```

## Domain Types (`models/domain/`)

**Naming conventions:**
```go
// Request: <Action><Feature>Req
type CreateBackupReq struct { ... }
type ListBackupsReq struct { ... }

// Response: <Action><Feature>Resp  
type CreateBackupResp struct { ... }
type ListBackupsResp struct { ... }

// Domain entity (business view)
type Backup struct {
    BackupID    uint64
    ClusterID   uint64
    Status      BackupStatus
    // ... business fields
}
```

**Conversion functions in `*_cvt.go`:**
```go
// Entity → Domain
func BackupEntity2Domain(e *entity.Backup) *Backup { ... }

// Domain → Dataflow (gRPC response)
func (b *Backup) ToDataflow() *dataflow.Backup { ... }
```

## Filter Generation

```go
// In domain types
func (r *ListBackupsReq) FilterOps() []repo.BackupFilterOp {
    ops := make([]repo.BackupFilterOp, 0)
    if r.ClusterID > 0 {
        ops = append(ops, func(qs entity.BackupQuerySet) entity.BackupQuerySet {
            return qs.ClusterIDEq(entity.ID(r.ClusterID))
        })
    }
    return ops
}
```

## Testing Pattern

```go
type backupModelTestSuite struct {
    suite.Suite
    model backup.BackupModel
    
    mockRepo     *repo_mock.BackupRepo
    mockCoreCli  *grpcli_mock.CoreGrpcClient
    allMocks     []*mock.Call
}

func TestBackupModelSuite(t *testing.T) {
    suite.Run(t, new(backupModelTestSuite))
}

func (s *backupModelTestSuite) SetupSuite() {
    s.mockRepo = repo_mock.NewBackupRepo(s.T())
    s.mockCoreCli = grpcli_mock.NewCoreGrpcClient(s.T())
    
    db, _ := repo.GetFakeDB()
    s.model = impl.NewBackupModel(
        db.NewProvider(db),
        s.mockRepo,
        s.mockCoreCli,
    )
}

func (s *backupModelTestSuite) TestCreate() {
    tests := map[string]struct {
        req     *domain.CreateBackupReq
        setup   func()
        wantErr bool
    }{
        "success": {
            req: &domain.CreateBackupReq{ClusterID: 123},
            setup: func() {
                s.mockCoreCli.On("GetClusterByID", mock.Anything, uint64(123)).
                    Return(&core.Cluster{}, nil)
                s.mockRepo.On("Create", mock.Anything, mock.Anything).
                    Return(nil)
            },
        },
    }
    
    for name, tc := range tests {
        s.Run(name, func() {
            tc.setup()
            _, err := s.model.Create(context.Background(), tc.req)
            if tc.wantErr {
                s.Error(err)
            } else {
                s.NoError(err)
            }
        })
    }
}
```

## Status Matrix Pattern

For entities with complex state transitions:

```go
var statusMatrix = map[string]map[string]string{
    string(StatusDisabled): {
        string(ActionEnable): string(StatusEnabling),
    },
    string(StatusEnabled): {
        string(ActionDisable): string(StatusDisabling),
    },
}

func NewStatusMatrix() status_matrix.StatusMatrix {
    return statusMatrixImpl.NewStatusMatrix(statusMatrix, friendlyActionMap, steadyStateMatrix)
}
```
