Creat Service BPJS
This commit is contained in:
92
internal/handlers/bpjs/peserta.go
Normal file
92
internal/handlers/bpjs/peserta.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
services "api-service/internal/services/bpjs"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PesertaHandler handles BPJS participant operations
|
||||
type PesertaHandler struct {
|
||||
bpjsService services.VClaimService
|
||||
}
|
||||
|
||||
// NewPesertaHandler creates a new PesertaHandler instance
|
||||
func NewPesertaHandler(cfg config.BpjsConfig) *PesertaHandler {
|
||||
return &PesertaHandler{
|
||||
bpjsService: services.NewService(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
// GetPesertaByNIK godoc
|
||||
// @Summary Get participant data by NIK
|
||||
// @Description Search participant data based on Population NIK and service date
|
||||
// @Tags bpjs
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param nik path string true "NIK KTP"
|
||||
// @Param tglSEP path string true "Service date/SEP date (format: yyyy-MM-dd)"
|
||||
// @Success 200 {object} map[string]interface{} "Participant data"
|
||||
// @Failure 400 {object} map[string]interface{} "Bad request"
|
||||
// @Failure 404 {object} map[string]interface{} "Participant not found"
|
||||
// @Failure 500 {object} map[string]interface{} "Internal server error"
|
||||
// @Router /api/v1/bpjs/Peserta/nik/{nik}/tglSEP/{tglSEP} [get]
|
||||
func (h *PesertaHandler) GetPesertaByNIK(c *gin.Context) {
|
||||
nik := c.Param("nik")
|
||||
tglSEP := c.Param("tglSEP")
|
||||
|
||||
// Validate parameters
|
||||
if nik == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "NIK parameter is required",
|
||||
"message": "NIK KTP tidak boleh kosong",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if tglSEP == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "tglSEP parameter is required",
|
||||
"message": "Tanggal SEP tidak boleh kosong",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate date format
|
||||
if _, err := time.Parse("2006-01-02", tglSEP); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Invalid date format",
|
||||
"message": "Format tanggal harus yyyy-MM-dd",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Build endpoint URL
|
||||
endpoint := fmt.Sprintf("/Peserta/nik/%s/tglSEP/%s", nik, tglSEP)
|
||||
|
||||
// Call BPJS service
|
||||
var result map[string]interface{}
|
||||
if err := h.bpjsService.Get(ctx, endpoint, &result); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to fetch participant data",
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Return successful response
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Data peserta berhasil diambil",
|
||||
"data": result,
|
||||
})
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user