perbaikan generete tool

This commit is contained in:
2025-08-18 12:23:14 +07:00
parent aee7020096
commit 27924aeb3c
12 changed files with 4215 additions and 393 deletions

View File

@@ -0,0 +1,683 @@
package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models/order"
"context"
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
// Initialize the database connection and validator
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("order_status", validateOrderStatus)
if db == nil {
log.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for order status
func validateOrderStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// OrderHandler handles order services
type OrderHandler struct {
db database.Service
}
// NewOrderHandler creates a new OrderHandler
func NewOrderHandler() *OrderHandler {
return &OrderHandler{
db: db,
}
}
// GetOrder godoc
// @Summary Get order with pagination and optional aggregation
// @Description Returns a paginated list of orders with optional summary statistics
// @Tags order
// @Accept json
// @Produce json
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Param include_summary query bool false "Include aggregation summary" default(false)
// @Param status query string false "Filter by status"
// @Param search query string false "Search in multiple fields"
// @Success 200 {object} models.OrderGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/orders [get]
func (h *OrderHandler) GetOrder(c *gin.Context) {
// Parse pagination parameters
limit, offset, err := h.parsePaginationParams(c)
if err != nil {
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
return
}
// Parse filter parameters
filter := h.parseFilterParams(c)
includeAggregation := c.Query("include_summary") == "true"
// Get database connection
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute concurrent operations
var (
items []models.Order
total int
aggregateData *models.AggregateData
wg sync.WaitGroup
errChan = make(chan error, 3)
mu sync.Mutex
)
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
mu.Lock()
errChan <- fmt.Errorf("failed to get total count: %w", err)
mu.Unlock()
}
}()
// Fetch main data - FIXED: Proper method name
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.fetchOrders(ctx, dbConn, filter, limit, offset)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to fetch data: %w", err)
} else {
items = result
}
mu.Unlock()
}()
// Fetch aggregation data if requested
if includeAggregation {
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.getAggregateData(ctx, dbConn, filter)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
} else {
aggregateData = result
}
mu.Unlock()
}()
}
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
return
}
}
// Build response
meta := h.calculateMeta(limit, offset, total)
response := models.OrderGetResponse{
Message: "Data order berhasil diambil",
Data: items,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetOrderByID godoc
// @Summary Get Order by ID
// @Description Returns a single order by ID
// @Tags order
// @Accept json
// @Produce json
// @Param id path string true "Order ID (UUID)"
// @Success 200 {object} models.OrderGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Order not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/order/{id} [get]
func (h *OrderHandler) GetOrderByID(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.getOrderByID(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Order not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get order", err, http.StatusInternalServerError)
}
return
}
response := models.OrderGetByIDResponse{
Message: "Order details retrieved successfully",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// CreateOrder godoc
// @Summary Create order
// @Description Creates a new order record
// @Tags order
// @Accept json
// @Produce json
// @Param request body models.OrderCreateRequest true "Order creation request"
// @Success 201 {object} models.OrderCreateResponse "Order created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/orders [post]
func (h *OrderHandler) CreateOrder(c *gin.Context) {
var req models.OrderCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.createOrder(ctx, dbConn, &req)
if err != nil {
h.logAndRespondError(c, "Failed to create order", err, http.StatusInternalServerError)
return
}
response := models.OrderCreateResponse{
Message: "Order berhasil dibuat",
Data: item,
}
c.JSON(http.StatusCreated, response)
}
// UpdateOrder godoc
// @Summary Update order
// @Description Updates an existing order record
// @Tags order
// @Accept json
// @Produce json
// @Param id path string true "Order ID (UUID)"
// @Param request body models.OrderUpdateRequest true "Order update request"
// @Success 200 {object} models.OrderUpdateResponse "Order updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Order not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/order/{id} [put]
func (h *OrderHandler) UpdateOrder(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
var req models.OrderUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Set ID from path parameter
req.ID = id
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.updateOrder(ctx, dbConn, &req)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Order not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update order", err, http.StatusInternalServerError)
}
return
}
response := models.OrderUpdateResponse{
Message: "Order berhasil diperbarui",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// DeleteOrder godoc
// @Summary Delete order
// @Description Soft deletes a order by setting status to 'deleted'
// @Tags order
// @Accept json
// @Produce json
// @Param id path string true "Order ID (UUID)"
// @Success 200 {object} models.OrderDeleteResponse "Order deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Order not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/order/{id} [delete]
func (h *OrderHandler) DeleteOrder(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
err = h.deleteOrder(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Order not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to delete order", err, http.StatusInternalServerError)
}
return
}
response := models.OrderDeleteResponse{
Message: "Order berhasil dihapus",
ID: id,
}
c.JSON(http.StatusOK, response)
}
// GetOrderStats godoc
// @Summary Get order statistics
// @Description Returns comprehensive statistics about order data
// @Tags order
// @Accept json
// @Produce json
// @Param status query string false "Filter statistics by status"
// @Success 200 {object} models.AggregateData "Statistics data"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/orders/stats [get]
func (h *OrderHandler) GetOrderStats(c *gin.Context) {
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
filter := h.parseFilterParams(c)
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
if err != nil {
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Statistik order berhasil diambil",
"data": aggregateData,
})
}
// Database operations
func (h *OrderHandler) getOrderByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Order, error) {
query := "SELECT id, status, date_created, date_updated, name FROM data_order WHERE id = $1 AND status != 'deleted'"
row := dbConn.QueryRowContext(ctx, query, id)
var item models.Order
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, err
}
return &item, nil
}
func (h *OrderHandler) createOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderCreateRequest) (*models.Order, error) {
id := uuid.New().String()
now := time.Now()
query := "INSERT INTO data_order (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
var item models.Order
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to create order: %w", err)
}
return &item, nil
}
func (h *OrderHandler) updateOrder(ctx context.Context, dbConn *sql.DB, req *models.OrderUpdateRequest) (*models.Order, error) {
now := time.Now()
query := "UPDATE data_order SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
var item models.Order
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to update order: %w", err)
}
return &item, nil
}
func (h *OrderHandler) deleteOrder(ctx context.Context, dbConn *sql.DB, id string) error {
now := time.Now()
query := "UPDATE data_order SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'"
result, err := dbConn.ExecContext(ctx, query, id, now)
if err != nil {
return fmt.Errorf("failed to delete order: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func (h *OrderHandler) fetchOrders(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, limit, offset int) ([]models.Order, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_order WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
args = append(args, limit, offset)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch orders query failed: %w", err)
}
defer rows.Close()
items := make([]models.Order, 0, limit)
for rows.Next() {
var item models.Order
err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("scan Order failed: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return items, nil
}
func (h *OrderHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_order WHERE %s", whereClause)
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
return fmt.Errorf("total count query failed: %w", err)
}
return nil
}
func (h *OrderHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.OrderFilter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
}
whereClause, args := h.buildWhereClause(filter)
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_order WHERE %s GROUP BY status ORDER BY status", whereClause)
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
if err != nil {
return nil, fmt.Errorf("status query failed: %w", err)
}
defer rows.Close()
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
return nil, fmt.Errorf("status scan failed: %w", err)
}
aggregate.ByStatus[status] = count
switch status {
case "active":
aggregate.TotalActive = count
case "draft":
aggregate.TotalDraft = count
case "inactive":
aggregate.TotalInactive = count
}
}
return aggregate, nil
}
// Helper methods
func (h *OrderHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
log.Printf("[ERROR] %s: %v", message, err)
h.respondError(c, message, err, statusCode)
}
func (h *OrderHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{
Error: errorMessage,
Code: statusCode,
Message: err.Error(),
Timestamp: time.Now(),
})
}
func (h *OrderHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
limit := 10 // Default limit
offset := 0 // Default offset
if limitStr := c.Query("limit"); limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
}
if parsedLimit <= 0 {
return 0, 0, fmt.Errorf("limit must be greater than 0")
}
if parsedLimit > 100 {
return 0, 0, fmt.Errorf("limit cannot exceed 100")
}
limit = parsedLimit
}
if offsetStr := c.Query("offset"); offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
}
if parsedOffset < 0 {
return 0, 0, fmt.Errorf("offset cannot be negative")
}
offset = parsedOffset
}
return limit, offset, nil
}
func (h *OrderHandler) parseFilterParams(c *gin.Context) models.OrderFilter {
filter := models.OrderFilter{}
if status := c.Query("status"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
// Parse date filters
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
filter.DateFrom = &dateFrom
}
}
if dateToStr := c.Query("date_to"); dateToStr != "" {
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
filter.DateTo = &dateTo
}
}
return filter
}
func (h *OrderHandler) buildWhereClause(filter models.OrderFilter) (string, []interface{}) {
conditions := []string{"status != 'deleted'"}
args := []interface{}{}
paramCount := 1
if filter.Status != nil {
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
args = append(args, *filter.Status)
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
if filter.DateFrom != nil {
conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
args = append(args, *filter.DateFrom)
paramCount++
}
if filter.DateTo != nil {
conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
paramCount++
}
return strings.Join(conditions, " AND "), args
}
func (h *OrderHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages := 0
currentPage := 1
if limit > 0 {
totalPages = (total + limit - 1) / limit // Ceiling division
currentPage = (offset / limit) + 1
}
return models.MetaResponse{
Limit: limit,
Offset: offset,
Total: total,
TotalPages: totalPages,
CurrentPage: currentPage,
HasNext: offset+limit < total,
HasPrev: offset > 0,
}
}

View File

@@ -0,0 +1,683 @@
package handlers
import (
"api-service/internal/config"
"api-service/internal/database"
models "api-service/internal/models/product"
"context"
"database/sql"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"github.com/google/uuid"
)
var (
db database.Service
once sync.Once
validate *validator.Validate
)
// Initialize the database connection and validator
func init() {
once.Do(func() {
db = database.New(config.LoadConfig())
validate = validator.New()
// Register custom validations if needed
validate.RegisterValidation("product_status", validateProductStatus)
if db == nil {
log.Fatal("Failed to initialize database connection")
}
})
}
// Custom validation for product status
func validateProductStatus(fl validator.FieldLevel) bool {
return models.IsValidStatus(fl.Field().String())
}
// ProductHandler handles product services
type ProductHandler struct {
db database.Service
}
// NewProductHandler creates a new ProductHandler
func NewProductHandler() *ProductHandler {
return &ProductHandler{
db: db,
}
}
// GetProduct godoc
// @Summary Get product with pagination and optional aggregation
// @Description Returns a paginated list of products with optional summary statistics
// @Tags product
// @Accept json
// @Produce json
// @Param limit query int false "Limit (max 100)" default(10)
// @Param offset query int false "Offset" default(0)
// @Param include_summary query bool false "Include aggregation summary" default(false)
// @Param status query string false "Filter by status"
// @Param search query string false "Search in multiple fields"
// @Success 200 {object} models.ProductGetResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Bad request"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/products [get]
func (h *ProductHandler) GetProduct(c *gin.Context) {
// Parse pagination parameters
limit, offset, err := h.parsePaginationParams(c)
if err != nil {
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
return
}
// Parse filter parameters
filter := h.parseFilterParams(c)
includeAggregation := c.Query("include_summary") == "true"
// Get database connection
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
// Create context with timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
// Execute concurrent operations
var (
items []models.Product
total int
aggregateData *models.AggregateData
wg sync.WaitGroup
errChan = make(chan error, 3)
mu sync.Mutex
)
// Fetch total count
wg.Add(1)
go func() {
defer wg.Done()
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
mu.Lock()
errChan <- fmt.Errorf("failed to get total count: %w", err)
mu.Unlock()
}
}()
// Fetch main data - FIXED: Proper method name
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.fetchProducts(ctx, dbConn, filter, limit, offset)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to fetch data: %w", err)
} else {
items = result
}
mu.Unlock()
}()
// Fetch aggregation data if requested
if includeAggregation {
wg.Add(1)
go func() {
defer wg.Done()
result, err := h.getAggregateData(ctx, dbConn, filter)
mu.Lock()
if err != nil {
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
} else {
aggregateData = result
}
mu.Unlock()
}()
}
// Wait for all goroutines
wg.Wait()
close(errChan)
// Check for errors
for err := range errChan {
if err != nil {
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
return
}
}
// Build response
meta := h.calculateMeta(limit, offset, total)
response := models.ProductGetResponse{
Message: "Data product berhasil diambil",
Data: items,
Meta: meta,
}
if includeAggregation && aggregateData != nil {
response.Summary = aggregateData
}
c.JSON(http.StatusOK, response)
}
// GetProductByID godoc
// @Summary Get Product by ID
// @Description Returns a single product by ID
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID (UUID)"
// @Success 200 {object} models.ProductGetByIDResponse "Success response"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/product/{id} [get]
func (h *ProductHandler) GetProductByID(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.getProductByID(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Product not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to get product", err, http.StatusInternalServerError)
}
return
}
response := models.ProductGetByIDResponse{
Message: "Product details retrieved successfully",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// CreateProduct godoc
// @Summary Create product
// @Description Creates a new product record
// @Tags product
// @Accept json
// @Produce json
// @Param request body models.ProductCreateRequest true "Product creation request"
// @Success 201 {object} models.ProductCreateResponse "Product created successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/products [post]
func (h *ProductHandler) CreateProduct(c *gin.Context) {
var req models.ProductCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.createProduct(ctx, dbConn, &req)
if err != nil {
h.logAndRespondError(c, "Failed to create product", err, http.StatusInternalServerError)
return
}
response := models.ProductCreateResponse{
Message: "Product berhasil dibuat",
Data: item,
}
c.JSON(http.StatusCreated, response)
}
// UpdateProduct godoc
// @Summary Update product
// @Description Updates an existing product record
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID (UUID)"
// @Param request body models.ProductUpdateRequest true "Product update request"
// @Success 200 {object} models.ProductUpdateResponse "Product updated successfully"
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/product/{id} [put]
func (h *ProductHandler) UpdateProduct(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
var req models.ProductUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
return
}
// Set ID from path parameter
req.ID = id
// Validate request
if err := validate.Struct(&req); err != nil {
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
item, err := h.updateProduct(ctx, dbConn, &req)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Product not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to update product", err, http.StatusInternalServerError)
}
return
}
response := models.ProductUpdateResponse{
Message: "Product berhasil diperbarui",
Data: item,
}
c.JSON(http.StatusOK, response)
}
// DeleteProduct godoc
// @Summary Delete product
// @Description Soft deletes a product by setting status to 'deleted'
// @Tags product
// @Accept json
// @Produce json
// @Param id path string true "Product ID (UUID)"
// @Success 200 {object} models.ProductDeleteResponse "Product deleted successfully"
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
// @Failure 404 {object} models.ErrorResponse "Product not found"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/product/{id} [delete]
func (h *ProductHandler) DeleteProduct(c *gin.Context) {
id := c.Param("id")
// Validate UUID format
if _, err := uuid.Parse(id); err != nil {
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
return
}
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
err = h.deleteProduct(ctx, dbConn, id)
if err != nil {
if err == sql.ErrNoRows {
h.respondError(c, "Product not found", err, http.StatusNotFound)
} else {
h.logAndRespondError(c, "Failed to delete product", err, http.StatusInternalServerError)
}
return
}
response := models.ProductDeleteResponse{
Message: "Product berhasil dihapus",
ID: id,
}
c.JSON(http.StatusOK, response)
}
// GetProductStats godoc
// @Summary Get product statistics
// @Description Returns comprehensive statistics about product data
// @Tags product
// @Accept json
// @Produce json
// @Param status query string false "Filter statistics by status"
// @Success 200 {object} models.AggregateData "Statistics data"
// @Failure 500 {object} models.ErrorResponse "Internal server error"
// @Router /api/v1/products/stats [get]
func (h *ProductHandler) GetProductStats(c *gin.Context) {
dbConn, err := h.db.GetDB("satudata")
if err != nil {
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
return
}
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
defer cancel()
filter := h.parseFilterParams(c)
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
if err != nil {
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Statistik product berhasil diambil",
"data": aggregateData,
})
}
// Database operations
func (h *ProductHandler) getProductByID(ctx context.Context, dbConn *sql.DB, id string) (*models.Product, error) {
query := "SELECT id, status, date_created, date_updated, name FROM data_product WHERE id = $1 AND status != 'deleted'"
row := dbConn.QueryRowContext(ctx, query, id)
var item models.Product
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, err
}
return &item, nil
}
func (h *ProductHandler) createProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductCreateRequest) (*models.Product, error) {
id := uuid.New().String()
now := time.Now()
query := "INSERT INTO data_product (id, status, date_created, date_updated, name) VALUES ($1, $2, $3, $4, $5) RETURNING id, status, date_created, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, id, req.Status, now, now, req.Name)
var item models.Product
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to create product: %w", err)
}
return &item, nil
}
func (h *ProductHandler) updateProduct(ctx context.Context, dbConn *sql.DB, req *models.ProductUpdateRequest) (*models.Product, error) {
now := time.Now()
query := "UPDATE data_product SET status = $2, date_updated = $3, name = $4 WHERE id = $1 AND status != 'deleted' RETURNING id, status, date_created, date_updated, name"
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Status, now, req.Name)
var item models.Product
err := row.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("failed to update product: %w", err)
}
return &item, nil
}
func (h *ProductHandler) deleteProduct(ctx context.Context, dbConn *sql.DB, id string) error {
now := time.Now()
query := "UPDATE data_product SET status = 'deleted', date_updated = $2 WHERE id = $1 AND status != 'deleted'"
result, err := dbConn.ExecContext(ctx, query, id, now)
if err != nil {
return fmt.Errorf("failed to delete product: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get affected rows: %w", err)
}
if rowsAffected == 0 {
return sql.ErrNoRows
}
return nil
}
func (h *ProductHandler) fetchProducts(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, limit, offset int) ([]models.Product, error) {
whereClause, args := h.buildWhereClause(filter)
query := fmt.Sprintf("SELECT id, status, date_created, date_updated, name FROM data_product WHERE %s ORDER BY date_created DESC NULLS LAST LIMIT $%d OFFSET $%d", whereClause, len(args)+1, len(args)+2)
args = append(args, limit, offset)
rows, err := dbConn.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("fetch products query failed: %w", err)
}
defer rows.Close()
items := make([]models.Product, 0, limit)
for rows.Next() {
var item models.Product
err := rows.Scan(&item.ID, &item.Status, &item.DateCreated, &item.DateUpdated, &item.Name)
if err != nil {
return nil, fmt.Errorf("scan Product failed: %w", err)
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return items, nil
}
func (h *ProductHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter, total *int) error {
whereClause, args := h.buildWhereClause(filter)
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM data_product WHERE %s", whereClause)
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
return fmt.Errorf("total count query failed: %w", err)
}
return nil
}
func (h *ProductHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter models.ProductFilter) (*models.AggregateData, error) {
aggregate := &models.AggregateData{
ByStatus: make(map[string]int),
}
whereClause, args := h.buildWhereClause(filter)
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM data_product WHERE %s GROUP BY status ORDER BY status", whereClause)
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
if err != nil {
return nil, fmt.Errorf("status query failed: %w", err)
}
defer rows.Close()
for rows.Next() {
var status string
var count int
if err := rows.Scan(&status, &count); err != nil {
return nil, fmt.Errorf("status scan failed: %w", err)
}
aggregate.ByStatus[status] = count
switch status {
case "active":
aggregate.TotalActive = count
case "draft":
aggregate.TotalDraft = count
case "inactive":
aggregate.TotalInactive = count
}
}
return aggregate, nil
}
// Helper methods
func (h *ProductHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
log.Printf("[ERROR] %s: %v", message, err)
h.respondError(c, message, err, statusCode)
}
func (h *ProductHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
errorMessage := message
if gin.Mode() == gin.ReleaseMode {
errorMessage = "Internal server error"
}
c.JSON(statusCode, models.ErrorResponse{
Error: errorMessage,
Code: statusCode,
Message: err.Error(),
Timestamp: time.Now(),
})
}
func (h *ProductHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
limit := 10 // Default limit
offset := 0 // Default offset
if limitStr := c.Query("limit"); limitStr != "" {
parsedLimit, err := strconv.Atoi(limitStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
}
if parsedLimit <= 0 {
return 0, 0, fmt.Errorf("limit must be greater than 0")
}
if parsedLimit > 100 {
return 0, 0, fmt.Errorf("limit cannot exceed 100")
}
limit = parsedLimit
}
if offsetStr := c.Query("offset"); offsetStr != "" {
parsedOffset, err := strconv.Atoi(offsetStr)
if err != nil {
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
}
if parsedOffset < 0 {
return 0, 0, fmt.Errorf("offset cannot be negative")
}
offset = parsedOffset
}
return limit, offset, nil
}
func (h *ProductHandler) parseFilterParams(c *gin.Context) models.ProductFilter {
filter := models.ProductFilter{}
if status := c.Query("status"); status != "" {
if models.IsValidStatus(status) {
filter.Status = &status
}
}
if search := c.Query("search"); search != "" {
filter.Search = &search
}
// Parse date filters
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
filter.DateFrom = &dateFrom
}
}
if dateToStr := c.Query("date_to"); dateToStr != "" {
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
filter.DateTo = &dateTo
}
}
return filter
}
func (h *ProductHandler) buildWhereClause(filter models.ProductFilter) (string, []interface{}) {
conditions := []string{"status != 'deleted'"}
args := []interface{}{}
paramCount := 1
if filter.Status != nil {
conditions = append(conditions, fmt.Sprintf("status = $%d", paramCount))
args = append(args, *filter.Status)
paramCount++
}
if filter.Search != nil {
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
conditions = append(conditions, searchCondition)
searchTerm := "%" + *filter.Search + "%"
args = append(args, searchTerm)
paramCount++
}
if filter.DateFrom != nil {
conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
args = append(args, *filter.DateFrom)
paramCount++
}
if filter.DateTo != nil {
conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
paramCount++
}
return strings.Join(conditions, " AND "), args
}
func (h *ProductHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
totalPages := 0
currentPage := 1
if limit > 0 {
totalPages = (total + limit - 1) / limit // Ceiling division
currentPage = (offset / limit) + 1
}
return models.MetaResponse{
Limit: limit,
Offset: offset,
Total: total,
TotalPages: totalPages,
CurrentPage: currentPage,
HasNext: offset+limit < total,
HasPrev: offset > 0,
}
}

