Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 89 additions & 24 deletions internal/adapters/postgres/limit_postgresql_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,22 @@ import (
// - sql.Null* types for nullable database columns
// - JSON serialization for scopes array
type LimitPostgreSQLModel struct {
ID string `db:"id"`
Name string `db:"name"`
Description sql.NullString `db:"description"`
LimitType string `db:"limit_type"`
MaxAmount decimal.Decimal `db:"max_amount"`
Currency string `db:"currency"`
Scopes string `db:"scopes"`
Status string `db:"status"`
ResetAt sql.NullTime `db:"reset_at"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
ID string `db:"id"`
Name string `db:"name"`
Description sql.NullString `db:"description"`
LimitType string `db:"limit_type"`
MaxAmount decimal.Decimal `db:"max_amount"`
Currency string `db:"currency"`
Scopes string `db:"scopes"`
Status string `db:"status"`
ResetAt sql.NullTime `db:"reset_at"`
ActiveTimeStart sql.NullString `db:"active_time_start"`
ActiveTimeEnd sql.NullString `db:"active_time_end"`
CustomStartDate sql.NullTime `db:"custom_start_date"`
CustomEndDate sql.NullTime `db:"custom_end_date"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
DeletedAt sql.NullTime `db:"deleted_at"`
}

// ToEntity converts the database model to a domain entity.
Expand Down Expand Up @@ -91,19 +95,54 @@ func (m *LimitPostgreSQLModel) ToEntity() (*model.Limit, error) {
return nil, fmt.Errorf("invalid limit status in database: %s", m.Status)
}

// Convert time windows from database strings to TimeOfDay
var activeTimeStart, activeTimeEnd *model.TimeOfDay

if m.ActiveTimeStart.Valid {
parsed, err := model.NewTimeOfDay(m.ActiveTimeStart.String)
if err != nil {
return nil, fmt.Errorf("invalid active_time_start in database: %w", err)
}

activeTimeStart = &parsed
}

if m.ActiveTimeEnd.Valid {
parsed, err := model.NewTimeOfDay(m.ActiveTimeEnd.String)
if err != nil {
return nil, fmt.Errorf("invalid active_time_end in database: %w", err)
}

activeTimeEnd = &parsed
}

// Convert custom period dates
var customStartDate, customEndDate *time.Time
if m.CustomStartDate.Valid {
customStartDate = &m.CustomStartDate.Time
}

if m.CustomEndDate.Valid {
customEndDate = &m.CustomEndDate.Time
}

return &model.Limit{
ID: id,
Name: m.Name,
Description: description,
LimitType: limitType,
MaxAmount: m.MaxAmount,
Currency: m.Currency,
Scopes: scopes,
Status: status,
ResetAt: resetAt,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
DeletedAt: deletedAt,
ID: id,
Name: m.Name,
Description: description,
LimitType: limitType,
MaxAmount: m.MaxAmount,
Currency: m.Currency,
Scopes: scopes,
Status: status,
ResetAt: resetAt,
ActiveTimeStart: activeTimeStart,
ActiveTimeEnd: activeTimeEnd,
CustomStartDate: customStartDate,
CustomEndDate: customEndDate,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
DeletedAt: deletedAt,
}, nil
}

Expand Down Expand Up @@ -161,5 +200,31 @@ func (m *LimitPostgreSQLModel) FromEntity(entity *model.Limit) error {
m.DeletedAt = sql.NullTime{Valid: false}
}

// Convert time windows to strings
if entity.ActiveTimeStart != nil {
m.ActiveTimeStart = sql.NullString{String: entity.ActiveTimeStart.String(), Valid: true}
} else {
m.ActiveTimeStart = sql.NullString{Valid: false}
}

if entity.ActiveTimeEnd != nil {
m.ActiveTimeEnd = sql.NullString{String: entity.ActiveTimeEnd.String(), Valid: true}
} else {
m.ActiveTimeEnd = sql.NullString{Valid: false}
}

// Convert custom period dates
if entity.CustomStartDate != nil {
m.CustomStartDate = sql.NullTime{Time: *entity.CustomStartDate, Valid: true}
} else {
m.CustomStartDate = sql.NullTime{Valid: false}
}

