diff --git a/config/db.go b/config/db.go index 53214c3..f8e1548 100644 --- a/config/db.go +++ b/config/db.go @@ -20,7 +20,7 @@ type Database struct { } func (c Database) DSN() string { - return fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s TimeZone=UTC", c.Host, c.Port, c.DB, c.Username, c.Password, c.SslMode) + return fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s TimeZone=Asia/Jakarta", c.Host, c.Port, c.DB, c.Username, c.Password, c.SslMode) } func (c Database) ConnectionMaxLifetime() time.Duration { diff --git a/internal/db/database.go b/internal/db/database.go index daf3a2a..a0180da 100644 --- a/internal/db/database.go +++ b/internal/db/database.go @@ -3,6 +3,7 @@ package db import ( "apskel-pos-be/config" "fmt" + _ "github.com/lib/pq" "go.uber.org/zap" _ "gopkg.in/yaml.v3" diff --git a/internal/transformer/analytics_transformer.go b/internal/transformer/analytics_transformer.go index 96e663d..48bbfa1 100644 --- a/internal/transformer/analytics_transformer.go +++ b/internal/transformer/analytics_transformer.go @@ -3,30 +3,22 @@ package transformer import ( "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" + "apskel-pos-be/internal/util" "fmt" "time" ) -const ddmmyyyy = "02-01-2006" - // PaymentMethodAnalyticsContractToModel converts contract request to model func PaymentMethodAnalyticsContractToModel(req *contract.PaymentMethodAnalyticsRequest) *models.PaymentMethodAnalyticsRequest { var dateFrom, dateTo time.Time - if req.DateFrom != "" { - df, err := time.Parse(ddmmyyyy, req.DateFrom) - if err == nil { - dateFrom = df - } - } - if req.DateTo != "" { - dt, err := time.Parse(ddmmyyyy, req.DateTo) - if err == nil { - dateTo = dt - } - } - if req.DateFrom == req.DateTo { - dateTo = dateTo.AddDate(0, 0, 1) + if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil { + if fromTime != nil { + dateFrom = *fromTime + } + if toTime != nil { + dateTo = *toTime + } } return &models.PaymentMethodAnalyticsRequest{ @@ -75,21 +67,14 @@ func PaymentMethodAnalyticsModelToContract(resp *models.PaymentMethodAnalyticsRe func SalesAnalyticsContractToModel(req *contract.SalesAnalyticsRequest) *models.SalesAnalyticsRequest { var dateFrom, dateTo time.Time - if req.DateFrom != "" { - df, err := time.Parse(ddmmyyyy, req.DateFrom) - if err == nil { - dateFrom = df - } - } - if req.DateTo != "" { - dt, err := time.Parse(ddmmyyyy, req.DateTo) - if err == nil { - dateTo = dt - } - } - if req.DateFrom == req.DateTo { - dateTo = dateTo.AddDate(0, 0, 1) + if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil { + if fromTime != nil { + dateFrom = *fromTime + } + if toTime != nil { + dateTo = *toTime + } } return &models.SalesAnalyticsRequest{ @@ -142,21 +127,14 @@ func SalesAnalyticsModelToContract(resp *models.SalesAnalyticsResponse) *contrac // ProductAnalyticsContractToModel converts contract request to model func ProductAnalyticsContractToModel(req *contract.ProductAnalyticsRequest) *models.ProductAnalyticsRequest { var dateFrom, dateTo time.Time - if req.DateFrom != "" { - df, err := time.Parse(ddmmyyyy, req.DateFrom) - if err == nil { - dateFrom = df - } - } - if req.DateTo != "" { - dt, err := time.Parse(ddmmyyyy, req.DateTo) - if err == nil { - dateTo = dt - } - } - if req.DateFrom == req.DateTo { - dateTo = dateTo.AddDate(0, 0, 1) + if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil { + if fromTime != nil { + dateFrom = *fromTime + } + if toTime != nil { + dateTo = *toTime + } } return &models.ProductAnalyticsRequest{ @@ -200,21 +178,15 @@ func ProductAnalyticsModelToContract(resp *models.ProductAnalyticsResponse) *con // DashboardAnalyticsContractToModel converts contract request to model func DashboardAnalyticsContractToModel(req *contract.DashboardAnalyticsRequest) *models.DashboardAnalyticsRequest { var dateFrom, dateTo time.Time - if req.DateFrom != "" { - df, err := time.Parse(ddmmyyyy, req.DateFrom) - if err == nil { - dateFrom = df - } - } - if req.DateTo != "" { - dt, err := time.Parse(ddmmyyyy, req.DateTo) - if err == nil { - dateTo = dt - } - } - if req.DateFrom == req.DateTo { - dateTo = dateTo.AddDate(0, 0, 1) + // Parse date range using utility function + if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo); err == nil { + if fromTime != nil { + dateFrom = *fromTime + } + if toTime != nil { + dateTo = *toTime + } } return &models.DashboardAnalyticsRequest{ @@ -296,21 +268,21 @@ func ProfitLossAnalyticsContractToModel(req *contract.ProfitLossAnalyticsRequest return nil, fmt.Errorf("request cannot be nil") } - dateFrom, err := time.Parse("02-01-2006", req.DateFrom) + // Parse date range using utility function + dateFrom, dateTo, err := util.ParseDateRangeToJakartaTime(req.DateFrom, req.DateTo) if err != nil { - return nil, fmt.Errorf("invalid date_from format: %w", err) + return nil, fmt.Errorf("invalid date format: %w", err) } - dateTo, err := time.Parse("02-01-2006", req.DateTo) - if err != nil { - return nil, fmt.Errorf("invalid date_to format: %w", err) + if dateFrom == nil || dateTo == nil { + return nil, fmt.Errorf("both date_from and date_to are required") } return &models.ProfitLossAnalyticsRequest{ OrganizationID: req.OrganizationID, OutletID: req.OutletID, - DateFrom: dateFrom, - DateTo: dateTo, + DateFrom: *dateFrom, + DateTo: *dateTo, GroupBy: req.GroupBy, }, nil } diff --git a/internal/transformer/order_transformer.go b/internal/transformer/order_transformer.go index 73463bb..d886408 100644 --- a/internal/transformer/order_transformer.go +++ b/internal/transformer/order_transformer.go @@ -4,8 +4,8 @@ import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/models" + "apskel-pos-be/internal/util" "strconv" - "time" "github.com/google/uuid" ) @@ -239,21 +239,9 @@ func ListOrdersQueryToModel(query *contract.ListOrdersQuery) *models.ListOrdersR } } - if query.DateFrom != "" { - if dateFrom, err := time.Parse(ddmmyyyy, query.DateFrom); err == nil { - req.DateFrom = &dateFrom - } - } - - if query.DateTo != "" { - if dateTo, err := time.Parse(ddmmyyyy, query.DateTo); err == nil { - req.DateTo = &dateTo - } - } - - if query.DateFrom != "" && query.DateFrom == query.DateTo { - newDate := req.DateTo.AddDate(0, 0, 1) - req.DateTo = &newDate + if dateFrom, dateTo, err := util.ParseDateRangeToJakartaTime(query.DateFrom, query.DateTo); err == nil { + req.DateFrom = dateFrom + req.DateTo = dateTo } return req diff --git a/internal/util/date_util.go b/internal/util/date_util.go new file mode 100644 index 0000000..e9459f5 --- /dev/null +++ b/internal/util/date_util.go @@ -0,0 +1,72 @@ +package util + +import ( + "time" +) + +const DateFormatDDMMYYYY = "02-01-2006" + +// ParseDateToJakartaTime parses a date string in DD-MM-YYYY format and converts it to Jakarta timezone +// Returns start of day (00:00:00) in Jakarta timezone +func ParseDateToJakartaTime(dateStr string) (*time.Time, error) { + if dateStr == "" { + return nil, nil + } + + date, err := time.Parse(DateFormatDDMMYYYY, dateStr) + if err != nil { + return nil, err + } + + jakartaLoc, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, err + } + + jakartaTime := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, jakartaLoc) + return &jakartaTime, nil +} + +// ParseDateToJakartaTimeEndOfDay parses a date string in DD-MM-YYYY format and converts it to Jakarta timezone +// Returns end of day (23:59:59.999999999) in Jakarta timezone +func ParseDateToJakartaTimeEndOfDay(dateStr string) (*time.Time, error) { + if dateStr == "" { + return nil, nil + } + + date, err := time.Parse(DateFormatDDMMYYYY, dateStr) + if err != nil { + return nil, err + } + + jakartaLoc, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, err + } + + jakartaTime := time.Date(date.Year(), date.Month(), date.Day(), 23, 59, 59, 999999999, jakartaLoc) + return &jakartaTime, nil +} + +// ParseDateRangeToJakartaTime parses date_from and date_to strings and returns them in Jakarta timezone +// date_from will be start of day (00:00:00), date_to will be end of day (23:59:59.999999999) +func ParseDateRangeToJakartaTime(dateFrom, dateTo string) (*time.Time, *time.Time, error) { + var fromTime, toTime *time.Time + var err error + + if dateFrom != "" { + fromTime, err = ParseDateToJakartaTime(dateFrom) + if err != nil { + return nil, nil, err + } + } + + if dateTo != "" { + toTime, err = ParseDateToJakartaTimeEndOfDay(dateTo) + if err != nil { + return nil, nil, err + } + } + + return fromTime, toTime, nil +} diff --git a/internal/util/date_util_test.go b/internal/util/date_util_test.go new file mode 100644 index 0000000..54ff167 --- /dev/null +++ b/internal/util/date_util_test.go @@ -0,0 +1,211 @@ +package util + +import ( + "testing" + "time" +) + +func TestParseDateToJakartaTime(t *testing.T) { + tests := []struct { + name string + dateStr string + expected *time.Time + hasError bool + }{ + { + name: "valid date", + dateStr: "06-08-2025", + expected: nil, // Will be set during test + hasError: false, + }, + { + name: "empty string", + dateStr: "", + expected: nil, + hasError: false, + }, + { + name: "invalid date format", + dateStr: "2025-08-06", + hasError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseDateToJakartaTime(tt.dateStr) + + if tt.hasError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if tt.expected == nil && tt.dateStr == "" { + if result != nil { + t.Errorf("Expected nil but got %v", result) + } + return + } + + if result == nil && tt.dateStr != "" { + t.Errorf("Expected time but got nil") + return + } + + // Check if it's in Jakarta timezone + jakartaLoc, _ := time.LoadLocation("Asia/Jakarta") + if result.Location().String() != jakartaLoc.String() { + t.Errorf("Expected Jakarta timezone but got %v", result.Location()) + } + + // Check if it's start of day + if result.Hour() != 0 || result.Minute() != 0 || result.Second() != 0 { + t.Errorf("Expected start of day but got %v", result.Format("15:04:05")) + } + }) + } +} + +func TestParseDateToJakartaTimeEndOfDay(t *testing.T) { + tests := []struct { + name string + dateStr string + expected *time.Time + hasError bool + }{ + { + name: "valid date", + dateStr: "06-08-2025", + expected: nil, // Will be set during test + hasError: false, + }, + { + name: "empty string", + dateStr: "", + expected: nil, + hasError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ParseDateToJakartaTimeEndOfDay(tt.dateStr) + + if tt.hasError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + if tt.expected == nil && tt.dateStr == "" { + if result != nil { + t.Errorf("Expected nil but got %v", result) + } + return + } + + if result == nil && tt.dateStr != "" { + t.Errorf("Expected time but got nil") + return + } + + // Check if it's in Jakarta timezone + jakartaLoc, _ := time.LoadLocation("Asia/Jakarta") + if result.Location().String() != jakartaLoc.String() { + t.Errorf("Expected Jakarta timezone but got %v", result.Location()) + } + + // Check if it's end of day + if result.Hour() != 23 || result.Minute() != 59 || result.Second() != 59 { + t.Errorf("Expected end of day but got %v", result.Format("15:04:05")) + } + }) + } +} + +func TestParseDateRangeToJakartaTime(t *testing.T) { + tests := []struct { + name string + dateFrom string + dateTo string + hasError bool + }{ + { + name: "valid date range", + dateFrom: "06-08-2025", + dateTo: "06-08-2025", + hasError: false, + }, + { + name: "empty strings", + dateFrom: "", + dateTo: "", + hasError: false, + }, + { + name: "only date_from", + dateFrom: "06-08-2025", + dateTo: "", + hasError: false, + }, + { + name: "only date_to", + dateFrom: "", + dateTo: "06-08-2025", + hasError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fromTime, toTime, err := ParseDateRangeToJakartaTime(tt.dateFrom, tt.dateTo) + + if tt.hasError { + if err == nil { + t.Errorf("Expected error but got none") + } + return + } + + if err != nil { + t.Errorf("Unexpected error: %v", err) + return + } + + // If dateFrom is provided, check it's start of day + if tt.dateFrom != "" && fromTime != nil { + jakartaLoc, _ := time.LoadLocation("Asia/Jakarta") + if fromTime.Location().String() != jakartaLoc.String() { + t.Errorf("Expected Jakarta timezone for date_from but got %v", fromTime.Location()) + } + if fromTime.Hour() != 0 || fromTime.Minute() != 0 || fromTime.Second() != 0 { + t.Errorf("Expected start of day for date_from but got %v", fromTime.Format("15:04:05")) + } + } + + // If dateTo is provided, check it's end of day + if tt.dateTo != "" && toTime != nil { + jakartaLoc, _ := time.LoadLocation("Asia/Jakarta") + if toTime.Location().String() != jakartaLoc.String() { + t.Errorf("Expected Jakarta timezone for date_to but got %v", toTime.Location()) + } + if toTime.Hour() != 23 || toTime.Minute() != 59 || toTime.Second() != 59 { + t.Errorf("Expected end of day for date_to but got %v", toTime.Format("15:04:05")) + } + } + }) + } +}