View File

@@ -0,0 +1,195 @@
package models
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"time"
)
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
type NullableInt32 struct {
Int32 int32 `json:"int32,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableInt32
func (n *NullableInt32) Scan(value interface{}) error {
var ni sql.NullInt32
if err := ni.Scan(value); err != nil {
return err
}
n.Int32 = ni.Int32
n.Valid = ni.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableInt32
func (n NullableInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Int32, nil
}
// Order represents the data structure for the order table
type Order struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
Name sql.NullString `json:"name,omitempty" db:"name"`
}
// Custom JSON marshaling for Order
func (r Order) MarshalJSON() ([]byte, error) {
type Alias Order
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Name *string `json:"name,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Name.Valid {
aux.Name = &r.Name.String
}
return json.Marshal(aux)
}
// Helper methods
func (r *Order) GetName() string {
if r.Name.Valid {
return r.Name.String
}
return ""
}
// Response struct for GET by ID
type OrderGetByIDResponse struct {
Message string `json:"message"`
Data *Order `json:"data"`
}
// Enhanced GET response with pagination and aggregation
type OrderGetResponse struct {
Message string `json:"message"`
Data []Order `json:"data"`
Meta MetaResponse `json:"meta"`
Summary *AggregateData `json:"summary,omitempty"`
}
// Request struct for create
type OrderCreateRequest struct {
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct for create
type OrderCreateResponse struct {
Message string `json:"message"`
Data *Order `json:"data"`
}
// Update request
type OrderUpdateRequest struct {
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct for update
type OrderUpdateResponse struct {
Message string `json:"message"`
Data *Order `json:"data"`
}
// Response struct for delete
type OrderDeleteResponse struct {
Message string `json:"message"`
ID string `json:"id"`
}
// Metadata for pagination
type MetaResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Aggregate data for summary
type AggregateData struct {
TotalActive int `json:"total_active"`
TotalDraft int `json:"total_draft"`
TotalInactive int `json:"total_inactive"`
ByStatus map[string]int `json:"by_status"`
LastUpdated *time.Time `json:"last_updated,omitempty"`
CreatedToday int `json:"created_today"`
UpdatedToday int `json:"updated_today"`
}
// Error response
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
// Filter struct for query parameters
type OrderFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
}
// Validation constants
const (
StatusDraft = "draft"
StatusActive = "active"
StatusInactive = "inactive"
StatusDeleted = "deleted"
)
// ValidStatuses for validation
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
// IsValidStatus helper function
func IsValidStatus(status string) bool {
for _, validStatus := range ValidStatuses {
if status == validStatus {
return true
}
}
return false
}

View File

@@ -0,0 +1,195 @@
package models
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"time"
)
// NullableInt32 is a custom type to replace sql.NullInt32 for swagger compatibility
type NullableInt32 struct {
Int32 int32 `json:"int32,omitempty"`
Valid bool `json:"valid"`
}
// Scan implements the sql.Scanner interface for NullableInt32
func (n *NullableInt32) Scan(value interface{}) error {
var ni sql.NullInt32
if err := ni.Scan(value); err != nil {
return err
}
n.Int32 = ni.Int32
n.Valid = ni.Valid
return nil
}
// Value implements the driver.Valuer interface for NullableInt32
func (n NullableInt32) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Int32, nil
}
// Product represents the data structure for the product table
type Product struct {
ID string `json:"id" db:"id"`
Status string `json:"status" db:"status"`
Sort NullableInt32 `json:"sort,omitempty" db:"sort"`
UserCreated sql.NullString `json:"user_created,omitempty" db:"user_created"`
DateCreated sql.NullTime `json:"date_created,omitempty" db:"date_created"`
UserUpdated sql.NullString `json:"user_updated,omitempty" db:"user_updated"`
DateUpdated sql.NullTime `json:"date_updated,omitempty" db:"date_updated"`
Name sql.NullString `json:"name,omitempty" db:"name"`
}
// Custom JSON marshaling for Product
func (r Product) MarshalJSON() ([]byte, error) {
type Alias Product
aux := &struct {
Sort *int `json:"sort,omitempty"`
UserCreated *string `json:"user_created,omitempty"`
DateCreated *time.Time `json:"date_created,omitempty"`
UserUpdated *string `json:"user_updated,omitempty"`
DateUpdated *time.Time `json:"date_updated,omitempty"`
Name *string `json:"name,omitempty"`
*Alias
}{
Alias: (*Alias)(&r),
}
if r.Sort.Valid {
sort := int(r.Sort.Int32)
aux.Sort = &sort
}
if r.UserCreated.Valid {
aux.UserCreated = &r.UserCreated.String
}
if r.DateCreated.Valid {
aux.DateCreated = &r.DateCreated.Time
}
if r.UserUpdated.Valid {
aux.UserUpdated = &r.UserUpdated.String
}
if r.DateUpdated.Valid {
aux.DateUpdated = &r.DateUpdated.Time
}
if r.Name.Valid {
aux.Name = &r.Name.String
}
return json.Marshal(aux)
}
// Helper methods
func (r *Product) GetName() string {
if r.Name.Valid {
return r.Name.String
}
return ""
}
// Response struct for GET by ID
type ProductGetByIDResponse struct {
Message string `json:"message"`
Data *Product `json:"data"`
}
// Enhanced GET response with pagination and aggregation
type ProductGetResponse struct {
Message string `json:"message"`
Data []Product `json:"data"`
Meta MetaResponse `json:"meta"`
Summary *AggregateData `json:"summary,omitempty"`
}
// Request struct for create
type ProductCreateRequest struct {
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct for create
type ProductCreateResponse struct {
Message string `json:"message"`
Data *Product `json:"data"`
}
// Update request
type ProductUpdateRequest struct {
ID string `json:"-" validate:"required,uuid4"` // ID dari URL path
Status string `json:"status" validate:"required,oneof=draft active inactive"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
}
// Response struct for update
type ProductUpdateResponse struct {
Message string `json:"message"`
Data *Product `json:"data"`
}
// Response struct for delete
type ProductDeleteResponse struct {
Message string `json:"message"`
ID string `json:"id"`
}
// Metadata for pagination
type MetaResponse struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
CurrentPage int `json:"current_page"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
// Aggregate data for summary
type AggregateData struct {
TotalActive int `json:"total_active"`
TotalDraft int `json:"total_draft"`
TotalInactive int `json:"total_inactive"`
ByStatus map[string]int `json:"by_status"`
LastUpdated *time.Time `json:"last_updated,omitempty"`
CreatedToday int `json:"created_today"`
UpdatedToday int `json:"updated_today"`
}
// Error response
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
// Filter struct for query parameters
type ProductFilter struct {
Status *string `json:"status,omitempty" form:"status"`
Search *string `json:"search,omitempty" form:"search"`
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
}
// Validation constants
const (
StatusDraft = "draft"
StatusActive = "active"
StatusInactive = "inactive"
StatusDeleted = "deleted"
)
// ValidStatuses for validation
var ValidStatuses = []string{StatusDraft, StatusActive, StatusInactive}
// IsValidStatus helper function
func IsValidStatus(status string) bool {
for _, validStatus := range ValidStatuses {
if status == validStatus {
return true
}
}
return false
}

View File

@@ -1,6 +1,7 @@
package v1
import (
orderHandlers "api-service/internal/handlers/order"
retribusiHandlers "api-service/internal/handlers/retribusi"
"net/http"
@@ -59,6 +60,15 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
v1.DELETE("/retribusi/:id", retribusiHandler.DeleteRetribusi)
// Protected routes (require authentication)
// Order endpoints
orderHandler := orderHandlers.NewOrderHandler()
v1.GET("/orders", orderHandler.GetOrder)
v1.GET("/order/:id", orderHandler.GetOrderByID)
v1.POST("/orders", orderHandler.CreateOrder)
v1.PUT("/order/:id", orderHandler.UpdateOrder)
v1.DELETE("/order/:id", orderHandler.DeleteOrder)
protected := v1.Group("/")
protected.Use(middleware.JWTAuthMiddleware(authService))
{

View File

@@ -1,141 +0,0 @@
package product
import (
"context"
"errors"
"time"
model "api-service/internal/models/product"
"api-service/internal/repository/product"
)
// Service defines the interface for product business logic
type Service interface {
CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error)
GetProduct(ctx context.Context, id string) (*model.ProductResponse, error)
GetAllProducts(ctx context.Context) (*model.ProductsResponse, error)
UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error)
DeleteProduct(ctx context.Context, id string) error
}
// service implements the Service interface
type service struct {
repo product.Repository
}
// NewService creates a new product service
func NewService(repo product.Repository) Service {
return &service{repo: repo}
}
// CreateProduct creates a new product
func (s *service) CreateProduct(ctx context.Context, req *model.ProductCreateRequest) (*model.ProductResponse, error) {
if req.Name == "" {
return nil, errors.New("product name is required")
}
if req.Price <= 0 {
return nil, errors.New("product price must be greater than 0")
}
product := &model.Product{
ID: generateID(),
Name: req.Name,
Description: req.Description,
Price: req.Price,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := s.repo.Create(ctx, product)
if err != nil {
return nil, err
}
return &model.ProductResponse{
ID: product.ID,
Name: product.Name,
Description: product.Description,
Price: product.Price,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}, nil
}
// GetProduct retrieves a product by ID
func (s *service) GetProduct(ctx context.Context, id string) (*model.ProductResponse, error) {
if id == "" {
return nil, errors.New("product ID is required")
}
product, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return &model.ProductResponse{
ID: product.ID,
Name: product.Name,
Description: product.Description,
Price: product.Price,
CreatedAt: product.CreatedAt,
UpdatedAt: product.UpdatedAt,
}, nil
}
// GetAllProducts retrieves all products
func (s *service) GetAllProducts(ctx context.Context) (*model.ProductsResponse, error) {
products, err := s.repo.GetAll(ctx)
if err != nil {
return nil, err
}
return &model.ProductsResponse{
Data: products,
}, nil
}
// UpdateProduct updates an existing product
func (s *service) UpdateProduct(ctx context.Context, id string, req *model.ProductUpdateRequest) (*model.ProductResponse, error) {
if id == "" {
return nil, errors.New("product ID is required")
}
existingProduct, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, err
}
existingProduct.Name = req.Name
existingProduct.Description = req.Description
existingProduct.Price = req.Price
existingProduct.UpdatedAt = time.Now()
err = s.repo.Update(ctx, existingProduct)
if err != nil {
return nil, err
}
return &model.ProductResponse{
ID: existingProduct.ID,
Name: existingProduct.Name,
Description: existingProduct.Description,
Price: existingProduct.Price,
CreatedAt: existingProduct.CreatedAt,
UpdatedAt: existingProduct.UpdatedAt,
}, nil
}
// DeleteProduct deletes a product
func (s *service) DeleteProduct(ctx context.Context, id string) error {
if id == "" {
return errors.New("product ID is required")
}
return s.repo.Delete(ctx, id)
}
// Helper functions
func generateID() string {
return "prod_" + time.Now().Format("20060102150405")
}