if entity.CustomEndDate != nil {
m.CustomEndDate = sql.NullTime{Time: *entity.CustomEndDate, Valid: true}
} else {
m.CustomEndDate = sql.NullTime{Valid: false}
}

return nil
}
20 changes: 16 additions & 4 deletions internal/adapters/postgres/limit_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ func (r *LimitRepository) Create(ctx context.Context, lmt *model.Limit) error {
}

query := sq.Insert(r.tableName).
Columns("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "created_at", "updated_at").
Values(dbModel.ID, dbModel.Name, dbModel.Description, dbModel.LimitType, dbModel.MaxAmount, dbModel.Currency, dbModel.Scopes, dbModel.Status, dbModel.ResetAt, dbModel.CreatedAt, dbModel.UpdatedAt).
Columns("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "active_time_start", "active_time_end", "custom_start_date", "custom_end_date", "created_at", "updated_at").
Values(dbModel.ID, dbModel.Name, dbModel.Description, dbModel.LimitType, dbModel.MaxAmount, dbModel.Currency, dbModel.Scopes, dbModel.Status, dbModel.ResetAt, dbModel.ActiveTimeStart, dbModel.ActiveTimeEnd, dbModel.CustomStartDate, dbModel.CustomEndDate, dbModel.CreatedAt, dbModel.UpdatedAt).
PlaceholderFormat(sq.Dollar)

sqlStr, args, err := query.ToSql()
Expand Down Expand Up @@ -133,7 +133,7 @@ func (r *LimitRepository) GetByID(ctx context.Context, limitID uuid.UUID) (*mode
return nil, fmt.Errorf("failed to get database connection: %w", err)
}

query := sq.Select("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "created_at", "updated_at", "deleted_at").
query := sq.Select("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "active_time_start", "active_time_end", "custom_start_date", "custom_end_date", "created_at", "updated_at", "deleted_at").
From(r.tableName).
Where(sq.Eq{"id": limitID}).
Where(sq.Eq{"deleted_at": nil}).
Expand Down Expand Up @@ -194,7 +194,7 @@ func (r *LimitRepository) List(ctx context.Context, filters *model.ListLimitsFil
return nil, fmt.Errorf("failed to get database connection: %w", err)
}

query := sq.Select("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "created_at", "updated_at", "deleted_at").
query := sq.Select("id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "active_time_start", "active_time_end", "custom_start_date", "custom_end_date", "created_at", "updated_at", "deleted_at").
From(r.tableName).
Where(sq.Eq{"deleted_at": nil}).
PlaceholderFormat(sq.Dollar)
Expand Down Expand Up @@ -324,6 +324,10 @@ func (r *LimitRepository) Update(ctx context.Context, lmt *model.Limit) error {
Set("max_amount", dbModel.MaxAmount).
Set("scopes", dbModel.Scopes).
Set("status", dbModel.Status).
Set("active_time_start", dbModel.ActiveTimeStart).
Set("active_time_end", dbModel.ActiveTimeEnd).
Set("custom_start_date", dbModel.CustomStartDate).
Set("custom_end_date", dbModel.CustomEndDate).
Set("updated_at", dbModel.UpdatedAt).
Where(sq.Eq{"id": dbModel.ID}).
Where(sq.Eq{"deleted_at": nil}).
Expand Down Expand Up @@ -695,6 +699,10 @@ func (r *LimitRepository) scanLimit(ctx context.Context, row *sql.Row) (*model.L
&scopesJSON,
&dbModel.Status,
&dbModel.ResetAt,
&dbModel.ActiveTimeStart,
&dbModel.ActiveTimeEnd,
&dbModel.CustomStartDate,
&dbModel.CustomEndDate,
&dbModel.CreatedAt,
&dbModel.UpdatedAt,
&dbModel.DeletedAt,
Expand Down Expand Up @@ -743,6 +751,10 @@ func (r *LimitRepository) scanLimitFromRows(ctx context.Context, rows *sql.Rows)
&scopesJSON,
&dbModel.Status,
&dbModel.ResetAt,
&dbModel.ActiveTimeStart,
&dbModel.ActiveTimeEnd,
&dbModel.CustomStartDate,
&dbModel.CustomEndDate,
&dbModel.CreatedAt,
&dbModel.UpdatedAt,
&dbModel.DeletedAt,
Expand Down
48 changes: 42 additions & 6 deletions internal/adapters/postgres/limit_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func testLimit() *model.Limit {

// limitColumns returns the column names for limit queries.
func limitColumns() []string {
return []string{"id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "created_at", "updated_at", "deleted_at"}
return []string{"id", "name", "description", "limit_type", "max_amount", "currency", "scopes", "status", "reset_at", "active_time_start", "active_time_end", "custom_start_date", "custom_end_date", "created_at", "updated_at", "deleted_at"}
}

// limitRow creates a sqlmock row from a limit.
Expand All @@ -92,6 +92,22 @@ func limitRow(t *testing.T, lmt *model.Limit) *sqlmock.Rows {
resetAt = *lmt.ResetAt
}

var activeTimeStart, activeTimeEnd interface{}
if lmt.ActiveTimeStart != nil {
activeTimeStart = lmt.ActiveTimeStart.String()
}
if lmt.ActiveTimeEnd != nil {
activeTimeEnd = lmt.ActiveTimeEnd.String()
}

var customStartDate, customEndDate interface{}
if lmt.CustomStartDate != nil {
customStartDate = *lmt.CustomStartDate
}
if lmt.CustomEndDate != nil {
customEndDate = *lmt.CustomEndDate
}

return sqlmock.NewRows(limitColumns()).
AddRow(
lmt.ID,
Expand All @@ -103,6 +119,10 @@ func limitRow(t *testing.T, lmt *model.Limit) *sqlmock.Rows {
scopesJSON,
lmt.Status,
resetAt,
activeTimeStart,
activeTimeEnd,
customStartDate,
customEndDate,
lmt.CreatedAt,
lmt.UpdatedAt,
deletedAt,
Expand Down Expand Up @@ -153,6 +173,10 @@ func TestLimitRepository_Create(t *testing.T) {
sqlmock.AnyArg(), // scopesJSON
lmt.Status,
sqlmock.AnyArg(), // resetAt - may be normalized
sqlmock.AnyArg(), // activeTimeStart
sqlmock.AnyArg(), // activeTimeEnd
sqlmock.AnyArg(), // customStartDate
sqlmock.AnyArg(), // customEndDate
sqlmock.AnyArg(), // createdAt - may be set by Create
sqlmock.AnyArg(), // updatedAt - may be set by Create
).
Expand Down Expand Up @@ -181,6 +205,10 @@ func TestLimitRepository_Create(t *testing.T) {
sqlmock.AnyArg(), // scopesJSON
lmt.Status,
sqlmock.AnyArg(), // resetAt - may be normalized
sqlmock.AnyArg(), // activeTimeStart
sqlmock.AnyArg(), // activeTimeEnd
sqlmock.AnyArg(), // customStartDate
sqlmock.AnyArg(), // customEndDate
sqlmock.AnyArg(), // createdAt - may be set by Create
sqlmock.AnyArg(), // updatedAt - may be set by Create
).
Expand Down Expand Up @@ -357,7 +385,7 @@ func TestLimitRepository_List(t *testing.T) {
mockSetup: func(mock sqlmock.Sqlmock) {
rows := limitRow(t, testLimit())
// Query fetches limit+1 (11) to detect hasMore; no filter args since only deleted_at IS NULL
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, active_time_start, active_time_end, custom_start_date, custom_end_date, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
WillReturnRows(rows)
},
wantLen: 1,
Expand All @@ -375,7 +403,7 @@ func TestLimitRepository_List(t *testing.T) {
mockSetup: func(mock sqlmock.Sqlmock) {
rows := limitRow(t, testLimit())
// Query includes status filter arg; limit+1 (11) for hasMore detection
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL AND status = $1 ORDER BY created_at DESC, id DESC LIMIT 11`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, active_time_start, active_time_end, custom_start_date, custom_end_date, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL AND status = $1 ORDER BY created_at DESC, id DESC LIMIT 11`)).
WithArgs(string(model.LimitStatusActive)).
WillReturnRows(rows)
},
Expand All @@ -394,7 +422,7 @@ func TestLimitRepository_List(t *testing.T) {
mockSetup: func(mock sqlmock.Sqlmock) {
rows := limitRow(t, testLimit())
// Query includes limit_type filter arg; limit+1 (11) for hasMore detection
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL AND limit_type = $1 ORDER BY created_at DESC, id DESC LIMIT 11`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, active_time_start, active_time_end, custom_start_date, custom_end_date, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL AND limit_type = $1 ORDER BY created_at DESC, id DESC LIMIT 11`)).
WithArgs(string(model.LimitTypeDaily)).
WillReturnRows(rows)
},
Expand All @@ -406,7 +434,7 @@ func TestLimitRepository_List(t *testing.T) {
filters: &model.ListLimitsFilter{Limit: 10},
mockSetup: func(mock sqlmock.Sqlmock) {
// Query uses limit+1 (11) even for empty results
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, active_time_start, active_time_end, custom_start_date, custom_end_date, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
WillReturnRows(sqlmock.NewRows(limitColumns()))
},
wantLen: 0,
Expand All @@ -416,7 +444,7 @@ func TestLimitRepository_List(t *testing.T) {
name: "Error - database query fails",
filters: &model.ListLimitsFilter{Limit: 10},
mockSetup: func(mock sqlmock.Sqlmock) {
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, name, description, limit_type, max_amount, currency, scopes, status, reset_at, active_time_start, active_time_end, custom_start_date, custom_end_date, created_at, updated_at, deleted_at FROM limits WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC LIMIT 11`)).
WillReturnError(errors.New("database error"))
},
wantErr: true,
Expand Down Expand Up @@ -491,6 +519,10 @@ func TestLimitRepository_Update(t *testing.T) {
lmt.MaxAmount,
sqlmock.AnyArg(), // scopesJSON
lmt.Status,
sqlmock.AnyArg(), // activeTimeStart
sqlmock.AnyArg(), // activeTimeEnd
sqlmock.AnyArg(), // customStartDate
sqlmock.AnyArg(), // customEndDate
lmt.UpdatedAt,
lmt.ID,
).
Expand Down Expand Up @@ -1633,6 +1665,7 @@ func TestLimitRepository_List_Pagination_HasMore(t *testing.T) {
rows.AddRow(
lmt.ID, lmt.Name, lmt.Description, lmt.LimitType, lmt.MaxAmount,
lmt.Currency, scopesJSON, lmt.Status, resetAt,
nil, nil, nil, nil,
lmt.CreatedAt, lmt.UpdatedAt, nil,
)
}
Expand Down Expand Up @@ -1693,6 +1726,7 @@ func TestLimitRepository_List_Pagination_NoMore(t *testing.T) {
rows.AddRow(
lmt.ID, lmt.Name, lmt.Description, lmt.LimitType, lmt.MaxAmount,
lmt.Currency, scopesJSON, lmt.Status, resetAt,
nil, nil, nil, nil,
lmt.CreatedAt, lmt.UpdatedAt, nil,
)
}
Expand Down Expand Up @@ -1807,6 +1841,7 @@ func TestLimitRepository_List_Pagination_ExactlyAtLimit(t *testing.T) {
rows.AddRow(
lmt.ID, lmt.Name, lmt.Description, lmt.LimitType, lmt.MaxAmount,
lmt.Currency, scopesJSON, lmt.Status, resetAt,
nil, nil, nil, nil,
lmt.CreatedAt, lmt.UpdatedAt, nil,
)
}
Expand Down Expand Up @@ -1890,6 +1925,7 @@ func TestLimitRepository_List_Pagination_CursorWithDifferentSortFields(t *testin
rows.AddRow(
lmt.ID, lmt.Name, lmt.Description, lmt.LimitType, lmt.MaxAmount,
lmt.Currency, scopesJSON, lmt.Status, resetAt,
nil, nil, nil, nil,
lmt.CreatedAt, lmt.UpdatedAt, nil,
)
}
Expand Down
Loading
Loading