Merge pull request #6 from pahmiudahgede/api_v3

Api v3
This commit is contained in:
Fahmi Kurniawan 2025-07-06 22:04:25 +07:00 committed by GitHub
commit 6d6223d13f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
180 changed files with 16742 additions and 6175 deletions

71
.dockerignore Normal file
View File

@ -0,0 +1,71 @@
# Git
.git
.gitignore
README.md
.gitattributes
# Documentation
*.md
docs/
# Environment files (kecuali yang diperlukan)
.env
.env.local
.env.example
# Kita tetap include .env.dev dan .env.docker untuk development
# Logs
*.log
logs/
# Dependencies
vendor/
# Test files
*_test.go
testdata/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Temporary files
*.tmp
*.temp
tmp/
# Build artifacts (untuk production)
main
*.exe
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Air specific
.air.toml
tmp/
*_templ.go
# Coverage
*.out
coverage.html
# Database
*.db
*.sqlite
*.sqlite3
# Public uploads (jika ada)
public/uploads/
# Makefile
Makefile

View File

@ -1,3 +1,6 @@
#BASE URL
BASE_URL=
# SERVER SETTINGS
SERVER_HOST=
SERVER_PORT=
@ -20,3 +23,8 @@ API_KEY=
#SECRET_KEY
SECRET_KEY=
# TTL
ACCESS_TOKEN_EXPIRY=
REFRESH_TOKEN_EXPIRY=
PARTIAL_TOKEN_EXPIRY=

65
.gitignore vendored
View File

@ -1,6 +1,3 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
@ -14,19 +11,61 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Dependency directories
vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
.env.prod
.env.dev
# Environment files - ignore all variations
.env*
!.env.example
# Ignore avatar images
/public/uploads/avatars/
/public/uploads/articles/
/public/uploads/banners/
# Logs
*.log
logs/
# Temporary files
tmp/
*.tmp
*.temp
# IDE/Editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Air live reload tool
.air.toml
# Public uploads - user generated content
/public/apirijig/v2/uploads/
# Build outputs
/bin/
/build/
/dist/
# Coverage reports
coverage.txt
coverage.html
*.cover
# Debug files
debug
*.pprof
# Local development files
*.local

49
Dockerfile.dev Normal file
View File

@ -0,0 +1,49 @@
# Multi-stage Dockerfile untuk development dengan Air hot reload
FROM golang:1.23-alpine AS base
# Install dependencies yang diperlukan
RUN apk add --no-cache \
git \
ca-certificates \
curl \
tzdata \
make \
gcc \
musl-dev
# Install Air untuk hot reload dengan versi yang stabil
RUN go install github.com/cosmtrek/air@v1.49.0
# Set timezone ke Asia/Jakarta
ENV TZ=Asia/Jakarta
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Set working directory
WORKDIR /app
# Create user untuk security (non-root)
RUN addgroup -g 1001 -S golang && \
adduser -S golang -u 1001 -G golang
# Copy go.mod dan go.sum terlebih dahulu untuk better caching
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download && go mod verify
# Copy source code
COPY . .
# Create tmp directory dengan permissions yang tepat
RUN mkdir -p tmp && \
chown -R golang:golang /app && \
chmod -R 755 /app
# Switch to non-root user
USER golang
# Expose port
EXPOSE 7000
# Command untuk menjalankan Air
CMD ["air", "-c", ".air.toml"]

151
Makefile Normal file
View File

@ -0,0 +1,151 @@
# Makefile untuk mengelola Docker commands - Optimized Version
.PHONY: help build up down restart logs clean dev prod dev-build dev-up dev-down dev-logs
# Color codes untuk output yang lebih menarik
GREEN := \033[0;32m
YELLOW := \033[1;33m
RED := \033[0;31m
BLUE := \033[0;34m
PURPLE := \033[0;35m
CYAN := \033[0;36m
NC := \033[0m # No Color
# Project variables
PROJECT_NAME := rijig_backend
DEV_COMPOSE_FILE := docker-compose.dev.yml
# Default target
help:
@echo "$(GREEN)🚀 $(PROJECT_NAME) - Available Commands:$(NC)"
@echo ""
@echo "$(YELLOW)📦 Development Commands (Hot Reload):$(NC)"
@echo " $(CYAN)dev$(NC) - Complete development setup (build + up)"
@echo " $(CYAN)dev-build$(NC) - Build development images"
@echo " $(CYAN)dev-up$(NC) - Start development environment"
@echo " $(CYAN)dev-down$(NC) - Stop development environment"
@echo " $(CYAN)dev-restart$(NC) - Restart development services"
@echo " $(CYAN)dev-logs$(NC) - Show development logs (all services)"
@echo " $(CYAN)dev-clean$(NC) - Clean development environment"
@echo ""
@echo "$(YELLOW)🛠️ Development Utilities:$(NC)"
@echo " $(CYAN)dev-app-logs$(NC) - Show only app logs"
@echo " $(CYAN)dev-db-logs$(NC) - Show only database logs"
@echo " $(CYAN)dev-shell$(NC) - Access app container shell"
@echo " $(CYAN)dev-status$(NC) - Check development services status"
@echo " $(CYAN)psql$(NC) - Connect to development PostgreSQL"
@echo " $(CYAN)redis-cli$(NC) - Connect to development Redis"
@echo ""
@echo "$(YELLOW)🧹 Maintenance:$(NC)"
@echo " $(RED)clean-all$(NC) - Clean everything (containers, volumes, images)"
@echo " $(RED)system-prune$(NC) - Clean Docker system"
@echo " $(CYAN)stats$(NC) - Show container resource usage"
# ======================
# DEVELOPMENT COMMANDS
# ======================
# Quick development setup (recommended)
dev: dev-build dev-up
@echo "$(GREEN)✨ Development environment ready!$(NC)"
@echo "$(BLUE)🌐 Services:$(NC)"
@echo " • API Server: $(CYAN)http://localhost:7000$(NC)"
@echo " • PostgreSQL: $(CYAN)localhost:5433$(NC)"
@echo " • Redis: $(CYAN)localhost:6378$(NC)"
@echo " • pgAdmin: $(CYAN)http://localhost:8080$(NC) (admin@rijig.com / admin123)"
@echo " • Redis Commander: $(CYAN)http://localhost:8081$(NC)"
@echo ""
@echo "$(GREEN)🔥 Hot reload is active! Edit your Go files and see changes automatically$(NC)"
dev-build:
@echo "$(YELLOW)🔨 Building development images...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) build --no-cache
@echo "$(GREEN)✅ Development images built successfully!$(NC)"
dev-up:
@echo "$(YELLOW)🚀 Starting development services...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) up -d
@echo "$(GREEN)✅ Development services started!$(NC)"
@make dev-status
dev-down:
@echo "$(RED)🛑 Stopping development services...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) down
@echo "$(GREEN)✅ Development services stopped!$(NC)"
dev-restart:
@echo "$(YELLOW)🔄 Restarting development services...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) restart
@echo "$(GREEN)✅ Development services restarted!$(NC)"
dev-logs:
@echo "$(CYAN)📋 Showing development logs (Ctrl+C to exit)...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=100
dev-clean:
@echo "$(RED)🧹 Cleaning development environment...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans
@echo "$(GREEN)✅ Development environment cleaned!$(NC)"
# ======================
# DEVELOPMENT UTILITIES
# ======================
dev-app-logs:
@echo "$(CYAN)📋 Showing app logs (Ctrl+C to exit)...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 app
dev-db-logs:
@echo "$(CYAN)📋 Showing database logs (Ctrl+C to exit)...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) logs -f --tail=50 postgres
dev-shell:
@echo "$(CYAN)🐚 Accessing app container shell...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) exec app sh
dev-status:
@echo "$(BLUE)📊 Development services status:$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) ps
psql:
@echo "$(CYAN)🐘 Connecting to development PostgreSQL...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) exec postgres psql -U postgres -d apirijig_v2
redis-cli:
@echo "$(CYAN)⚡ Connecting to development Redis...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) exec redis redis-cli
# ======================
# MAINTENANCE COMMANDS
# ======================
clean-all:
@echo "$(RED)🧹 Performing complete cleanup...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) down -v --remove-orphans 2>/dev/null || true
@echo "$(YELLOW)🗑️ Removing unused containers, networks, and images...$(NC)"
@docker system prune -a -f --volumes
@echo "$(GREEN)✅ Complete cleanup finished!$(NC)"
system-prune:
@echo "$(YELLOW)🗑️ Cleaning Docker system...$(NC)"
@docker system prune -f
@echo "$(GREEN)✅ Docker system cleaned!$(NC)"
stats:
@echo "$(BLUE)📈 Container resource usage:$(NC)"
@docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}\t{{.BlockIO}}"
# ======================
# QUICK COMMANDS
# ======================
# App only restart (faster for development)
app-restart:
@echo "$(YELLOW)🔄 Restarting app container only...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) restart app
@echo "$(GREEN)✅ App container restarted!$(NC)"
# Check if containers are healthy
health-check:
@echo "$(BLUE)🏥 Checking container health...$(NC)"
@docker compose -f $(DEV_COMPOSE_FILE) ps --format "table {{.Name}}\t{{.Status}}"

260
README.md
View File

@ -1,2 +1,260 @@
# build_api_golang
this is rest API using go lang and go fiber with postgreql database for my personal project.
# 🗂️ Waste Management System API
> **RESTful API untuk sistem pengelolaan sampah terintegrasi yang menghubungkan masyarakat, pengepul, dan pengelola dalam satu ekosistem digital.**
[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=for-the-badge&logo=go)](https://golang.org/)
[![Fiber](https://img.shields.io/badge/Fiber-v2.52+-00ADD8?style=for-the-badge&logo=go)](https://gofiber.io/)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-316192?style=for-the-badge&logo=postgresql)](https://www.postgresql.org/)
[![Redis](https://img.shields.io/badge/Redis-7.0+-DC382D?style=for-the-badge&logo=redis)](https://redis.io/)
[![Docker](https://img.shields.io/badge/Docker-24.0+-2496ED?style=for-the-badge&logo=docker)](https://www.docker.com/)
[![GORM](https://img.shields.io/badge/GORM-Latest-00ADD8?style=for-the-badge)](https://gorm.io/)
## 📋 Deskripsi Aplikasi
Waste Management System adalah backend API yang dikembangkan untuk mendigitalisasi sistem pengelolaan sampah di Indonesia. Aplikasi ini menghubungkan tiga stakeholder utama dalam rantai pengelolaan sampah melalui platform terintegrasi yang efisien dan transparan.
### 🎯 Latar Belakang Masalah
Indonesia menghadapi krisis pengelolaan sampah dengan berbagai tantangan:
- **Volume Sampah Tinggi**: 67.8 juta ton sampah yang dihasilkan per tahun
- **Koordinasi Lemah**: Minimnya sinergi antar stakeholder pengelolaan sampah
- **Tracking Tidak Optimal**: Kurangnya visibility dalam proses pengelolaan sampah
- **Partisipasi Rendah**: Minimnya engagement masyarakat dalam program daur ulang
- **Inefisiensi Operasional**: Proses manual yang memakan waktu dan biaya tinggi
### 💡 Solusi yang Ditawarkan
Platform digital komprehensif yang menyediakan:
- **Koordinasi Terintegrasi**: Menghubungkan seluruh stakeholder dalam satu platform
- **Tracking System**: Pelacakan sampah dari sumber hingga pengolahan akhir
- **Optimasi Proses**: Automasi dan optimasi rute pengumpulan
- **Engagement Platform**: Sistem gamifikasi untuk meningkatkan partisipasi
- **Data-Driven Insights**: Analytics untuk pengambilan keputusan berbasis data
## 👥 Stakeholder Sistem
### 🏠 **Masyarakat (Citizens)**
*Pengguna akhir yang menghasilkan sampah rumah tangga*
**Peran dalam Sistem:**
- Melaporkan jenis dan volume sampah yang dihasilkan
- Mengakses informasi jadwal pengumpulan sampah
- Menerima edukasi tentang pemilahan sampah yang benar
- Berpartisipasi dalam program reward dan gamifikasi
- Melacak kontribusi personal terhadap lingkungan
**Manfaat yang Diperoleh:**
- Kemudahan dalam melaporkan sampah
- Reward dan insentif dari partisipasi aktif
- Edukasi lingkungan yang berkelanjutan
- Transparansi dalam proses pengelolaan sampah
### ♻️ **Pengepul (Collectors)**
*Pelaku usaha yang mengumpulkan dan mendistribusikan sampah*
**Peran dalam Sistem:**
- Mengelola rute dan jadwal pengumpulan sampah optimal
- Memvalidasi dan menimbang sampah yang dikumpulkan
- Melakukan pemilahan awal berdasarkan kategori sampah
- Mengatur distribusi sampah ke berbagai pengelola
- Melaporkan volume dan jenis sampah yang berhasil dikumpulkan
**Manfaat yang Diperoleh:**
- Optimasi rute untuk efisiensi operasional
- System tracking untuk akuntabilitas
- Platform untuk memperluas jangkauan bisnis
- Data analytics untuk business intelligence
### 🏭 **Pengelola (Processors)**
*Institusi atau perusahaan pengolahan akhir sampah*
**Peran dalam Sistem:**
- Mengelola fasilitas pengolahan sampah
- Memproses sampah menjadi produk daur ulang bernilai
- Melaporkan hasil pengolahan dan dampak lingkungan
- Memberikan feedback ke pengepul dan masyarakat
- Mengelola sistem pembayaran dan insentif
**Manfaat yang Diperoleh:**
- Supply chain management yang terorganisir
- Traceability sampah untuk quality control
- Data untuk compliance dan sustainability reporting
- Platform untuk program CSR dan community engagement
## ✨ Fitur Unggulan
### 🔄 **End-to-End Waste Tracking**
Sistem pelacakan komprehensif yang memungkinkan monitoring sampah dari sumber hingga pengolahan akhir, memberikan transparansi penuh dalam setiap tahap proses.
### 📊 **Real-time Analytics Dashboard**
Interface dashboard yang menampilkan data statistik, trend analysis, dan key performance indicators dengan visualisasi yang mudah dipahami semua stakeholder.
### 🗺️ **Geographic Information System**
Sistem pemetaan cerdas untuk optimasi rute pengumpulan, identifikasi titik pengumpulan strategis, dan monitoring coverage area secara real-time.
### 🎁 **Gamification & Reward System**
Program insentif untuk mendorong partisipasi aktif masyarakat melalui sistem poin, achievement badges, leaderboard, dan berbagai reward menarik.
### 🔔 **Smart Notification System**
Sistem notifikasi multi-channel yang memberikan informasi real-time tentang jadwal pengumpulan, status sampah, achievement unlock, dan update penting lainnya.
### 📈 **Comprehensive Reporting**
Modul pelaporan dengan kemampuan generate report otomatis, export dalam berbagai format, dan customizable dashboard untuk setiap role pengguna.
## 🛠️ Tech Stack & Architecture
### **Backend Development**
#### **🚀 Golang (Go)**
*Primary Backend Language*
**Mengapa Memilih Golang:**
- **Performance Excellence**: Compiled language dengan execution speed yang sangat tinggi
- **Concurrency Native**: Goroutines dan channels untuk handle ribuan concurrent requests
- **Memory Efficiency**: Garbage collector yang optimal dengan memory footprint rendah
- **Scalability Ready**: Mampu handle high-traffic dengan minimal resource consumption
- **Simple yet Powerful**: Syntax yang clean namun feature-rich untuk rapid development
**Keunggulan untuk Waste Management System:**
- Mampu menangani concurrent requests dari multiple stakeholders secara simultan
- Processing real-time data tracking dengan performa tinggi
- Ideal untuk microservices architecture dan distributed systems
- Strong typing system untuk data integrity dalam financial transactions
#### **⚡ Fiber Framework**
*High-Performance Web Framework*
**Mengapa Memilih Fiber:**
- **Speed Optimized**: Salah satu framework tercepat untuk Go dengan minimal overhead
- **Memory Efficient**: Extremely low memory usage bahkan pada high load
- **Express-like API**: Familiar syntax bagi developer dengan background Node.js/Express
- **Rich Middleware Ecosystem**: Built-in middleware untuk authentication, CORS, logging, rate limiting
- **Zero Allocation**: Optimized untuk minimize memory allocation
**Keunggulan untuk Waste Management System:**
- RESTful API development yang rapid dan efficient
- Middleware ecosystem yang mendukung complex business logic requirements
- Auto-recovery dan error handling untuk system reliability
- Built-in JSON serialization yang optimal untuk mobile app integration
### **Database & Data Management**
#### **🐘 PostgreSQL**
*Advanced Relational Database Management System*
**Mengapa Memilih PostgreSQL:**
- **ACID Compliance**: Full transactional integrity untuk financial dan tracking data
- **Advanced Data Types**: JSON, Array, Geographic data types untuk flexible schema
- **Geospatial Support**: PostGIS extension untuk location-based features
- **Full-Text Search**: Built-in search capabilities untuk content discovery
- **Scalability Options**: Horizontal dan vertical scaling dengan replication support
**Keunggulan untuk Waste Management System:**
- Geospatial data support untuk location tracking dan route optimization
- JSON storage untuk flexible metadata dan dynamic content
- Complex relationship handling untuk multi-stakeholder interactions
- Data consistency untuk transaction processing dan reward calculations
#### **🔧 GORM (Go ORM)**
*Developer-Friendly Object-Relational Mapping*
**Mengapa Memilih GORM:**
- **Auto Migration**: Automatic database schema migration dan versioning
- **Association Handling**: Powerful relationship management dengan lazy/eager loading
- **Hook System**: Lifecycle events untuk implement business rules
- **Query Builder**: Type-safe dan flexible query construction
- **Database Agnostic**: Support multiple database dengan same codebase
**Keunggulan untuk Waste Management System:**
- Model relationship yang complex untuk stakeholder interactions
- Data validation dan business rules enforcement di ORM level
- Performance optimization dengan intelligent query generation
- Schema evolution yang safe untuk production deployments
#### **⚡ Redis**
*In-Memory Data Structure Store*
**Mengapa Memilih Redis:**
- **Ultra-High Performance**: Sub-millisecond response times untuk real-time features
- **Rich Data Structures**: Strings, Hashes, Lists, Sets, Sorted Sets, Streams
- **Pub/Sub Messaging**: Real-time communication untuk notification system
- **Persistence Options**: Data durability dengan configurable persistence
- **Clustering Support**: Horizontal scaling dengan Redis Cluster
**Keunggulan untuk Waste Management System:**
- Session management untuk multi-role authentication system
- Real-time notifications dan messaging antar stakeholders
- Caching layer untuk frequently accessed data (routes, user profiles)
- Rate limiting untuk API protection dan fair usage
- Leaderboard dan ranking system untuk gamification features
### **Infrastructure & Deployment**
#### **🐳 Docker**
*Application Containerization Platform*
**Mengapa Memilih Docker:**
- **Environment Consistency**: Identical environment dari development hingga production
- **Scalability Ready**: Easy horizontal scaling dengan container orchestration
- **Resource Efficiency**: Lightweight containers dibanding traditional virtual machines
- **Deployment Simplicity**: One-command deployment dengan reproducible builds
- **Microservices Architecture**: Perfect untuk distributed system deployment
**Keunggulan untuk Waste Management System:**
- Development environment yang consistent untuk seluruh tim developer
- Production deployment yang reliable dan reproducible
- Easy scaling berdasarkan load dari multiple stakeholders
- Integration yang seamless dengan CI/CD pipeline
- Service isolation untuk better security dan debugging
## 🏗️ System Architecture
### **Layered Architecture Pattern**
```
┌─────────────────────────────────────┐
│ Presentation Layer │
│ (Mobile Apps, Web Dashboard) │
└─────────────────┬───────────────────┘
│ RESTful API
┌─────────────────▼───────────────────┐
│ API Gateway Layer │
│ (Fiber + Middleware) │
└─────────────────┬───────────────────┘
┌─────────────────▼───────────────────┐
│ Business Logic Layer │
│ (Service Components) │
└─────────────────┬───────────────────┘
┌─────────────────▼───────────────────┐
│ Data Access Layer │
│ (Repository Pattern + GORM) │
└─────────────────┬───────────────────┘
┌─────────────────▼───────────────────┐
│ Persistence Layer │
│ (PostgreSQL + Redis) │
└─────────────────────────────────────┘
```
### **Key Architectural Principles**
- **Separation of Concerns**: Clear separation antara business logic, data access, dan presentation
- **Dependency Injection**: Loose coupling antar components untuk better testability
- **Repository Pattern**: Abstraction layer untuk data access operations
- **Middleware Pattern**: Cross-cutting concerns seperti authentication, logging, validation
- **Event-Driven Architecture**: Pub/sub pattern untuk real-time notifications
---
<div align="center">
**Waste Management System** menggunakan cutting-edge technology stack untuk menciptakan solusi digital yang sustainable, scalable, dan user-centric dalam pengelolaan sampah di Indonesia.
🌱 **Built for Sustainability • Designed for Scale • Engineered for Impact** 🌱
</div>

View File

@ -1,17 +1,54 @@
package main
import (
"log"
"rijig/config"
"rijig/internal/cart"
"rijig/internal/trash"
"rijig/internal/worker"
"time"
// "rijig/internal/repositories"
// "rijig/internal/services"
"rijig/router"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/config"
"github.com/pahmiudahgede/senggoldong/router"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
config.SetupConfig()
cartRepo := cart.NewCartRepository()
trashRepo := trash.NewTrashRepository(config.DB)
cartService := cart.NewCartService(cartRepo, trashRepo)
worker := worker.NewCartWorker(cartService, cartRepo, trashRepo)
app := fiber.New()
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
if err := worker.AutoCommitExpiringCarts(); err != nil {
log.Printf("Auto-commit error: %v", err)
}
}
}()
app := fiber.New(fiber.Config{
ErrorHandler: func(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
if e, ok := err.(*fiber.Error); ok {
code = e.Code
}
return c.Status(code).JSON(fiber.Map{
"error": err.Error(),
})
},
})
app.Use(cors.New())
router.SetupRoutes(app)
config.StartServer(app)
}

View File

@ -5,7 +5,6 @@ import (
"log"
"os"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
@ -13,7 +12,6 @@ import (
var DB *gorm.DB
func ConnectDatabase() {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable",
os.Getenv("DB_HOST"),
@ -30,28 +28,7 @@ func ConnectDatabase() {
}
log.Println("Database connected successfully!")
err = DB.AutoMigrate(
// ==wilayah indonesia==
&model.Province{},
&model.Regency{},
&model.District{},
&model.Village{},
// ==wilayah indonesia==
// ==main feature==
&model.User{},
&model.Role{},
&model.UserPin{},
&model.Address{},
&model.Article{},
&model.Banner{},
&model.InitialCoint{},
&model.TrashCategory{},
&model.TrashDetail{},
// ==main feature==
)
if err != nil {
if err := RunMigrations(DB); err != nil {
log.Fatalf("Error performing auto-migration: %v", err)
}
log.Println("Database migrated successfully!")
}

66
config/migration.go Normal file
View File

@ -0,0 +1,66 @@
package config
import (
"log"
"rijig/model"
"gorm.io/gorm"
)
func RunMigrations(db *gorm.DB) error {
log.Println("Starting database migration...")
err := db.AutoMigrate(
// Location models
&model.Province{},
&model.Regency{},
&model.District{},
&model.Village{},
// User related models
&model.User{},
&model.Collector{},
&model.AvaibleTrashByCollector{},
&model.Role{},
&model.UserPin{},
&model.Address{},
&model.IdentityCard{},
&model.CompanyProfile{},
// Pickup related models
&model.RequestPickup{},
&model.RequestPickupItem{},
&model.PickupStatusHistory{},
&model.PickupRating{},
// Cart related models
&model.Cart{},
&model.CartItem{},
// Store related models
&model.Store{},
&model.Product{},
&model.ProductImage{},
// Content models
&model.Article{},
&model.Banner{},
&model.InitialCoint{},
&model.About{},
&model.AboutDetail{},
&model.CoverageArea{},
// Trash related models
&model.TrashCategory{},
&model.TrashDetail{},
)
if err != nil {
log.Printf("Error performing auto-migration: %v", err)
return err
}
log.Println("Database migrated successfully!")
return nil
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"strconv"
"github.com/go-redis/redis/v8"
)
@ -13,13 +14,21 @@ var RedisClient *redis.Client
var Ctx = context.Background()
func ConnectRedis() {
redisDBStr := os.Getenv("REDIS_DB")
redisDB, err := strconv.Atoi(redisDBStr)
if err != nil {
log.Fatalf("Error converting REDIS_DB to integer: %v", err)
}
RedisClient = redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%s", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")),
Password: os.Getenv("REDIS_PASSWORD"),
DB: 0,
DB: redisDB,
})
_, err := RedisClient.Ping(Ctx).Result()
_, err = RedisClient.Ping(Ctx).Result()
if err != nil {
log.Fatalf("Error connecting to Redis: %v", err)
}

View File

@ -8,14 +8,18 @@ import (
"github.com/gofiber/fiber/v2"
)
func GetSecretKey() string {
return os.Getenv("SECRET_KEY")
}
func StartServer(app *fiber.App) {
host := os.Getenv("SERVER_HOST")
port := os.Getenv("SERVER_PORT")
address := fmt.Sprintf("%s:%s", host, port)
log.Printf("Server is running on http://%s", address)
log.Printf("server berjalan di http://%s", address)
if err := app.Listen(address); err != nil {
log.Fatalf("Error starting server: %v", err)
log.Fatalf("gagal saat menjalankan server: %v", err)
}
}

View File

@ -1,17 +1,27 @@
package config
package config
import (
"log"
"os"
"github.com/joho/godotenv"
)
func SetupConfig() {
err := godotenv.Load(".env.dev")
if err != nil {
log.Fatalf("Error loading .env file: %v", err)
if _, exists := os.LookupEnv("DOCKER_ENV"); exists {
log.Println("Running in Docker container, using environment variables")
} else {
err := godotenv.Load(".env.dev")
if err != nil {
log.Printf("Warning: Error loading .env file: %v", err)
log.Println("Trying to use system environment variables...")
} else {
log.Println("Loaded environment from .env.dev file")
}
}
ConnectDatabase()
ConnectRedis()
go func() {
InitWhatsApp() // Ini tidak akan blocking startup server
}()
}

176
config/whatsapp.go Normal file
View File

@ -0,0 +1,176 @@
package config
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"os/signal"
"syscall"
_ "github.com/lib/pq"
"github.com/skip2/go-qrcode"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
"google.golang.org/protobuf/proto"
)
type WhatsAppService struct {
Client *whatsmeow.Client
Container *sqlstore.Container
}
var whatsappService *WhatsAppService
func InitWhatsApp() {
var err error
connectionString := fmt.Sprintf(
"user=%s password=%s dbname=%s host=%s port=%s sslmode=disable",
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
)
dbLog := waLog.Stdout("Database", "DEBUG", true)
container, err := sqlstore.New("postgres", connectionString, dbLog)
if err != nil {
log.Fatalf("Failed to connect to WhatsApp database: %v", err)
}
whatsappService = &WhatsAppService{
Container: container,
}
}
func GetWhatsAppService() *WhatsAppService {
return whatsappService
}
func eventHandler(evt interface{}) {
switch v := evt.(type) {
case *events.Message:
fmt.Println("Received a message!", v.Message.GetConversation())
case *events.Connected:
fmt.Println("WhatsApp client connected!")
case *events.Disconnected:
fmt.Println("WhatsApp client disconnected!")
}
}
func (wa *WhatsAppService) GenerateQR() (string, error) {
if wa.Container == nil {
return "", fmt.Errorf("container is not initialized")
}
deviceStore, err := wa.Container.GetFirstDevice()
if err != nil {
return "", fmt.Errorf("failed to get first device: %v", err)
}
clientLog := waLog.Stdout("Client", "DEBUG", true)
wa.Client = whatsmeow.NewClient(deviceStore, clientLog)
wa.Client.AddEventHandler(eventHandler)
if wa.Client.Store.ID == nil {
fmt.Println("Client is not logged in, generating QR code...")
qrChan, _ := wa.Client.GetQRChannel(context.Background())
err = wa.Client.Connect()
if err != nil {
return "", fmt.Errorf("failed to connect: %v", err)
}
for evt := range qrChan {
if evt.Event == "code" {
fmt.Println("QR code generated:", evt.Code)
png, err := qrcode.Encode(evt.Code, qrcode.Medium, 256)
if err != nil {
return "", fmt.Errorf("failed to create QR code: %v", err)
}
dataURI := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
return dataURI, nil
} else {
fmt.Println("Login event:", evt.Event)
if evt.Event == "success" {
return "success", nil
}
}
}
} else {
fmt.Println("Client already logged in, connecting...")
err = wa.Client.Connect()
if err != nil {
return "", fmt.Errorf("failed to connect: %v", err)
}
return "already_connected", nil
}
return "", fmt.Errorf("failed to generate QR code")
}
func (wa *WhatsAppService) SendMessage(phoneNumber, message string) error {
if wa.Client == nil {
return fmt.Errorf("client not initialized")
}
targetJID, err := types.ParseJID(phoneNumber + "@s.whatsapp.net")
if err != nil {
return fmt.Errorf("invalid phone number: %v", err)
}
msg := &waE2E.Message{
Conversation: proto.String(message),
}
_, err = wa.Client.SendMessage(context.Background(), targetJID, msg)
if err != nil {
return fmt.Errorf("failed to send message: %v", err)
}
return nil
}
func (wa *WhatsAppService) Logout() error {
if wa.Client == nil {
return fmt.Errorf("no active client session")
}
err := wa.Client.Logout()
if err != nil {
return fmt.Errorf("failed to logout: %v", err)
}
wa.Client.Disconnect()
wa.Client = nil
return nil
}
func (wa *WhatsAppService) IsConnected() bool {
return wa.Client != nil && wa.Client.IsConnected()
}
func (wa *WhatsAppService) IsLoggedIn() bool {
return wa.Client != nil && wa.Client.Store.ID != nil
}
func (wa *WhatsAppService) GracefulShutdown() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
fmt.Println("Shutting down WhatsApp client...")
if wa.Client != nil {
wa.Client.Disconnect()
}
os.Exit(0)
}()
}

112
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,112 @@
# docker-compose.dev.yml - Development environment dengan Air hot reload
services:
# PostgreSQL Database
postgres:
image: postgres:16-alpine
container_name: rijig_postgres_dev
restart: unless-stopped
environment:
POSTGRES_DB: apirijig_v2
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pahmiadmin
PGDATA: /var/lib/postgresql/data/pgdata
ports:
- "5433:5432"
volumes:
- postgres_data_dev:/var/lib/postgresql/data
- ./init-db:/docker-entrypoint-initdb.d
networks:
- rijig_network_dev
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d apirijig_v2"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis Cache
redis:
image: redis:7-alpine
container_name: rijig_redis_dev
restart: unless-stopped
ports:
- "6378:6379"
volumes:
- redis_data_dev:/data
networks:
- rijig_network_dev
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
start_period: 15s
# Go Application dengan Air hot reload
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: rijig_app_dev
restart: unless-stopped
ports:
- "7000:7000"
env_file:
- .env.docker
volumes:
# Mount source code untuk hot reload
- .:/app
# Cache Go modules untuk performance
- go_modules_cache:/go/pkg/mod
# Exclude tmp directory untuk mencegah konflik
- /app/tmp
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- rijig_network_dev
working_dir: /app
# pgAdmin (optional - untuk GUI database management)
pgadmin:
image: dpage/pgadmin4:latest
container_name: rijig_pgadmin_dev
restart: unless-stopped
environment:
PGADMIN_DEFAULT_EMAIL: admin@rijig.com
PGADMIN_DEFAULT_PASSWORD: admin123
PGADMIN_CONFIG_SERVER_MODE: "False"
ports:
- "8080:80"
volumes:
- pgadmin_data_dev:/var/lib/pgadmin
depends_on:
- postgres
networks:
- rijig_network_dev
# Redis Commander (optional - untuk GUI redis management)
redis-commander:
image: rediscommander/redis-commander:latest
container_name: rijig_redis_commander_dev
restart: unless-stopped
environment:
REDIS_HOSTS: local:redis:6379
ports:
- "8081:8081"
depends_on:
- redis
networks:
- rijig_network_dev
networks:
rijig_network_dev:
driver: bridge
volumes:
postgres_data_dev:
redis_data_dev:
pgadmin_data_dev:
go_modules_cache:

View File

@ -1,61 +0,0 @@
package dto
import "strings"
type AddressResponseDTO struct {
UserID string `json:"user_id"`
ID string `json:"address_id"`
Province string `json:"province"`
Regency string `json:"regency"`
District string `json:"district"`
Village string `json:"village"`
PostalCode string `json:"postalCode"`
Detail string `json:"detail"`
Geography string `json:"geography"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type CreateAddressDTO struct {
Province string `json:"province_id"`
Regency string `json:"regency_id"`
District string `json:"district_id"`
Village string `json:"village_id"`
PostalCode string `json:"postalCode"`
Detail string `json:"detail"`
Geography string `json:"geography"`
}
func (r *CreateAddressDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Province) == "" {
errors["province_id"] = append(errors["province_id"], "Province ID is required")
}
if strings.TrimSpace(r.Regency) == "" {
errors["regency_id"] = append(errors["regency_id"], "Regency ID is required")
}
if strings.TrimSpace(r.District) == "" {
errors["district_id"] = append(errors["district_id"], "District ID is required")
}
if strings.TrimSpace(r.Village) == "" {
errors["village_id"] = append(errors["village_id"], "Village ID is required")
}
if strings.TrimSpace(r.PostalCode) == "" {
errors["postalCode"] = append(errors["village_id"], "PostalCode ID is required")
} else if len(r.PostalCode) < 5 {
errors["postalCode"] = append(errors["postalCode"], "kode pos belum sesuai")
}
if strings.TrimSpace(r.Detail) == "" {
errors["detail"] = append(errors["detail"], "Detail address is required")
}
if strings.TrimSpace(r.Geography) == "" {
errors["geography"] = append(errors["geography"], "Geographic coordinates are required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -1,121 +0,0 @@
package dto
import (
"regexp"
"strings"
)
type LoginDTO struct {
RoleID string `json:"roleid"`
Identifier string `json:"identifier"`
Password string `json:"password"`
}
type UserResponseWithToken struct {
UserID string `json:"user_id"`
RoleName string `json:"role_name"`
Token string `json:"token"`
}
type RegisterDTO struct {
Username string `json:"username"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
Password string `json:"password"`
ConfirmPassword string `json:"confirm_password"`
RoleID string `json:"roleId,omitempty"`
}
func (l *LoginDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(l.RoleID) == "" {
errors["roleid"] = append(errors["roleid"], "Role ID is required")
}
if strings.TrimSpace(l.Identifier) == "" {
errors["identifier"] = append(errors["identifier"], "Identifier (username, email, or phone) is required")
}
if strings.TrimSpace(l.Password) == "" {
errors["password"] = append(errors["password"], "Password is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RegisterDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
r.validateRequiredFields(errors)
if r.Phone != "" && !IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits")
}
if r.Email != "" && !IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if r.Password != "" && !IsValidPassword(r.Password) {
errors["password"] = append(errors["password"], "Password must be at least 8 characters long and contain at least one number")
}
if r.ConfirmPassword != "" && r.Password != r.ConfirmPassword {
errors["confirm_password"] = append(errors["confirm_password"], "Password and confirm password do not match")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RegisterDTO) validateRequiredFields(errors map[string][]string) {
if strings.TrimSpace(r.Username) == "" {
errors["username"] = append(errors["username"], "Username is required")
}
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "Name is required")
}
if strings.TrimSpace(r.Phone) == "" {
errors["phone"] = append(errors["phone"], "Phone number is required")
}
if strings.TrimSpace(r.Email) == "" {
errors["email"] = append(errors["email"], "Email is required")
}
if strings.TrimSpace(r.Password) == "" {
errors["password"] = append(errors["password"], "Password is required")
}
if strings.TrimSpace(r.ConfirmPassword) == "" {
errors["confirm_password"] = append(errors["confirm_password"], "Confirm password is required")
}
if strings.TrimSpace(r.RoleID) == "" {
errors["roleId"] = append(errors["roleId"], "RoleID is required")
}
}
func IsValidPhoneNumber(phone string) bool {
re := regexp.MustCompile(`^\+62\d{9,13}$`)
return re.MatchString(phone)
}
func IsValidEmail(email string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(email)
}
func IsValidPassword(password string) bool {
if len(password) < 8 {
return false
}
re := regexp.MustCompile(`\d`)
return re.MatchString(password)
}

View File

@ -1,29 +0,0 @@
package dto
import "strings"
type ResponseBannerDTO struct {
ID string `json:"id"`
BannerName string `json:"bannername"`
BannerImage string `json:"bannerimage"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestBannerDTO struct {
BannerName string `json:"bannername"`
BannerImage string `json:"bannerimage"`
}
func (r *RequestBannerDTO) ValidateBannerInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.BannerName) == "" {
errors["bannername"] = append(errors["bannername"], "nama banner harus diisi")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -1,34 +0,0 @@
package dto
import "strings"
type ReponseInitialCointDTO struct {
ID string `json:"coin_id"`
CoinName string `json:"coin_name"`
ValuePerUnit float64 `json:"value_perunit"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestInitialCointDTO struct {
CoinName string `json:"coin_name"`
ValuePerUnit float64 `json:"value_perunit"`
}
func (r *RequestInitialCointDTO) ValidateCointInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.CoinName) == "" {
errors["coin_name"] = append(errors["coin_name"], "nama coin harus diisi")
}
if r.ValuePerUnit <= 0 {
errors["value_perunit"] = append(errors["value_perunit"], "value per unit harus lebih besar dari 0")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -1,59 +0,0 @@
package dto
import "strings"
type RequestTrashCategoryDTO struct {
Name string `json:"name"`
}
type ResponseTrashCategoryDTO struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
Details []ResponseTrashDetailDTO `json:"details,omitempty"`
}
type ResponseTrashDetailDTO struct {
ID string `json:"id"`
CategoryID string `json:"category_id"`
Description string `json:"description"`
Price float64 `json:"price"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestTrashDetailDTO struct {
CategoryID string `json:"category_id"`
Description string `json:"description"`
Price float64 `json:"price"`
}
func (r *RequestTrashCategoryDTO) ValidateTrashCategoryInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "name is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RequestTrashDetailDTO) ValidateTrashDetailInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Description) == "" {
errors["description"] = append(errors["description"], "description is required")
}
if r.Price <= 0 {
errors["price"] = append(errors["price"], "price must be greater than 0")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -1,95 +0,0 @@
package dto
import (
"regexp"
"strings"
)
type UserResponseDTO struct {
ID string `json:"id"`
Username string `json:"username"`
Avatar *string `json:"photoprofile,omitempty"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
EmailVerified bool `json:"emailVerified"`
RoleName string `json:"role"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type UpdateUserDTO struct {
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email"`
}
func (r *UpdateUserDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "Name is required")
}
if strings.TrimSpace(r.Phone) == "" {
errors["phone"] = append(errors["phone"], "Phone number is required")
} else if !IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "Invalid phone number format. Use +62 followed by 9-13 digits")
}
if strings.TrimSpace(r.Email) == "" {
errors["email"] = append(errors["email"], "Email is required")
} else if !IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func IsUpdateValidPhoneNumber(phone string) bool {
re := regexp.MustCompile(`^\+62\d{9,13}$`)
return re.MatchString(phone)
}
func IsUPdateValidEmail(email string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(email)
}
type UpdatePasswordDTO struct {
OldPassword string `json:"old_password"`
NewPassword string `json:"new_password"`
ConfirmNewPassword string `json:"confirm_new_password"`
}
func (u *UpdatePasswordDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if u.OldPassword == "" {
errors["old_password"] = append(errors["old_password"], "Old password is required")
}
if u.NewPassword == "" {
errors["new_password"] = append(errors["new_password"], "New password is required")
} else if len(u.NewPassword) < 8 {
errors["new_password"] = append(errors["new_password"], "Password must be at least 8 characters long")
}
if u.ConfirmNewPassword == "" {
errors["confirm_new_password"] = append(errors["confirm_new_password"], "Confirm new password is required")
} else if u.NewPassword != u.ConfirmNewPassword {
errors["confirm_new_password"] = append(errors["confirm_new_password"], "Passwords do not match")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -1,66 +0,0 @@
package dto
import (
"fmt"
"regexp"
"strings"
)
type RequestUserPinDTO struct {
Pin string `json:"userpin"`
}
func (r *RequestUserPinDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Pin) == "" {
errors["pin"] = append(errors["pin"], "Pin is required")
}
if err := validatePin(r.Pin); err != nil {
errors["pin"] = append(errors["pin"], err.Error())
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type UpdateUserPinDTO struct {
OldPin string `json:"old_pin"`
NewPin string `json:"new_pin"`
}
func (u *UpdateUserPinDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(u.OldPin) == "" {
errors["old_pin"] = append(errors["old_pin"], "Old pin is required")
}
if err := validatePin(u.NewPin); err != nil {
errors["new_pin"] = append(errors["new_pin"], err.Error())
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func isNumeric(s string) bool {
re := regexp.MustCompile(`^[0-9]+$`)
return re.MatchString(s)
}
func validatePin(pin string) error {
if len(pin) != 6 {
return fmt.Errorf("pin harus terdiri dari 6 digit")
} else if !isNumeric(pin) {
return fmt.Errorf("pin harus berupa angka")
}
return nil
}

54
go.mod
View File

@ -1,41 +1,57 @@
module github.com/pahmiudahgede/senggoldong
module rijig
go 1.23.3
require (
github.com/go-redis/redis/v8 v8.11.5
github.com/gofiber/fiber/v2 v2.52.5
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
golang.org/x/crypto v0.36.0
gorm.io/driver/postgres v1.5.11
gorm.io/gorm v1.25.12
)
require (
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // direct
)
// require (
// golang.org/x/term v0.30.0 // indirect
// rsc.io/qr v0.2.0 // indirect
// )
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.23.0 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/gofiber/fiber/v2 v2.52.5 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lucsky/cuid v1.2.1 // indirect
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // direct
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.20.0 // indirect
gorm.io/driver/postgres v1.5.11 // indirect
gorm.io/gorm v1.25.12 // indirect
go.mau.fi/libsignal v0.1.2 // indirect
go.mau.fi/util v0.8.6 // indirect
go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175
golang.org/x/net v0.37.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5
)

101
go.sum
View File

@ -1,26 +1,30 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -37,48 +41,89 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucsky/cuid v1.2.1 h1:MtJrL2OFhvYufUIn48d35QGXyeTC8tn0upumW9WwTHg=
github.com/lucsky/cuid v1.2.1/go.mod h1:QaaJqckboimOmhRSJXSx/+IT+VTfxfPGSo/6mfgUfmE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175 h1:BDShdc10qJzi3B0xPGA6HVQl+929wIFst8/W+8EnvbI=
go.mau.fi/whatsmeow v0.0.0-20250316144733-e7e263bf2175/go.mod h1:WNhj4JeQ6YR6dUOEiCXKqmE4LavSFkwRoKmu4atRrRs=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View File

@ -0,0 +1,66 @@
package about
import (
"strings"
)
type RequestAboutDTO struct {
Title string `json:"title"`
CoverImage string `json:"cover_image"`
}
func (r *RequestAboutDTO) ValidateAbout() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Title) == "" {
errors["title"] = append(errors["title"], "Title is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type ResponseAboutDTO struct {
ID string `json:"id"`
Title string `json:"title"`
CoverImage string `json:"cover_image"`
AboutDetail *[]ResponseAboutDetailDTO `json:"about_detail"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type RequestAboutDetailDTO struct {
AboutId string `json:"about_id"`
ImageDetail string `json:"image_detail"`
Description string `json:"description"`
}
func (r *RequestAboutDetailDTO) ValidateAboutDetail() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.AboutId) == "" {
errors["about_id"] = append(errors["about_id"], "about_id is required")
}
if strings.TrimSpace(r.Description) == "" {
errors["description"] = append(errors["description"], "Description is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type ResponseAboutDetailDTO struct {
ID string `json:"id"`
AboutID string `json:"about_id"`
ImageDetail string `json:"image_detail"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -0,0 +1,175 @@
package about
import (
"fmt"
"log"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type AboutHandler struct {
AboutService AboutService
}
func NewAboutHandler(aboutService AboutService) *AboutHandler {
return &AboutHandler{
AboutService: aboutService,
}
}
func (h *AboutHandler) CreateAbout(c *fiber.Ctx) error {
var request RequestAboutDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateAbout()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
aboutCoverImage, err := c.FormFile("cover_image")
if err != nil {
return utils.BadRequest(c, "Cover image is required")
}
response, err := h.AboutService.CreateAbout(c.Context(), request, aboutCoverImage)
if err != nil {
log.Printf("Error creating About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to create About: %v", err))
}
return utils.CreateSuccessWithData(c, "Successfully created About", response)
}
func (h *AboutHandler) UpdateAbout(c *fiber.Ctx) error {
id := c.Params("id")
var request RequestAboutDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
aboutCoverImage, err := c.FormFile("cover_image")
if err != nil {
log.Printf("Error retrieving cover image about from request: %v", err)
return utils.BadRequest(c, "cover_image is required")
}
response, err := h.AboutService.UpdateAbout(c.Context(), id, request, aboutCoverImage)
if err != nil {
log.Printf("Error updating About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to update About: %v", err))
}
return utils.SuccessWithData(c, "Successfully updated About", response)
}
func (h *AboutHandler) GetAllAbout(c *fiber.Ctx) error {
response, err := h.AboutService.GetAllAbout(c.Context())
if err != nil {
log.Printf("Error fetching all About: %v", err)
return utils.InternalServerError(c, "Failed to fetch About list")
}
return utils.SuccessWithPagination(c, "Successfully fetched About list", response, 1, len(response))
}
func (h *AboutHandler) GetAboutByID(c *fiber.Ctx) error {
id := c.Params("id")
response, err := h.AboutService.GetAboutByID(c.Context(), id)
if err != nil {
log.Printf("Error fetching About by ID: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err))
}
return utils.SuccessWithData(c, "Successfully fetched About", response)
}
func (h *AboutHandler) GetAboutDetailById(c *fiber.Ctx) error {
id := c.Params("id")
response, err := h.AboutService.GetAboutDetailById(c.Context(), id)
if err != nil {
log.Printf("Error fetching About detail by ID: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to fetch About by ID: %v", err))
}
return utils.SuccessWithData(c, "Successfully fetched About", response)
}
func (h *AboutHandler) DeleteAbout(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.AboutService.DeleteAbout(c.Context(), id); err != nil {
log.Printf("Error deleting About: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to delete About: %v", err))
}
return utils.Success(c, "Successfully deleted About")
}
func (h *AboutHandler) CreateAboutDetail(c *fiber.Ctx) error {
var request RequestAboutDetailDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
errors, valid := request.ValidateAboutDetail()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
aboutDetailImage, err := c.FormFile("image_detail")
if err != nil {
log.Printf("Error retrieving image detail from request: %v", err)
return utils.BadRequest(c, "image_detail is required")
}
response, err := h.AboutService.CreateAboutDetail(c.Context(), request, aboutDetailImage)
if err != nil {
log.Printf("Error creating AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to create AboutDetail: %v", err))
}
return utils.CreateSuccessWithData(c, "Successfully created AboutDetail", response)
}
func (h *AboutHandler) UpdateAboutDetail(c *fiber.Ctx) error {
id := c.Params("id")
var request RequestAboutDetailDTO
if err := c.BodyParser(&request); err != nil {
log.Printf("Error parsing request body: %v", err)
return utils.BadRequest(c, "Invalid input data")
}
aboutDetailImage, err := c.FormFile("image_detail")
if err != nil {
log.Printf("Error retrieving image detail from request: %v", err)
return utils.BadRequest(c, "image_detail is required")
}
response, err := h.AboutService.UpdateAboutDetail(c.Context(), id, request, aboutDetailImage)
if err != nil {
log.Printf("Error updating AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to update AboutDetail: %v", err))
}
return utils.SuccessWithData(c, "Successfully updated AboutDetail", response)
}
func (h *AboutHandler) DeleteAboutDetail(c *fiber.Ctx) error {
id := c.Params("id")
if err := h.AboutService.DeleteAboutDetail(c.Context(), id); err != nil {
log.Printf("Error deleting AboutDetail: %v", err)
return utils.InternalServerError(c, fmt.Sprintf("Failed to delete AboutDetail: %v", err))
}
return utils.Success(c, "Successfully deleted AboutDetail")
}

View File

@ -0,0 +1,113 @@
package about
import (
"context"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type AboutRepository interface {
CreateAbout(ctx context.Context, about *model.About) error
CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error
GetAllAbout(ctx context.Context) ([]model.About, error)
GetAboutByID(ctx context.Context, id string) (*model.About, error)
GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error)
GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error)
UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error)
UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error)
DeleteAbout(ctx context.Context, id string) error
DeleteAboutDetail(ctx context.Context, id string) error
}
type aboutRepository struct {
db *gorm.DB
}
func NewAboutRepository(db *gorm.DB) AboutRepository {
return &aboutRepository{db}
}
func (r *aboutRepository) CreateAbout(ctx context.Context, about *model.About) error {
if err := r.db.WithContext(ctx).Create(&about).Error; err != nil {
return fmt.Errorf("failed to create About: %v", err)
}
return nil
}
func (r *aboutRepository) CreateAboutDetail(ctx context.Context, aboutDetail *model.AboutDetail) error {
if err := r.db.WithContext(ctx).Create(&aboutDetail).Error; err != nil {
return fmt.Errorf("failed to create AboutDetail: %v", err)
}
return nil
}
func (r *aboutRepository) GetAllAbout(ctx context.Context) ([]model.About, error) {
var abouts []model.About
if err := r.db.WithContext(ctx).Find(&abouts).Error; err != nil {
return nil, fmt.Errorf("failed to fetch all About records: %v", err)
}
return abouts, nil
}
func (r *aboutRepository) GetAboutByID(ctx context.Context, id string) (*model.About, error) {
var about model.About
if err := r.db.WithContext(ctx).Preload("AboutDetail").Where("id = ?", id).First(&about).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("about with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch About by ID: %v", err)
}
return &about, nil
}
func (r *aboutRepository) GetAboutByIDWithoutPrel(ctx context.Context, id string) (*model.About, error) {
var about model.About
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&about).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("about with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch About by ID: %v", err)
}
return &about, nil
}
func (r *aboutRepository) GetAboutDetailByID(ctx context.Context, id string) (*model.AboutDetail, error) {
var aboutDetail model.AboutDetail
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&aboutDetail).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("aboutdetail with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch AboutDetail by ID: %v", err)
}
return &aboutDetail, nil
}
func (r *aboutRepository) UpdateAbout(ctx context.Context, id string, about *model.About) (*model.About, error) {
if err := r.db.WithContext(ctx).Model(&about).Where("id = ?", id).Updates(about).Error; err != nil {
return nil, fmt.Errorf("failed to update About: %v", err)
}
return about, nil
}
func (r *aboutRepository) UpdateAboutDetail(ctx context.Context, id string, aboutDetail *model.AboutDetail) (*model.AboutDetail, error) {
if err := r.db.WithContext(ctx).Model(&aboutDetail).Where("id = ?", id).Updates(aboutDetail).Error; err != nil {
return nil, fmt.Errorf("failed to update AboutDetail: %v", err)
}
return aboutDetail, nil
}
func (r *aboutRepository) DeleteAbout(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.About{}).Error; err != nil {
return fmt.Errorf("failed to delete About: %v", err)
}
return nil
}
func (r *aboutRepository) DeleteAboutDetail(ctx context.Context, id string) error {
if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.AboutDetail{}).Error; err != nil {
return fmt.Errorf("failed to delete AboutDetail: %v", err)
}
return nil
}

View File

@ -0,0 +1,32 @@
package about
import (
"rijig/config"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func AboutRouter(api fiber.Router) {
aboutRepo := NewAboutRepository(config.DB)
aboutService := NewAboutService(aboutRepo)
aboutHandler := NewAboutHandler(aboutService)
aboutRoutes := api.Group("/about")
aboutRoutes.Use(middleware.AuthMiddleware())
aboutRoutes.Get("/", aboutHandler.GetAllAbout)
aboutRoutes.Get("/:id", aboutHandler.GetAboutByID)
aboutRoutes.Post("/", aboutHandler.CreateAbout)
aboutRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAbout)
aboutRoutes.Delete("/:id", aboutHandler.DeleteAbout)
aboutDetailRoutes := api.Group("/about-detail")
aboutDetailRoutes.Use(middleware.AuthMiddleware())
aboutDetailRoute := api.Group("/about-detail")
aboutDetailRoute.Get("/:id", aboutHandler.GetAboutDetailById)
aboutDetailRoutes.Post("/", aboutHandler.CreateAboutDetail)
aboutDetailRoutes.Put("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.UpdateAboutDetail)
aboutDetailRoutes.Delete("/:id", middleware.RequireRoles(utils.RoleAdministrator), aboutHandler.DeleteAboutDetail)
}

View File

@ -0,0 +1,496 @@
package about
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/model"
"rijig/utils"
"time"
"github.com/google/uuid"
)
const (
cacheKeyAllAbout = "about:all"
cacheKeyAboutByID = "about:id:%s"
cacheKeyAboutDetail = "about_detail:id:%s"
cacheTTL = 30 * time.Minute
)
type AboutService interface {
CreateAbout(ctx context.Context, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error)
UpdateAbout(ctx context.Context, id string, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error)
GetAllAbout(ctx context.Context) ([]ResponseAboutDTO, error)
GetAboutByID(ctx context.Context, id string) (*ResponseAboutDTO, error)
GetAboutDetailById(ctx context.Context, id string) (*ResponseAboutDetailDTO, error)
DeleteAbout(ctx context.Context, id string) error
CreateAboutDetail(ctx context.Context, request RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error)
UpdateAboutDetail(ctx context.Context, id string, request RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error)
DeleteAboutDetail(ctx context.Context, id string) error
}
type aboutService struct {
aboutRepo AboutRepository
}
func NewAboutService(aboutRepo AboutRepository) AboutService {
return &aboutService{aboutRepo: aboutRepo}
}
func (s *aboutService) invalidateAboutCaches(aboutID string) {
if err := utils.DeleteCache(cacheKeyAllAbout); err != nil {
log.Printf("Failed to invalidate all about cache: %v", err)
}
aboutCacheKey := fmt.Sprintf(cacheKeyAboutByID, aboutID)
if err := utils.DeleteCache(aboutCacheKey); err != nil {
log.Printf("Failed to invalidate about cache for ID %s: %v", aboutID, err)
}
}
func (s *aboutService) invalidateAboutDetailCaches(aboutDetailID, aboutID string) {
detailCacheKey := fmt.Sprintf(cacheKeyAboutDetail, aboutDetailID)
if err := utils.DeleteCache(detailCacheKey); err != nil {
log.Printf("Failed to invalidate about detail cache for ID %s: %v", aboutDetailID, err)
}
s.invalidateAboutCaches(aboutID)
}
func formatResponseAboutDetailDTO(about *model.AboutDetail) (*ResponseAboutDetailDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt)
response := &ResponseAboutDetailDTO{
ID: about.ID,
AboutID: about.AboutID,
ImageDetail: about.ImageDetail,
Description: about.Description,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
return response, nil
}
func formatResponseAboutDTO(about *model.About) (*ResponseAboutDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(about.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(about.UpdatedAt)
response := &ResponseAboutDTO{
ID: about.ID,
Title: about.Title,
CoverImage: about.CoverImage,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
return response, nil
}
func (s *aboutService) saveCoverImageAbout(coverImageAbout *multipart.FileHeader) (string, error) {
pathImage := "/uploads/coverabout/"
coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil {
return "", fmt.Errorf("gagal membuat direktori untuk cover image about: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverImageAbout.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverImageFileName := fmt.Sprintf("%s_coverabout%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName)
src, err := coverImageAbout.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image about file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover image about: %v", err)
}
coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName)
return coverImageAboutUrl, nil
}
func (s *aboutService) saveCoverImageAboutDetail(coverImageAbout *multipart.FileHeader) (string, error) {
pathImage := "/uploads/coverabout/coveraboutdetail/"
coverImageAboutDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(coverImageAboutDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageAboutDir, os.ModePerm); err != nil {
return "", fmt.Errorf("gagal membuat direktori untuk cover image about detail: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverImageAbout.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverImageFileName := fmt.Sprintf("%s_coveraboutdetail%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageAboutDir, coverImageFileName)
src, err := coverImageAbout.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image about detail file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover image about detail: %v", err)
}
coverImageAboutUrl := fmt.Sprintf("%s%s", pathImage, coverImageFileName)
return coverImageAboutUrl, nil
}
func deleteCoverImageAbout(coverimageAboutPath string) error {
if coverimageAboutPath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + coverimageAboutPath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *aboutService) CreateAbout(ctx context.Context, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) {
errors, valid := request.ValidateAbout()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
coverImageAboutPath, err := s.saveCoverImageAbout(coverImageAbout)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan cover image about: %v", err)
}
about := model.About{
Title: request.Title,
CoverImage: coverImageAboutPath,
}
if err := s.aboutRepo.CreateAbout(ctx, &about); err != nil {
return nil, fmt.Errorf("failed to create About: %v", err)
}
response, err := formatResponseAboutDTO(&about)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
s.invalidateAboutCaches("")
return response, nil
}
func (s *aboutService) UpdateAbout(ctx context.Context, id string, request RequestAboutDTO, coverImageAbout *multipart.FileHeader) (*ResponseAboutDTO, error) {
errors, valid := request.ValidateAbout()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about not found: %v", err)
}
oldCoverImage := about.CoverImage
var coverImageAboutPath string
if coverImageAbout != nil {
coverImageAboutPath, err = s.saveCoverImageAbout(coverImageAbout)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err)
}
}
about.Title = request.Title
if coverImageAboutPath != "" {
about.CoverImage = coverImageAboutPath
}
updatedAbout, err := s.aboutRepo.UpdateAbout(ctx, id, about)
if err != nil {
return nil, fmt.Errorf("failed to update About: %v", err)
}
if oldCoverImage != "" && coverImageAboutPath != "" {
if err := deleteCoverImageAbout(oldCoverImage); err != nil {
log.Printf("Warning: failed to delete old image: %v", err)
}
}
response, err := formatResponseAboutDTO(updatedAbout)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
s.invalidateAboutCaches(id)
return response, nil
}
func (s *aboutService) GetAllAbout(ctx context.Context) ([]ResponseAboutDTO, error) {
var cachedAbouts []ResponseAboutDTO
if err := utils.GetCache(cacheKeyAllAbout, &cachedAbouts); err == nil {
return cachedAbouts, nil
}
aboutList, err := s.aboutRepo.GetAllAbout(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get About list: %v", err)
}
var aboutDTOList []ResponseAboutDTO
for _, about := range aboutList {
response, err := formatResponseAboutDTO(&about)
if err != nil {
log.Printf("Error formatting About response: %v", err)
continue
}
aboutDTOList = append(aboutDTOList, *response)
}
if err := utils.SetCache(cacheKeyAllAbout, aboutDTOList, cacheTTL); err != nil {
log.Printf("Failed to cache all about data: %v", err)
}
return aboutDTOList, nil
}
func (s *aboutService) GetAboutByID(ctx context.Context, id string) (*ResponseAboutDTO, error) {
cacheKey := fmt.Sprintf(cacheKeyAboutByID, id)
var cachedAbout ResponseAboutDTO
if err := utils.GetCache(cacheKey, &cachedAbout); err == nil {
return &cachedAbout, nil
}
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about not found: %v", err)
}
response, err := formatResponseAboutDTO(about)
if err != nil {
return nil, fmt.Errorf("error formatting About response: %v", err)
}
var responseDetails []ResponseAboutDetailDTO
for _, detail := range about.AboutDetail {
formattedDetail, err := formatResponseAboutDetailDTO(&detail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
responseDetails = append(responseDetails, *formattedDetail)
}
response.AboutDetail = &responseDetails
if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil {
log.Printf("Failed to cache about data for ID %s: %v", id, err)
}
return response, nil
}
func (s *aboutService) GetAboutDetailById(ctx context.Context, id string) (*ResponseAboutDetailDTO, error) {
cacheKey := fmt.Sprintf(cacheKeyAboutDetail, id)
var cachedDetail ResponseAboutDetailDTO
if err := utils.GetCache(cacheKey, &cachedDetail); err == nil {
return &cachedDetail, nil
}
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about detail not found: %v", err)
}
response, err := formatResponseAboutDetailDTO(aboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
if err := utils.SetCache(cacheKey, response, cacheTTL); err != nil {
log.Printf("Failed to cache about detail data for ID %s: %v", id, err)
}
return response, nil
}
func (s *aboutService) DeleteAbout(ctx context.Context, id string) error {
about, err := s.aboutRepo.GetAboutByID(ctx, id)
if err != nil {
return fmt.Errorf("about not found: %v", err)
}
if about.CoverImage != "" {
if err := deleteCoverImageAbout(about.CoverImage); err != nil {
log.Printf("Warning: failed to delete cover image: %v", err)
}
}
for _, detail := range about.AboutDetail {
if detail.ImageDetail != "" {
if err := deleteCoverImageAbout(detail.ImageDetail); err != nil {
log.Printf("Warning: failed to delete detail image: %v", err)
}
}
}
if err := s.aboutRepo.DeleteAbout(ctx, id); err != nil {
return fmt.Errorf("failed to delete About: %v", err)
}
s.invalidateAboutCaches(id)
return nil
}
func (s *aboutService) CreateAboutDetail(ctx context.Context, request RequestAboutDetailDTO, coverImageAboutDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) {
errors, valid := request.ValidateAboutDetail()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
_, err := s.aboutRepo.GetAboutByIDWithoutPrel(ctx, request.AboutId)
if err != nil {
return nil, fmt.Errorf("about_id tidak ditemukan: %v", err)
}
coverImageAboutDetailPath, err := s.saveCoverImageAboutDetail(coverImageAboutDetail)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan cover image about detail: %v", err)
}
aboutDetail := model.AboutDetail{
AboutID: request.AboutId,
ImageDetail: coverImageAboutDetailPath,
Description: request.Description,
}
if err := s.aboutRepo.CreateAboutDetail(ctx, &aboutDetail); err != nil {
return nil, fmt.Errorf("failed to create AboutDetail: %v", err)
}
response, err := formatResponseAboutDetailDTO(&aboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
s.invalidateAboutDetailCaches("", request.AboutId)
return response, nil
}
func (s *aboutService) UpdateAboutDetail(ctx context.Context, id string, request RequestAboutDetailDTO, imageDetail *multipart.FileHeader) (*ResponseAboutDetailDTO, error) {
errors, valid := request.ValidateAboutDetail()
if !valid {
return nil, fmt.Errorf("validation error: %v", errors)
}
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("about detail tidak ditemukan: %v", err)
}
oldImageDetail := aboutDetail.ImageDetail
var coverImageAboutDetailPath string
if imageDetail != nil {
coverImageAboutDetailPath, err = s.saveCoverImageAboutDetail(imageDetail)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan gambar baru: %v", err)
}
}
aboutDetail.Description = request.Description
if coverImageAboutDetailPath != "" {
aboutDetail.ImageDetail = coverImageAboutDetailPath
}
updatedAboutDetail, err := s.aboutRepo.UpdateAboutDetail(ctx, id, aboutDetail)
if err != nil {
return nil, fmt.Errorf("failed to update AboutDetail: %v", err)
}
if oldImageDetail != "" && coverImageAboutDetailPath != "" {
if err := deleteCoverImageAbout(oldImageDetail); err != nil {
log.Printf("Warning: failed to delete old detail image: %v", err)
}
}
response, err := formatResponseAboutDetailDTO(updatedAboutDetail)
if err != nil {
return nil, fmt.Errorf("error formatting AboutDetail response: %v", err)
}
s.invalidateAboutDetailCaches(id, aboutDetail.AboutID)
return response, nil
}
func (s *aboutService) DeleteAboutDetail(ctx context.Context, id string) error {
aboutDetail, err := s.aboutRepo.GetAboutDetailByID(ctx, id)
if err != nil {
return fmt.Errorf("about detail tidak ditemukan: %v", err)
}
aboutID := aboutDetail.AboutID
if aboutDetail.ImageDetail != "" {
if err := deleteCoverImageAbout(aboutDetail.ImageDetail); err != nil {
log.Printf("Warning: failed to delete detail image: %v", err)
}
}
if err := s.aboutRepo.DeleteAboutDetail(ctx, id); err != nil {
return fmt.Errorf("failed to delete AboutDetail: %v", err)
}
s.invalidateAboutDetailCaches(id, aboutID)
return nil
}

View File

@ -0,0 +1,73 @@
package address
import "strings"
type AddressResponseDTO struct {
UserID string `json:"user_id,omitempty"`
ID string `json:"address_id,omitempty"`
Province string `json:"province,omitempty"`
Regency string `json:"regency,omitempty"`
District string `json:"district,omitempty"`
Village string `json:"village,omitempty"`
PostalCode string `json:"postalCode,omitempty"`
Detail string `json:"detail,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
UpdatedAt string `json:"updatedAt,omitempty"`
}
type CreateAddressDTO struct {
Province string `json:"province_id"`
Regency string `json:"regency_id"`
District string `json:"district_id"`
Village string `json:"village_id"`
PostalCode string `json:"postalCode"`
Detail string `json:"detail"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
func (r *CreateAddressDTO) ValidateAddress() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Province) == "" {
errors["province_id"] = append(errors["province_id"], "Province ID is required")
}
if strings.TrimSpace(r.Regency) == "" {
errors["regency_id"] = append(errors["regency_id"], "Regency ID is required")
}
if strings.TrimSpace(r.District) == "" {
errors["district_id"] = append(errors["district_id"], "District ID is required")
}
if strings.TrimSpace(r.Village) == "" {
errors["village_id"] = append(errors["village_id"], "Village ID is required")
}
if strings.TrimSpace(r.PostalCode) == "" {
errors["postalCode"] = append(errors["postalCode"], "PostalCode is required")
} else if len(r.PostalCode) < 5 {
errors["postalCode"] = append(errors["postalCode"], "PostalCode must be at least 5 characters")
}
if strings.TrimSpace(r.Detail) == "" {
errors["detail"] = append(errors["detail"], "Detail address is required")
}
if r.Latitude == 0 {
errors["latitude"] = append(errors["latitude"], "Latitude is required")
}
if r.Longitude == 0 {
errors["longitude"] = append(errors["longitude"], "Longitude is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,112 @@
package address
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type AddressHandler struct {
AddressService AddressService
}
func NewAddressHandler(addressService AddressService) *AddressHandler {
return &AddressHandler{AddressService: addressService}
}
func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error {
var request CreateAddressDTO
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateAddress()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
addressResponse, err := h.AddressService.CreateAddress(c.Context(), claims.UserID, request)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.CreateSuccessWithData(c, "user address created successfully", addressResponse)
}
func (h *AddressHandler) GetAddressByUserID(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
addresses, err := h.AddressService.GetAddressByUserID(c.Context(), claims.UserID)
if err != nil {
return utils.NotFound(c, err.Error())
}
return utils.SuccessWithData(c, "User addresses fetched successfully", addresses)
}
func (h *AddressHandler) GetAddressByID(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
addressID := c.Params("address_id")
address, err := h.AddressService.GetAddressByID(c.Context(), claims.UserID, addressID)
if err != nil {
return utils.NotFound(c, err.Error())
}
return utils.SuccessWithData(c, "Address fetched successfully", address)
}
func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error {
addressID := c.Params("address_id")
var request CreateAddressDTO
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateAddress()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
updatedAddress, err := h.AddressService.UpdateAddress(c.Context(), claims.UserID, addressID, request)
if err != nil {
return utils.NotFound(c, err.Error())
}
return utils.SuccessWithData(c, "User address updated successfully", updatedAddress)
}
func (h *AddressHandler) DeleteAddress(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
addressID := c.Params("address_id")
err = h.AddressService.DeleteAddress(c.Context(), claims.UserID, addressID)
if err != nil {
return utils.Forbidden(c, err.Error())
}
return utils.Success(c, "Address deleted successfully")
}

View File

@ -0,0 +1,62 @@
package address
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type AddressRepository interface {
CreateAddress(ctx context.Context, address *model.Address) error
FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error)
FindAddressByID(ctx context.Context, id string) (*model.Address, error)
UpdateAddress(ctx context.Context, address *model.Address) error
DeleteAddress(ctx context.Context, id string) error
}
type addressRepository struct {
db *gorm.DB
}
func NewAddressRepository(db *gorm.DB) AddressRepository {
return &addressRepository{db}
}
func (r *addressRepository) CreateAddress(ctx context.Context, address *model.Address) error {
return r.db.WithContext(ctx).Create(address).Error
}
func (r *addressRepository) FindAddressByUserID(ctx context.Context, userID string) ([]model.Address, error) {
var addresses []model.Address
err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&addresses).Error
if err != nil {
return nil, err
}
return addresses, nil
}
func (r *addressRepository) FindAddressByID(ctx context.Context, id string) (*model.Address, error) {
var address model.Address
err := r.db.WithContext(ctx).Where("id = ?", id).First(&address).Error
if err != nil {
return nil, err
}
return &address, nil
}
func (r *addressRepository) UpdateAddress(ctx context.Context, address *model.Address) error {
err := r.db.WithContext(ctx).Save(address).Error
if err != nil {
return err
}
return nil
}
func (r *addressRepository) DeleteAddress(ctx context.Context, id string) error {
err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Address{}).Error
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,24 @@
package address
import (
"rijig/config"
"rijig/internal/wilayahindo"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func AddressRouter(api fiber.Router) {
addressRepo := NewAddressRepository(config.DB)
wilayahRepo := wilayahindo.NewWilayahIndonesiaRepository(config.DB)
addressService := NewAddressService(addressRepo, wilayahRepo)
addressHandler := NewAddressHandler(addressService)
adddressAPI := api.Group("/user/address")
adddressAPI.Post("/create-address", middleware.AuthMiddleware(), addressHandler.CreateAddress)
adddressAPI.Get("/get-address", middleware.AuthMiddleware(), addressHandler.GetAddressByUserID)
adddressAPI.Get("/get-address/:address_id", middleware.AuthMiddleware(), addressHandler.GetAddressByID)
adddressAPI.Put("/update-address/:address_id", middleware.AuthMiddleware(), addressHandler.UpdateAddress)
adddressAPI.Delete("/delete-address/:address_id", middleware.AuthMiddleware(), addressHandler.DeleteAddress)
}

View File

@ -0,0 +1,249 @@
package address
import (
"context"
"errors"
"fmt"
"time"
"rijig/internal/wilayahindo"
"rijig/model"
"rijig/utils"
)
const (
cacheTTL = time.Hour * 24
userAddressesCacheKeyPattern = "user:%s:addresses"
addressCacheKeyPattern = "address:%s"
)
type AddressService interface {
CreateAddress(ctx context.Context, userID string, request CreateAddressDTO) (*AddressResponseDTO, error)
GetAddressByUserID(ctx context.Context, userID string) ([]AddressResponseDTO, error)
GetAddressByID(ctx context.Context, userID, id string) (*AddressResponseDTO, error)
UpdateAddress(ctx context.Context, userID, id string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error)
DeleteAddress(ctx context.Context, userID, id string) error
}
type addressService struct {
addressRepo AddressRepository
wilayahRepo wilayahindo.WilayahIndonesiaRepository
}
func NewAddressService(addressRepo AddressRepository, wilayahRepo wilayahindo.WilayahIndonesiaRepository) AddressService {
return &addressService{
addressRepo: addressRepo,
wilayahRepo: wilayahRepo,
}
}
func (s *addressService) validateWilayahIDs(ctx context.Context, addressDTO CreateAddressDTO) (string, string, string, string, error) {
province, _, err := s.wilayahRepo.FindProvinceByID(ctx, addressDTO.Province, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid province_id: %w", err)
}
regency, _, err := s.wilayahRepo.FindRegencyByID(ctx, addressDTO.Regency, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid regency_id: %w", err)
}
district, _, err := s.wilayahRepo.FindDistrictByID(ctx, addressDTO.District, 0, 0)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid district_id: %w", err)
}
village, err := s.wilayahRepo.FindVillageByID(ctx, addressDTO.Village)
if err != nil {
return "", "", "", "", fmt.Errorf("invalid village_id: %w", err)
}
return province.Name, regency.Name, district.Name, village.Name, nil
}
func (s *addressService) mapToResponseDTO(address *model.Address) *AddressResponseDTO {
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
return &AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Latitude: address.Latitude,
Longitude: address.Longitude,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}
func (s *addressService) invalidateAddressCaches(userID, addressID string) {
if addressID != "" {
addressCacheKey := fmt.Sprintf(addressCacheKeyPattern, addressID)
if err := utils.DeleteCache(addressCacheKey); err != nil {
fmt.Printf("Error deleting address cache: %v\n", err)
}
}
userCacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
if err := utils.DeleteCache(userCacheKey); err != nil {
fmt.Printf("Error deleting user addresses cache: %v\n", err)
}
}
func (s *addressService) cacheAddress(addressDTO *AddressResponseDTO) {
cacheKey := fmt.Sprintf(addressCacheKeyPattern, addressDTO.ID)
if err := utils.SetCache(cacheKey, addressDTO, cacheTTL); err != nil {
fmt.Printf("Error caching address to Redis: %v\n", err)
}
}
func (s *addressService) cacheUserAddresses(ctx context.Context, userID string) ([]AddressResponseDTO, error) {
addresses, err := s.addressRepo.FindAddressByUserID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to fetch addresses: %w", err)
}
var addressDTOs []AddressResponseDTO
for _, address := range addresses {
addressDTOs = append(addressDTOs, *s.mapToResponseDTO(&address))
}
cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
if err := utils.SetCache(cacheKey, addressDTOs, cacheTTL); err != nil {
fmt.Printf("Error caching addresses to Redis: %v\n", err)
}
return addressDTOs, nil
}
func (s *addressService) checkAddressOwnership(ctx context.Context, userID, addressID string) (*model.Address, error) {
address, err := s.addressRepo.FindAddressByID(ctx, addressID)
if err != nil {
return nil, fmt.Errorf("address not found: %w", err)
}
if address.UserID != userID {
return nil, errors.New("you are not authorized to access this address")
}
return address, nil
}
func (s *addressService) CreateAddress(ctx context.Context, userID string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error) {
provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO)
if err != nil {
return nil, err
}
address := model.Address{
UserID: userID,
Province: provinceName,
Regency: regencyName,
District: districtName,
Village: villageName,
PostalCode: addressDTO.PostalCode,
Detail: addressDTO.Detail,
Latitude: addressDTO.Latitude,
Longitude: addressDTO.Longitude,
}
if err := s.addressRepo.CreateAddress(ctx, &address); err != nil {
return nil, fmt.Errorf("failed to create address: %w", err)
}
responseDTO := s.mapToResponseDTO(&address)
s.cacheAddress(responseDTO)
s.invalidateAddressCaches(userID, "")
return responseDTO, nil
}
func (s *addressService) GetAddressByUserID(ctx context.Context, userID string) ([]AddressResponseDTO, error) {
cacheKey := fmt.Sprintf(userAddressesCacheKeyPattern, userID)
var cachedAddresses []AddressResponseDTO
if err := utils.GetCache(cacheKey, &cachedAddresses); err == nil {
return cachedAddresses, nil
}
return s.cacheUserAddresses(ctx, userID)
}
func (s *addressService) GetAddressByID(ctx context.Context, userID, id string) (*AddressResponseDTO, error) {
address, err := s.checkAddressOwnership(ctx, userID, id)
if err != nil {
return nil, err
}
cacheKey := fmt.Sprintf(addressCacheKeyPattern, id)
var cachedAddress AddressResponseDTO
if err := utils.GetCache(cacheKey, &cachedAddress); err == nil {
return &cachedAddress, nil
}
responseDTO := s.mapToResponseDTO(address)
s.cacheAddress(responseDTO)
return responseDTO, nil
}
func (s *addressService) UpdateAddress(ctx context.Context, userID, id string, addressDTO CreateAddressDTO) (*AddressResponseDTO, error) {
address, err := s.checkAddressOwnership(ctx, userID, id)
if err != nil {
return nil, err
}
provinceName, regencyName, districtName, villageName, err := s.validateWilayahIDs(ctx, addressDTO)
if err != nil {
return nil, err
}
address.Province = provinceName
address.Regency = regencyName
address.District = districtName
address.Village = villageName
address.PostalCode = addressDTO.PostalCode
address.Detail = addressDTO.Detail
address.Latitude = addressDTO.Latitude
address.Longitude = addressDTO.Longitude
if err := s.addressRepo.UpdateAddress(ctx, address); err != nil {
return nil, fmt.Errorf("failed to update address: %w", err)
}
responseDTO := s.mapToResponseDTO(address)
s.cacheAddress(responseDTO)
s.invalidateAddressCaches(userID, "")
return responseDTO, nil
}
func (s *addressService) DeleteAddress(ctx context.Context, userID, addressID string) error {
address, err := s.checkAddressOwnership(ctx, userID, addressID)
if err != nil {
return err
}
if err := s.addressRepo.DeleteAddress(ctx, addressID); err != nil {
return fmt.Errorf("failed to delete address: %w", err)
}
s.invalidateAddressCaches(address.UserID, addressID)
return nil
}

View File

@ -1,4 +1,4 @@
package dto
package article
import (
"strings"
@ -23,7 +23,7 @@ type RequestArticleDTO struct {
Content string `json:"content"`
}
func (r *RequestArticleDTO) Validate() (map[string][]string, bool) {
func (r *RequestArticleDTO) ValidateRequestArticleDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Title) == "" {

View File

@ -0,0 +1,141 @@
package article
import (
"mime/multipart"
"rijig/utils"
"strconv"
"github.com/gofiber/fiber/v2"
)
type ArticleHandler struct {
articleService ArticleService
}
func NewArticleHandler(articleService ArticleService) *ArticleHandler {
return &ArticleHandler{articleService}
}
func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
var request RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateRequestArticleDTO()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
coverImage, err := c.FormFile("coverImage")
if err != nil {
return utils.BadRequest(c, "Cover image is required")
}
articleResponse, err := h.articleService.CreateArticle(c.Context(), request, coverImage)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Article created successfully", articleResponse)
}
func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil || page < 0 {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil || limit < 0 {
limit = 0
}
articles, totalArticles, err := h.articleService.GetAllArticles(c.Context(), page, limit)
if err != nil {
return utils.InternalServerError(c, "Failed to fetch articles")
}
responseData := map[string]interface{}{
"articles": articles,
"total": int(totalArticles),
}
if page == 0 && limit == 0 {
return utils.SuccessWithData(c, "Articles fetched successfully", responseData)
}
return utils.SuccessWithPagination(c, "Articles fetched successfully", responseData, page, limit)
}
func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
article, err := h.articleService.GetArticleByID(c.Context(), id)
if err != nil {
return utils.NotFound(c, "Article not found")
}
return utils.SuccessWithData(c, "Article fetched successfully", article)
}
func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
var request RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Invalid request body", map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateRequestArticleDTO()
if !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
var coverImage *multipart.FileHeader
coverImage, err := c.FormFile("coverImage")
if err != nil && err.Error() != "no such file" && err.Error() != "there is no uploaded file associated with the given key" {
return utils.BadRequest(c, "Invalid cover image")
}
articleResponse, err := h.articleService.UpdateArticle(c.Context(), id, request, coverImage)
if err != nil {
if isNotFoundError(err) {
return utils.NotFound(c, err.Error())
}
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Article updated successfully", articleResponse)
}
func (h *ArticleHandler) DeleteArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.BadRequest(c, "Article ID is required")
}
err := h.articleService.DeleteArticle(c.Context(), id)
if err != nil {
if isNotFoundError(err) {
return utils.NotFound(c, err.Error())
}
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "Article deleted successfully")
}
func isNotFoundError(err error) bool {
return err != nil && (err.Error() == "article not found" ||
err.Error() == "failed to find article: record not found" ||
false)
}

View File

@ -0,0 +1,148 @@
package article
import (
"context"
"errors"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type ArticleRepository interface {
CreateArticle(ctx context.Context, article *model.Article) error
FindArticleByID(ctx context.Context, id string) (*model.Article, error)
FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error)
UpdateArticle(ctx context.Context, id string, article *model.Article) error
DeleteArticle(ctx context.Context, id string) error
ArticleExists(ctx context.Context, id string) (bool, error)
}
type articleRepository struct {
db *gorm.DB
}
func NewArticleRepository(db *gorm.DB) ArticleRepository {
return &articleRepository{db: db}
}
func (r *articleRepository) CreateArticle(ctx context.Context, article *model.Article) error {
if article == nil {
return errors.New("article cannot be nil")
}
if err := r.db.WithContext(ctx).Create(article).Error; err != nil {
return fmt.Errorf("failed to create article: %w", err)
}
return nil
}
func (r *articleRepository) FindArticleByID(ctx context.Context, id string) (*model.Article, error) {
if id == "" {
return nil, errors.New("article ID cannot be empty")
}
var article model.Article
err := r.db.WithContext(ctx).Where("id = ?", id).First(&article).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("article with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch article: %w", err)
}
return &article, nil
}
func (r *articleRepository) FindAllArticles(ctx context.Context, page, limit int) ([]model.Article, int64, error) {
var articles []model.Article
var total int64
if page < 0 || limit < 0 {
return nil, 0, errors.New("page and limit must be non-negative")
}
if err := r.db.WithContext(ctx).Model(&model.Article{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count articles: %w", err)
}
query := r.db.WithContext(ctx).Model(&model.Article{})
if page > 0 && limit > 0 {
offset := (page - 1) * limit
query = query.Offset(offset).Limit(limit)
}
if err := query.Find(&articles).Error; err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %w", err)
}
return articles, total, nil
}
func (r *articleRepository) UpdateArticle(ctx context.Context, id string, article *model.Article) error {
if id == "" {
return errors.New("article ID cannot be empty")
}
if article == nil {
return errors.New("article cannot be nil")
}
exists, err := r.ArticleExists(ctx, id)
if err != nil {
return fmt.Errorf("failed to check article existence: %w", err)
}
if !exists {
return fmt.Errorf("article with ID %s not found", id)
}
result := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Updates(article)
if result.Error != nil {
return fmt.Errorf("failed to update article: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected when updating article with ID %s", id)
}
return nil
}
func (r *articleRepository) DeleteArticle(ctx context.Context, id string) error {
if id == "" {
return errors.New("article ID cannot be empty")
}
exists, err := r.ArticleExists(ctx, id)
if err != nil {
return fmt.Errorf("failed to check article existence: %w", err)
}
if !exists {
return fmt.Errorf("article with ID %s not found", id)
}
result := r.db.WithContext(ctx).Where("id = ?", id).Delete(&model.Article{})
if result.Error != nil {
return fmt.Errorf("failed to delete article: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("no rows affected when deleting article with ID %s", id)
}
return nil
}
func (r *articleRepository) ArticleExists(ctx context.Context, id string) (bool, error) {
if id == "" {
return false, errors.New("article ID cannot be empty")
}
var count int64
err := r.db.WithContext(ctx).Model(&model.Article{}).Where("id = ?", id).Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed to check article existence: %w", err)
}
return count > 0, nil
}

View File

@ -0,0 +1,23 @@
package article
import (
"rijig/config"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func ArticleRouter(api fiber.Router) {
articleRepo := NewArticleRepository(config.DB)
articleService := NewArticleService(articleRepo)
articleHandler := NewArticleHandler(articleService)
articleAPI := api.Group("/article")
articleAPI.Post("/create", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.CreateArticle)
articleAPI.Get("/view", articleHandler.GetAllArticles)
articleAPI.Get("/view/:article_id", articleHandler.GetArticleByID)
articleAPI.Put("/update/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.UpdateArticle)
articleAPI.Delete("/delete/:article_id", middleware.AuthMiddleware(), middleware.RequireRoles(utils.RoleAdministrator), articleHandler.DeleteArticle)
}

View File

@ -0,0 +1,337 @@
package article
import (
"context"
"fmt"
"log"
"mime/multipart"
"os"
"path/filepath"
"time"
"rijig/model"
"rijig/utils"
"github.com/google/uuid"
)
type ArticleService interface {
CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error)
GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error)
UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error)
DeleteArticle(ctx context.Context, id string) error
}
type articleService struct {
articleRepo ArticleRepository
}
func NewArticleService(articleRepo ArticleRepository) ArticleService {
return &articleService{articleRepo}
}
func (s *articleService) transformToDTO(article *model.Article) (*ArticleResponseDTO, error) {
publishedAt, err := utils.FormatDateToIndonesianFormat(article.PublishedAt)
if err != nil {
publishedAt = ""
}
updatedAt, err := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
if err != nil {
updatedAt = ""
}
return &ArticleResponseDTO{
ID: article.ID,
Title: article.Title,
CoverImage: article.CoverImage,
Author: article.Author,
Heading: article.Heading,
Content: article.Content,
PublishedAt: publishedAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *articleService) transformToDTOs(articles []model.Article) ([]ArticleResponseDTO, error) {
var articleDTOs []ArticleResponseDTO
for _, article := range articles {
dto, err := s.transformToDTO(&article)
if err != nil {
return nil, fmt.Errorf("failed to transform article %s: %w", article.ID, err)
}
articleDTOs = append(articleDTOs, *dto)
}
return articleDTOs, nil
}
func (s *articleService) saveCoverArticle(coverArticle *multipart.FileHeader) (string, error) {
if coverArticle == nil {
return "", fmt.Errorf("cover image is required")
}
pathImage := "/uploads/articles/"
coverArticleDir := "./public" + os.Getenv("BASE_URL") + pathImage
if err := os.MkdirAll(coverArticleDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for cover article: %w", err)
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true, ".svg": true}
extension := filepath.Ext(coverArticle.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, .png, and .svg are allowed")
}
coverArticleFileName := fmt.Sprintf("%s_coverarticle%s", uuid.New().String(), extension)
coverArticlePath := filepath.Join(coverArticleDir, coverArticleFileName)
src, err := coverArticle.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %w", err)
}
defer src.Close()
dst, err := os.Create(coverArticlePath)
if err != nil {
return "", fmt.Errorf("failed to create cover article file: %w", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return "", fmt.Errorf("failed to save cover article: %w", err)
}
return fmt.Sprintf("%s%s", pathImage, coverArticleFileName), nil
}
func (s *articleService) deleteCoverArticle(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
log.Printf("Image file not found (already deleted?): %s", absolutePath)
return nil
}
if err := os.Remove(absolutePath); err != nil {
return fmt.Errorf("failed to delete image: %w", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *articleService) invalidateArticleCache(articleID string) {
articleCacheKey := fmt.Sprintf("article:%s", articleID)
if err := utils.DeleteCache(articleCacheKey); err != nil {
log.Printf("Error deleting article cache: %v", err)
}
if err := utils.ScanAndDelete("articles:*"); err != nil {
log.Printf("Error deleting articles cache: %v", err)
}
}
func (s *articleService) CreateArticle(ctx context.Context, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
coverArticlePath, err := s.saveCoverArticle(coverImage)
if err != nil {
return nil, fmt.Errorf("failed to save cover image: %w", err)
}
article := model.Article{
Title: request.Title,
CoverImage: coverArticlePath,
Author: request.Author,
Heading: request.Heading,
Content: request.Content,
}
if err := s.articleRepo.CreateArticle(ctx, &article); err != nil {
if deleteErr := s.deleteCoverArticle(coverArticlePath); deleteErr != nil {
log.Printf("Failed to clean up image after create failure: %v", deleteErr)
}
return nil, fmt.Errorf("failed to create article: %w", err)
}
articleDTO, err := s.transformToDTO(&article)
if err != nil {
return nil, fmt.Errorf("failed to transform article: %w", err)
}
cacheKey := fmt.Sprintf("article:%s", article.ID)
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching article: %v", err)
}
s.invalidateArticleCache("")
return articleDTO, nil
}
func (s *articleService) GetAllArticles(ctx context.Context, page, limit int) ([]ArticleResponseDTO, int64, error) {
var cacheKey string
if page <= 0 || limit <= 0 {
cacheKey = "articles:all"
} else {
cacheKey = fmt.Sprintf("articles:page:%d:limit:%d", page, limit)
}
type CachedArticlesData struct {
Articles []ArticleResponseDTO `json:"articles"`
Total int64 `json:"total"`
}
var cachedData CachedArticlesData
if err := utils.GetCache(cacheKey, &cachedData); err == nil {
return cachedData.Articles, cachedData.Total, nil
}
articles, total, err := s.articleRepo.FindAllArticles(ctx, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %w", err)
}
articleDTOs, err := s.transformToDTOs(articles)
if err != nil {
return nil, 0, fmt.Errorf("failed to transform articles: %w", err)
}
cacheData := CachedArticlesData{
Articles: articleDTOs,
Total: total,
}
if err := utils.SetCache(cacheKey, cacheData, time.Hour*24); err != nil {
log.Printf("Error caching articles: %v", err)
}
return articleDTOs, total, nil
}
func (s *articleService) GetArticleByID(ctx context.Context, id string) (*ArticleResponseDTO, error) {
if id == "" {
return nil, fmt.Errorf("article ID cannot be empty")
}
cacheKey := fmt.Sprintf("article:%s", id)
var cachedArticle ArticleResponseDTO
if err := utils.GetCache(cacheKey, &cachedArticle); err == nil {
return &cachedArticle, nil
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch article: %w", err)
}
articleDTO, err := s.transformToDTO(article)
if err != nil {
return nil, fmt.Errorf("failed to transform article: %w", err)
}
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching article: %v", err)
}
return articleDTO, nil
}
func (s *articleService) UpdateArticle(ctx context.Context, id string, request RequestArticleDTO, coverImage *multipart.FileHeader) (*ArticleResponseDTO, error) {
if id == "" {
return nil, fmt.Errorf("article ID cannot be empty")
}
existingArticle, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("article not found: %w", err)
}
oldCoverImage := existingArticle.CoverImage
var newCoverPath string
if coverImage != nil {
newCoverPath, err = s.saveCoverArticle(coverImage)
if err != nil {
return nil, fmt.Errorf("failed to save new cover image: %w", err)
}
}
updatedArticle := &model.Article{
Title: request.Title,
Author: request.Author,
Heading: request.Heading,
Content: request.Content,
CoverImage: existingArticle.CoverImage,
}
if newCoverPath != "" {
updatedArticle.CoverImage = newCoverPath
}
if err := s.articleRepo.UpdateArticle(ctx, id, updatedArticle); err != nil {
if newCoverPath != "" {
s.deleteCoverArticle(newCoverPath)
}
return nil, fmt.Errorf("failed to update article: %w", err)
}
if newCoverPath != "" && oldCoverImage != "" {
if err := s.deleteCoverArticle(oldCoverImage); err != nil {
log.Printf("Warning: failed to delete old cover image: %v", err)
}
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated article: %w", err)
}
articleDTO, err := s.transformToDTO(article)
if err != nil {
return nil, fmt.Errorf("failed to transform updated article: %w", err)
}
cacheKey := fmt.Sprintf("article:%s", id)
if err := utils.SetCache(cacheKey, articleDTO, time.Hour*24); err != nil {
log.Printf("Error caching updated article: %v", err)
}
s.invalidateArticleCache(id)
return articleDTO, nil
}
func (s *articleService) DeleteArticle(ctx context.Context, id string) error {
if id == "" {
return fmt.Errorf("article ID cannot be empty")
}
article, err := s.articleRepo.FindArticleByID(ctx, id)
if err != nil {
return fmt.Errorf("failed to find article: %w", err)
}
if err := s.articleRepo.DeleteArticle(ctx, id); err != nil {
return fmt.Errorf("failed to delete article: %w", err)
}
if err := s.deleteCoverArticle(article.CoverImage); err != nil {
log.Printf("Warning: failed to delete cover image: %v", err)
}
s.invalidateArticleCache(id)
return nil
}

View File

@ -0,0 +1,416 @@
package authentication
import (
"rijig/utils"
"strings"
"time"
)
type LoginorRegistRequest struct {
Phone string `json:"phone" validate:"required,min=10,max=15"`
RoleName string `json:"role_name"`
}
type VerifyOtpRequest struct {
DeviceID string `json:"device_id" validate:"required"`
RoleName string `json:"role_name" validate:"required,oneof=masyarakat pengepul pengelola"`
Phone string `json:"phone" validate:"required"`
Otp string `json:"otp" validate:"required,len=6"`
}
type CreatePINRequest struct {
PIN string `json:"pin" validate:"required,len=6,numeric"`
ConfirmPIN string `json:"confirm_pin" validate:"required,len=6,numeric"`
}
type VerifyPINRequest struct {
PIN string `json:"pin" validate:"required,len=6,numeric"`
DeviceID string `json:"device_id" validate:"required"`
}
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
DeviceID string `json:"device_id" validate:"required"`
UserID string `json:"user_id" validate:"required"`
}
type LogoutRequest struct {
DeviceID string `json:"device_id" validate:"required"`
}
type OTPResponse struct {
Message string `json:"message"`
ExpiresIn int `json:"expires_in"`
Phone string `json:"phone"`
}
type AuthResponse struct {
Message string `json:"message"`
AccessToken string `json:"access_token,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
User *UserResponse `json:"user,omitempty"`
RegistrationStatus string `json:"registration_status,omitempty"`
NextStep string `json:"next_step,omitempty"`
SessionID string `json:"session_id,omitempty"`
}
type UserResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
Email string `json:"email,omitempty"`
Role string `json:"role"`
RegistrationStatus string `json:"registration_status"`
RegistrationProgress int8 `json:"registration_progress"`
PhoneVerified bool `json:"phone_verified"`
Avatar *string `json:"avatar,omitempty"`
Gender string `json:"gender,omitempty"`
DateOfBirth string `json:"date_of_birth,omitempty"`
PlaceOfBirth string `json:"place_of_birth,omitempty"`
}
type RegistrationStatusResponse struct {
CurrentStep int `json:"current_step"`
TotalSteps int `json:"total_steps"`
CompletedSteps []RegistrationStep `json:"completed_steps"`
NextStep *RegistrationStep `json:"next_step,omitempty"`
RegistrationStatus string `json:"registration_status"`
IsComplete bool `json:"is_complete"`
RequiresApproval bool `json:"requires_approval"`
ApprovalMessage string `json:"approval_message,omitempty"`
}
type RegistrationStep struct {
StepNumber int `json:"step_number"`
Title string `json:"title"`
Description string `json:"description"`
IsRequired bool `json:"is_required"`
IsCompleted bool `json:"is_completed"`
IsActive bool `json:"is_active"`
}
type OTPData struct {
Phone string `json:"phone"`
OTP string `json:"otp"`
UserID string `json:"user_id,omitempty"`
Role string `json:"role"`
RoleID string `json:"role_id,omitempty"`
Type string `json:"type"`
ExpiresAt time.Time `json:"expires_at"`
Attempts int `json:"attempts"`
}
type SessionData struct {
UserID string `json:"user_id"`
DeviceID string `json:"device_id"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
LastSeen time.Time `json:"last_seen"`
IsActive bool `json:"is_active"`
}
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code string `json:"code,omitempty"`
Details interface{} `json:"details,omitempty"`
}
type ValidationErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Fields map[string]string `json:"fields"`
}
type ApproveRegistrationRequest struct {
UserID string `json:"user_id" validate:"required"`
Message string `json:"message,omitempty"`
}
type RejectRegistrationRequest struct {
UserID string `json:"user_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
}
type PendingRegistrationResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
RegistrationData RegistrationData `json:"registration_data"`
SubmittedAt time.Time `json:"submitted_at"`
DocumentsUploaded []DocumentInfo `json:"documents_uploaded"`
}
type RegistrationData struct {
KTPNumber string `json:"ktp_number,omitempty"`
KTPImage string `json:"ktp_image,omitempty"`
FullName string `json:"full_name,omitempty"`
Address string `json:"address,omitempty"`
BusinessName string `json:"business_name,omitempty"`
BusinessType string `json:"business_type,omitempty"`
BusinessAddress string `json:"business_address,omitempty"`
BusinessPhone string `json:"business_phone,omitempty"`
TaxNumber string `json:"tax_number,omitempty"`
BusinessLicense string `json:"business_license,omitempty"`
}
type DocumentInfo struct {
Type string `json:"type"`
FileName string `json:"file_name"`
UploadedAt time.Time `json:"uploaded_at"`
Status string `json:"status"`
FileSize int64 `json:"file_size"`
ContentType string `json:"content_type"`
}
type AuthStatsResponse struct {
TotalUsers int64 `json:"total_users"`
ActiveUsers int64 `json:"active_users"`
PendingRegistrations int64 `json:"pending_registrations"`
UsersByRole map[string]int64 `json:"users_by_role"`
RegistrationStats RegistrationStatsData `json:"registration_stats"`
LoginStats LoginStatsData `json:"login_stats"`
}
type RegistrationStatsData struct {
TotalRegistrations int64 `json:"total_registrations"`
CompletedToday int64 `json:"completed_today"`
CompletedThisWeek int64 `json:"completed_this_week"`
CompletedThisMonth int64 `json:"completed_this_month"`
PendingApproval int64 `json:"pending_approval"`
RejectedRegistrations int64 `json:"rejected_registrations"`
}
type LoginStatsData struct {
TotalLogins int64 `json:"total_logins"`
LoginsToday int64 `json:"logins_today"`
LoginsThisWeek int64 `json:"logins_this_week"`
LoginsThisMonth int64 `json:"logins_this_month"`
UniqueUsersToday int64 `json:"unique_users_today"`
UniqueUsersWeek int64 `json:"unique_users_week"`
UniqueUsersMonth int64 `json:"unique_users_month"`
}
type PaginationRequest struct {
Page int `json:"page" query:"page" validate:"min=1"`
Limit int `json:"limit" query:"limit" validate:"min=1,max=100"`
Sort string `json:"sort" query:"sort"`
Order string `json:"order" query:"order" validate:"oneof=asc desc"`
Search string `json:"search" query:"search"`
Filter string `json:"filter" query:"filter"`
}
type PaginationResponse struct {
Page int `json:"page"`
Limit int `json:"limit"`
Total int64 `json:"total"`
TotalPages int `json:"total_pages"`
HasNext bool `json:"has_next"`
HasPrev bool `json:"has_prev"`
}
type PaginatedResponse struct {
Data interface{} `json:"data"`
Pagination PaginationResponse `json:"pagination"`
}
type SMSWebhookRequest struct {
MessageID string `json:"message_id"`
Phone string `json:"phone"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
}
type RateLimitInfo struct {
Limit int `json:"limit"`
Remaining int `json:"remaining"`
ResetTime time.Time `json:"reset_time"`
RetryAfter time.Duration `json:"retry_after,omitempty"`
}
type StepResponse struct {
UserID string `json:"user_id"`
Role string `json:"role"`
RegistrationStatus string `json:"registration_status"`
RegistrationProgress int `json:"registration_progress"`
NextStep string `json:"next_step"`
}
type RegisterAdminRequest struct {
Name string `json:"name"`
Gender string `json:"gender"`
DateOfBirth string `json:"dateofbirth"`
PlaceOfBirth string `json:"placeofbirth"`
Phone string `json:"phone"`
Email string `json:"email"`
Password string `json:"password"`
PasswordConfirm string `json:"password_confirm"`
}
type LoginAdminRequest struct {
Email string `json:"email"`
Password string `json:"password"`
DeviceID string `json:"device_id"`
}
type VerifyAdminOTPRequest struct {
Email string `json:"email" validate:"required,email"`
OTP string `json:"otp" validate:"required,len=6,numeric"`
DeviceID string `json:"device_id" validate:"required"`
}
type ResendAdminOTPRequest struct {
Email string `json:"email" validate:"required,email"`
}
type OTPAdminResponse struct {
Message string `json:"message"`
Email string `json:"email"`
ExpiresIn time.Duration `json:"expires_in_seconds"`
RemainingTime string `json:"remaining_time"`
CanResend bool `json:"can_resend"`
MaxAttempts int `json:"max_attempts"`
}
type ForgotPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
}
type ResetPasswordRequest struct {
Email string `json:"email" validate:"required,email"`
Token string `json:"token" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
type ResetPasswordResponse struct {
Message string `json:"message"`
Email string `json:"email"`
ExpiresIn time.Duration `json:"expires_in_seconds"`
RemainingTime string `json:"remaining_time"`
}
type VerifyEmailRequest struct {
Email string `json:"email" validate:"required,email"`
Token string `json:"token" validate:"required"`
}
type ResendVerificationRequest struct {
Email string `json:"email" validate:"required,email"`
}
type EmailVerificationResponse struct {
Message string `json:"message"`
Email string `json:"email"`
ExpiresIn time.Duration `json:"expires_in_seconds"`
RemainingTime string `json:"remaining_time"`
}
func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *VerifyOtpRequest) ValidateVerifyOtpRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if len(strings.TrimSpace(r.DeviceID)) < 10 {
errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters")
}
validRoles := map[string]bool{"masyarakat": true, "pengepul": true, "pengelola": true}
if _, ok := validRoles[r.RoleName]; !ok {
errors["role"] = append(errors["role"], "Role tidak valid, hanya masyarakat, pengepul, atau pengelola")
}
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if len(r.Otp) != 4 || !utils.IsNumeric(r.Otp) {
errors["otp"] = append(errors["otp"], "OTP must be 4-digit number")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *LoginAdminRequest) ValidateLoginAdminRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if !utils.IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if strings.TrimSpace(r.Password) == "" {
errors["password"] = append(errors["password"], "Password is required")
}
if len(strings.TrimSpace(r.DeviceID)) < 10 {
errors["device_id"] = append(errors["device_id"], "Device ID must be at least 10 characters")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *RegisterAdminRequest) ValidateRegisterAdminRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.Name) == "" {
errors["name"] = append(errors["name"], "Name is required")
}
genderLower := strings.ToLower(strings.TrimSpace(r.Gender))
if genderLower != "laki-laki" && genderLower != "perempuan" {
errors["gender"] = append(errors["gender"], "Gender must be either 'laki-laki' or 'perempuan'")
}
if strings.TrimSpace(r.DateOfBirth) == "" {
errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth is required")
} else {
_, err := time.Parse("02-01-2006", r.DateOfBirth)
if err != nil {
errors["dateofbirth"] = append(errors["dateofbirth"], "Date of birth must be in DD-MM-YYYY format")
}
}
if strings.TrimSpace(r.PlaceOfBirth) == "" {
errors["placeofbirth"] = append(errors["placeofbirth"], "Place of birth is required")
}
if !utils.IsValidPhoneNumber(r.Phone) {
errors["phone"] = append(errors["phone"], "Phone must be valid, has 8-14 digit and start with '62..'")
}
if !utils.IsValidEmail(r.Email) {
errors["email"] = append(errors["email"], "Invalid email format")
}
if !utils.IsValidPassword(r.Password) {
errors["password"] = append(errors["password"], "Password must be at least 8 characters, with uppercase, number, and special character")
}
if r.Password != r.PasswordConfirm {
errors["password_confirm"] = append(errors["password_confirm"], "Passwords do not match")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,351 @@
package authentication
import (
"log"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type AuthenticationHandler struct {
service AuthenticationService
}
func NewAuthenticationHandler(service AuthenticationService) *AuthenticationHandler {
return &AuthenticationHandler{service}
}
func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
if claims.DeviceID == "" {
return utils.BadRequest(c, "Device ID is required")
}
var body struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.BodyParser(&body); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if body.RefreshToken == "" {
return utils.BadRequest(c, "Refresh token is required")
}
if claims.UserID == "" {
return utils.BadRequest(c, "userid is required")
}
tokenData, err := utils.RefreshAccessToken(claims.UserID, claims.DeviceID, body.RefreshToken)
if err != nil {
return utils.Unauthorized(c, err.Error())
}
return utils.SuccessWithData(c, "Token refreshed successfully", tokenData)
}
func (h *AuthenticationHandler) GetMe(c *fiber.Ctx) error {
userID, _ := c.Locals("user_id").(string)
role, _ := c.Locals("role").(string)
deviceID, _ := c.Locals("device_id").(string)
regStatus, _ := c.Locals("registration_status").(string)
data := fiber.Map{
"user_id": userID,
"role": role,
"device_id": deviceID,
"registration_status": regStatus,
}
return utils.SuccessWithData(c, "User session data retrieved", data)
}
func (h *AuthenticationHandler) GetRegistrationStatus(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "unauthorized access")
}
res, err := h.service.GetRegistrationStatus(c.Context(), claims.UserID, claims.DeviceID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Registration status retrieved successfully", res)
}
func (h *AuthenticationHandler) Login(c *fiber.Ctx) error {
var req LoginAdminRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginAdminRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Validation failed",
},
"errors": errs,
})
}
res, err := h.service.LoginAdmin(c.Context(), &req)
if err != nil {
return utils.Unauthorized(c, err.Error())
}
return utils.SuccessWithData(c, "Login successful", res)
}
func (h *AuthenticationHandler) RegisterAdmin(c *fiber.Ctx) error {
var req RegisterAdminRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
response, err := h.service.RegisterAdmin(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "Admin registered successfully", response)
}
// POST /auth/admin/verify-email - Verify email dari registration
func (h *AuthenticationHandler) VerifyEmail(c *fiber.Ctx) error {
var req VerifyEmailRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
err := h.service.VerifyEmail(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "Email berhasil diverifikasi. Sekarang Anda dapat login", nil)
}
// POST /auth/admin/resend-verification - Resend verification email
func (h *AuthenticationHandler) ResendEmailVerification(c *fiber.Ctx) error {
var req ResendVerificationRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
response, err := h.service.ResendEmailVerification(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "Verification email resent", response)
}
func (h *AuthenticationHandler) VerifyAdminOTP(c *fiber.Ctx) error {
var req VerifyAdminOTPRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if errs, ok := req.Valida(); !ok {
// return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
// "meta": fiber.Map{
// "status": fiber.StatusBadRequest,
// "message": "periksa lagi inputan",
// },
// "errors": errs,
// })
// }
response, err := h.service.VerifyAdminOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP resent successfully", response)
}
// POST /auth/admin/resend-otp - Resend OTP
func (h *AuthenticationHandler) ResendAdminOTP(c *fiber.Ctx) error {
var req ResendAdminOTPRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
response, err := h.service.ResendAdminOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP resent successfully", response)
}
func (h *AuthenticationHandler) ForgotPassword(c *fiber.Ctx) error {
var req ForgotPasswordRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
response, err := h.service.ForgotPassword(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "Reset password email sent", response)
}
// POST /auth/admin/reset-password - Step 2: Reset password dengan token
func (h *AuthenticationHandler) ResetPassword(c *fiber.Ctx) error {
var req ResetPasswordRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
// if err := h.validator.Struct(&req); err != nil {
// return utils.BadRequest(c, "Validation failed: "+err.Error())
// }
err := h.service.ResetPassword(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "Password berhasil direset", nil)
}
func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error {
var req LoginorRegistRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginorRegistRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Input tidak valid",
},
"errors": errs,
})
}
_, err := h.service.SendLoginOTP(c.Context(), &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "OTP sent successfully")
}
func (h *AuthenticationHandler) VerifyOtpHandler(c *fiber.Ctx) error {
var req VerifyOtpRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if errs, ok := req.ValidateVerifyOtpRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"},
"errors": errs,
})
}
stepResp, err := h.service.VerifyLoginOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP verified successfully", stepResp)
}
func (h *AuthenticationHandler) RequestOtpRegistHandler(c *fiber.Ctx) error {
var req LoginorRegistRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if errs, ok := req.ValidateLoginorRegistRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{
"status": fiber.StatusBadRequest,
"message": "Input tidak valid",
},
"errors": errs,
})
}
_, err := h.service.SendRegistrationOTP(c.Context(), &req)
if err != nil {
return utils.Forbidden(c, err.Error())
}
return utils.Success(c, "OTP sent successfully")
}
func (h *AuthenticationHandler) VerifyOtpRegistHandler(c *fiber.Ctx) error {
var req VerifyOtpRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request body")
}
if errs, ok := req.ValidateVerifyOtpRequest(); !ok {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"meta": fiber.Map{"status": fiber.StatusBadRequest, "message": "Validation error"},
"errors": errs,
})
}
stepResp, err := h.service.VerifyRegistrationOTP(c.Context(), &req)
if err != nil {
return utils.BadRequest(c, err.Error())
}
return utils.SuccessWithData(c, "OTP verified successfully", stepResp)
}
func (h *AuthenticationHandler) LogoutAuthentication(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
err = h.service.LogoutAuthentication(c.Context(), claims.UserID, claims.DeviceID)
if err != nil {
return utils.InternalServerError(c, "Failed to logout")
}
return utils.Success(c, "Logout successful")
}

View File

@ -0,0 +1,125 @@
package authentication
import (
"context"
"fmt"
"log"
"rijig/model"
"gorm.io/gorm"
)
type AuthenticationRepository interface {
FindUserByPhone(ctx context.Context, phone string) (*model.User, error)
FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error)
FindUserByEmail(ctx context.Context, email string) (*model.User, error)
FindUserByID(ctx context.Context, userID string) (*model.User, error)
CreateUser(ctx context.Context, user *model.User) error
UpdateUser(ctx context.Context, user *model.User) error
PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error
GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error)
GetCompanyProfilesByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.CompanyProfile, error)
}
type authenticationRepository struct {
db *gorm.DB
}
func NewAuthenticationRepository(db *gorm.DB) AuthenticationRepository {
return &authenticationRepository{db}
}
func (r *authenticationRepository) FindUserByPhone(ctx context.Context, phone string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Where("phone = ?", phone).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByPhoneAndRole(ctx context.Context, phone, rolename string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Joins("JOIN roles AS role ON role.id = users.role_id").
Where("users.phone = ? AND role.role_name = ?", phone, rolename).
First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByEmail(ctx context.Context, email string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
Where("email = ?", email).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) FindUserByID(ctx context.Context, userID string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).
Preload("Role").
First(&user, "id = ?", userID).Error; err != nil {
return nil, err
}
return &user, nil
}
func (r *authenticationRepository) CreateUser(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *authenticationRepository) UpdateUser(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", user.ID).
Updates(user).Error
}
func (r *authenticationRepository) PatchUser(ctx context.Context, userID string, updates map[string]interface{}) error {
return r.db.WithContext(ctx).
Model(&model.User{}).
Where("id = ?", userID).
Updates(updates).Error
}
func (r *authenticationRepository) GetIdentityCardsByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.IdentityCard, error) {
var identityCards []model.IdentityCard
if err := r.db.WithContext(ctx).
Joins("JOIN users ON identity_cards.user_id = users.id").
Where("users.registration_status = ?", userRegStatus).
Preload("User").
Preload("User.Role").
Find(&identityCards).Error; err != nil {
log.Printf("Error fetching identity cards by user registration status: %v", err)
return nil, fmt.Errorf("error fetching identity cards by user registration status: %w", err)
}
log.Printf("Found %d identity cards with registration status: %s", len(identityCards), userRegStatus)
return identityCards, nil
}
func (r *authenticationRepository) GetCompanyProfilesByUserRegStatus(ctx context.Context, userRegStatus string) ([]model.CompanyProfile, error) {
var companyProfiles []model.CompanyProfile
if err := r.db.WithContext(ctx).
Joins("JOIN users ON company_profiles.user_id = users.id").
Where("users.registration_status = ?", userRegStatus).
Preload("User").
Preload("User.Role").
Find(&companyProfiles).Error; err != nil {
log.Printf("Error fetching company profiles by user registration status: %v", err)
return nil, fmt.Errorf("error fetching company profiles by user registration status: %w", err)
}
log.Printf("Found %d company profiles with registration status: %s", len(companyProfiles), userRegStatus)
return companyProfiles, nil
}

View File

@ -0,0 +1,51 @@
package authentication
import (
"rijig/config"
"rijig/internal/role"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func AuthenticationRouter(api fiber.Router) {
repoAuth := NewAuthenticationRepository(config.DB)
repoRole := role.NewRoleRepository(config.DB)
authService := NewAuthenticationService(repoAuth, repoRole)
authHandler := NewAuthenticationHandler(authService)
authRoute := api.Group("/auth")
authRoute.Post("/refresh-token",
middleware.AuthMiddleware(),
middleware.DeviceValidation(),
authHandler.RefreshToken,
)
// authRoute.Get("/me",
// middleware.AuthMiddleware(),
// middleware.CheckRefreshTokenTTL(30*time.Second),
// middleware.RequireApprovedRegistration(),
// authHandler.GetMe,
// )
authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus)
authRoute.Post("/login/admin", authHandler.Login)
authRoute.Post("/register/admin", authHandler.RegisterAdmin)
authRoute.Post("/verify-email", authHandler.VerifyEmail)
authRoute.Post("/resend-verification", authHandler.ResendEmailVerification)
authRoute.Post("/verify-otp-admin", authHandler.VerifyAdminOTP)
authRoute.Post("/resend-otp-admin", authHandler.ResendAdminOTP)
authRoute.Post("/forgot-password", authHandler.ForgotPassword)
authRoute.Post("/reset-password", authHandler.ResetPassword)
authRoute.Post("/request-otp", authHandler.RequestOtpHandler)
authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler)
authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler)
authRoute.Post("/verif-otp/register", authHandler.VerifyOtpRegistHandler)
authRoute.Post("/logout", middleware.AuthMiddleware(), authHandler.LogoutAuthentication)
}

View File

@ -0,0 +1,814 @@
package authentication
import (
"context"
"fmt"
"log"
"strings"
"time"
"rijig/internal/role"
"rijig/model"
"rijig/utils"
)
type AuthenticationService interface {
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error)
RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error)
VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error)
ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error)
ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error)
ResetPassword(ctx context.Context, req *ResetPasswordRequest) error
VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error
ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error)
SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
LogoutAuthentication(ctx context.Context, userID, deviceID string) error
}
type authenticationService struct {
authRepo AuthenticationRepository
roleRepo role.RoleRepository
emailService *utils.EmailService
}
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
return &authenticationService{
authRepo: authRepo,
roleRepo: roleRepo,
emailService: utils.NewEmailService(),
}
}
// func normalizeRoleName(roleName string) string {
// switch strings.ToLower(roleName) {
// case "administrator", "admin":
// return utils.RoleAdministrator
// case "pengelola":
// return utils.RolePengelola
// case "pengepul":
// return utils.RolePengepul
// case "masyarakat":
// return utils.RoleMasyarakat
// default:
// return strings.ToLower(roleName)
// }
// }
type GetRegistrationStatusResponse struct {
UserID string `json:"userId"`
RegistrationStatus string `json:"registrationStatus"`
RegistrationProgress int8 `json:"registrationProgress"`
Name string `json:"name"`
Phone string `json:"phone"`
Role string `json:"role"`
}
func (s *authenticationService) GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error) {
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
if user.Role.RoleName == "" {
return nil, fmt.Errorf("user role not found")
}
if user.RegistrationStatus == utils.RegStatusPending {
log.Printf("⏳ User %s (%s) registration is still pending approval", user.Name, user.Phone)
return &AuthResponse{
Message: "Your registration is currently under review. Please wait for approval.",
RegistrationStatus: user.RegistrationStatus,
NextStep: "wait_for_approval",
}, nil
}
if user.RegistrationStatus == utils.RegStatusConfirmed || user.RegistrationStatus == utils.RegStatusRejected {
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
user.Role.RoleName,
deviceID,
user.RegistrationStatus,
int(user.RegistrationProgress),
)
if err != nil {
log.Printf("GenerateTokenPair error: %v", err)
return nil, fmt.Errorf("failed to generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(
user.Role.RoleName,
int(user.RegistrationProgress),
user.RegistrationStatus,
)
var message string
if user.RegistrationStatus == utils.RegStatusConfirmed {
message = "Registration approved successfully"
log.Printf("✅ User %s (%s) registration approved - generating tokens", user.Name, user.Phone)
} else if user.RegistrationStatus == utils.RegStatusRejected {
message = "Registration has been rejected"
log.Printf("❌ User %s (%s) registration rejected - generating tokens for rejection flow", user.Name, user.Phone)
}
return &AuthResponse{
Message: message,
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: user.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
return nil, fmt.Errorf("unsupported registration status: %s", user.RegistrationStatus)
}
func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error) {
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("invalid credentials")
}
if user.Role == nil || user.Role.RoleName != "administrator" {
return nil, fmt.Errorf("invalid credentials")
}
if user.RegistrationStatus != "completed" {
return nil, fmt.Errorf("account not activated")
}
if !utils.CompareHashAndPlainText(user.Password, req.Password) {
return nil, fmt.Errorf("invalid credentials")
}
if utils.IsOTPValid(req.Email) {
remaining, _ := utils.GetOTPRemainingTime(req.Email)
return &OTPAdminResponse{
Message: "OTP sudah dikirim sebelumnya",
Email: req.Email,
ExpiresIn: remaining,
RemainingTime: formatDuration(remaining),
CanResend: false,
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
}, nil
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("failed to generate OTP")
}
if err := utils.StoreOTP(req.Email, otp); err != nil {
return nil, fmt.Errorf("failed to store OTP")
}
if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil {
log.Printf("Failed to send OTP email: %v", err)
return nil, fmt.Errorf("failed to send OTP email")
}
return &OTPAdminResponse{
Message: "Kode OTP berhasil dikirim ke email Anda",
Email: req.Email,
ExpiresIn: utils.OTP_EXPIRY,
RemainingTime: formatDuration(utils.OTP_EXPIRY),
CanResend: false,
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
}, nil
}
func (s *authenticationService) VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error) {
if err := utils.ValidateOTP(req.Email, req.OTP); err != nil {
return nil, err
}
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("user not found")
}
if !user.EmailVerified {
user.EmailVerified = true
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
log.Printf("Failed to update email verification status: %v", err)
}
}
token, err := utils.GenerateTokenPair(
user.ID,
user.Role.RoleName,
req.DeviceID,
user.RegistrationStatus,
int(user.RegistrationProgress),
)
if err != nil {
return nil, fmt.Errorf("failed to generate token: %w", err)
}
return &AuthResponse{
Message: "Login berhasil",
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
RegistrationStatus: user.RegistrationStatus,
SessionID: token.SessionID,
}, nil
}
func (s *authenticationService) ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error) {
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("email not found")
}
if user.Role == nil || user.Role.RoleName != "administrator" {
return nil, fmt.Errorf("not authorized")
}
if utils.IsOTPValid(req.Email) {
remaining, _ := utils.GetOTPRemainingTime(req.Email)
return nil, fmt.Errorf("OTP masih berlaku. Tunggu %s untuk kirim ulang", formatDuration(remaining))
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("failed to generate OTP")
}
if err := utils.StoreOTP(req.Email, otp); err != nil {
return nil, fmt.Errorf("failed to store OTP")
}
if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil {
log.Printf("Failed to send OTP email: %v", err)
return nil, fmt.Errorf("failed to send OTP email")
}
return &OTPAdminResponse{
Message: "Kode OTP baru berhasil dikirim",
Email: req.Email,
ExpiresIn: utils.OTP_EXPIRY,
RemainingTime: formatDuration(utils.OTP_EXPIRY),
CanResend: false,
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
}, nil
}
func (s *authenticationService) VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error {
verificationData, err := utils.ValidateEmailVerificationToken(req.Email, req.Token)
if err != nil {
return err
}
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return fmt.Errorf("user not found")
}
if user.ID != verificationData.UserID {
return fmt.Errorf("invalid verification token")
}
if user.EmailVerified {
return fmt.Errorf("email sudah terverifikasi sebelumnya")
}
user.EmailVerified = true
user.RegistrationStatus = utils.RegStatusComplete
// user.RegistrationProgress = 3
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
return fmt.Errorf("failed to update user verification status: %w", err)
}
if err := utils.MarkEmailVerificationTokenAsUsed(req.Email); err != nil {
log.Printf("Failed to mark verification token as used: %v", err)
}
return nil
}
func (s *authenticationService) ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error) {
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return nil, fmt.Errorf("email not found")
}
if user.Role == nil || user.Role.RoleName != "administrator" {
return nil, fmt.Errorf("not authorized")
}
if user.EmailVerified {
return nil, fmt.Errorf("email sudah terverifikasi")
}
if utils.IsEmailVerificationTokenValid(req.Email) {
remaining, _ := utils.GetEmailVerificationTokenRemainingTime(req.Email)
return &EmailVerificationResponse{
Message: "Email verifikasi sudah dikirim sebelumnya",
Email: req.Email,
ExpiresIn: remaining,
RemainingTime: formatDuration(remaining),
}, nil
}
token, err := utils.GenerateEmailVerificationToken()
if err != nil {
return nil, fmt.Errorf("failed to generate verification token")
}
if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil {
return nil, fmt.Errorf("failed to store verification token")
}
if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil {
log.Printf("Failed to send verification email: %v", err)
return nil, fmt.Errorf("failed to send verification email")
}
return &EmailVerificationResponse{
Message: "Email verifikasi berhasil dikirim ulang",
Email: req.Email,
ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY,
RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY),
}, nil
}
func (s *authenticationService) ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error) {
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return &ResetPasswordResponse{
Message: "Jika email terdaftar, link reset password akan dikirim",
Email: req.Email,
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
}, nil
}
if user.Role == nil || user.Role.RoleName != "administrator" {
return &ResetPasswordResponse{
Message: "Jika email terdaftar, link reset password akan dikirim",
Email: req.Email,
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
}, nil
}
if utils.IsResetTokenValid(req.Email) {
remaining, _ := utils.GetResetTokenRemainingTime(req.Email)
return &ResetPasswordResponse{
Message: "Link reset password sudah dikirim sebelumnya",
Email: req.Email,
ExpiresIn: remaining,
RemainingTime: formatDuration(remaining),
}, nil
}
token, err := utils.GenerateResetToken()
if err != nil {
return nil, fmt.Errorf("failed to generate reset token")
}
if err := utils.StoreResetToken(req.Email, user.ID, token); err != nil {
return nil, fmt.Errorf("failed to store reset token")
}
if err := s.emailService.SendResetPasswordEmail(req.Email, user.Name, token); err != nil {
log.Printf("Failed to send reset password email: %v", err)
return nil, fmt.Errorf("failed to send reset password email")
}
return &ResetPasswordResponse{
Message: "Link reset password berhasil dikirim ke email Anda",
Email: req.Email,
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
}, nil
}
func (s *authenticationService) ResetPassword(ctx context.Context, req *ResetPasswordRequest) error {
resetData, err := utils.ValidateResetToken(req.Email, req.Token)
if err != nil {
return err
}
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
if err != nil {
return fmt.Errorf("user not found")
}
if user.ID != resetData.UserID {
return fmt.Errorf("invalid reset token")
}
hashedPassword, err := utils.HashingPlainText(req.NewPassword)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
user.Password = hashedPassword
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
return fmt.Errorf("failed to update password: %w", err)
}
if err := utils.MarkResetTokenAsUsed(req.Email); err != nil {
log.Printf("Failed to mark reset token as used: %v", err)
}
if err := utils.RevokeAllRefreshTokens(user.ID); err != nil {
log.Printf("Failed to revoke refresh tokens: %v", err)
}
return nil
}
func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error) {
existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email)
if existingUser != nil {
return nil, fmt.Errorf("email already in use")
}
hashedPassword, err := utils.HashingPlainText(req.Password)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
role, err := s.roleRepo.FindRoleByName(ctx, "administrator")
if err != nil {
return nil, fmt.Errorf("role name not found: %w", err)
}
user := &model.User{
Name: req.Name,
Phone: req.Phone,
Email: req.Email,
Gender: req.Gender,
Dateofbirth: req.DateOfBirth,
Placeofbirth: req.PlaceOfBirth,
Password: hashedPassword,
RoleID: role.ID,
RegistrationStatus: "pending_email_verification",
RegistrationProgress: 1,
EmailVerified: false,
}
if err := s.authRepo.CreateUser(ctx, user); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
token, err := utils.GenerateEmailVerificationToken()
if err != nil {
return nil, fmt.Errorf("failed to generate verification token")
}
if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil {
return nil, fmt.Errorf("failed to store verification token")
}
if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil {
log.Printf("Failed to send verification email: %v", err)
return nil, fmt.Errorf("failed to send verification email")
}
return &EmailVerificationResponse{
Message: "Admin berhasil didaftarkan. Silakan cek email untuk verifikasi",
Email: req.Email,
ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY,
RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY),
}, nil
}
func formatDuration(d time.Duration) string {
minutes := int(d.Minutes())
seconds := int(d.Seconds()) % 60
return fmt.Sprintf("%d:%02d", minutes, seconds)
}
func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
normalizedRole := strings.ToLower(req.RoleName)
existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole)
if err == nil && existingUser != nil {
return nil, fmt.Errorf("nomor telepon dengan role %s sudah terdaftar", req.RoleName)
}
roleData, err := s.roleRepo.FindRoleByName(ctx, normalizedRole)
if err != nil {
return nil, fmt.Errorf("role tidak valid: %v", err)
}
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("gagal generate OTP: %v", err)
}
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
otpData := OTPData{
Phone: req.Phone,
OTP: otp,
Role: normalizedRole,
RoleID: roleData.ID,
Type: "register",
Attempts: 0,
ExpiresAt: time.Now().Add(90 * time.Second),
}
err = utils.SetCacheWithTTL(otpKey, otpData, 90*time.Second)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
}
err = sendOTP(req.Phone, otp)
if err != nil {
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
}
return &OTPResponse{
Message: "OTP berhasil dikirim",
ExpiresIn: 90,
Phone: maskPhoneNumber(req.Phone),
}, nil
}
func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
var otpData OTPData
err := utils.GetCache(otpKey, &otpData)
if err != nil {
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
}
if otpData.Attempts >= 3 {
utils.DeleteCache(otpKey)
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
}
if otpData.OTP != req.Otp {
otpData.Attempts++
utils.SetCacheWithTTL(otpKey, otpData, time.Until(otpData.ExpiresAt))
return nil, fmt.Errorf("kode OTP salah")
}
if otpData.Role != req.RoleName {
return nil, fmt.Errorf("role tidak sesuai")
}
normalizedRole := strings.ToLower(req.RoleName)
user := &model.User{
Phone: req.Phone,
PhoneVerified: true,
RoleID: otpData.RoleID,
RegistrationStatus: utils.RegStatusIncomplete,
RegistrationProgress: utils.ProgressOTPVerified,
Name: "",
Gender: "",
Dateofbirth: "",
Placeofbirth: "",
}
err = s.authRepo.CreateUser(ctx, user)
if err != nil {
return nil, fmt.Errorf("gagal membuat user: %v", err)
}
if user.ID == "" {
return nil, fmt.Errorf("gagal mendapatkan user ID setelah registrasi")
}
utils.DeleteCache(otpKey)
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
normalizedRole,
req.DeviceID,
user.RegistrationStatus,
int(user.RegistrationProgress),
)
if err != nil {
return nil, fmt.Errorf("gagal generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(
normalizedRole,
int(user.RegistrationProgress),
user.RegistrationStatus,
)
return &AuthResponse{
Message: "Registrasi berhasil",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: user.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
user, err := s.authRepo.FindUserByPhone(ctx, req.Phone)
if err != nil {
return nil, fmt.Errorf("nomor telepon tidak terdaftar")
}
if !user.PhoneVerified {
return nil, fmt.Errorf("nomor telepon belum diverifikasi")
}
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
}
otp, err := utils.GenerateOTP()
if err != nil {
return nil, fmt.Errorf("gagal generate OTP: %v", err)
}
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
otpData := OTPData{
Phone: req.Phone,
OTP: otp,
UserID: user.ID,
Role: user.Role.RoleName,
Type: "login",
Attempts: 0,
}
err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute)
if err != nil {
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
}
err = sendOTP(req.Phone, otp)
if err != nil {
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
}
return &OTPResponse{
Message: "OTP berhasil dikirim",
ExpiresIn: 300,
Phone: maskPhoneNumber(req.Phone),
}, nil
}
func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
var otpData OTPData
err := utils.GetCache(otpKey, &otpData)
if err != nil {
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
}
if otpData.Attempts >= 3 {
utils.DeleteCache(otpKey)
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
}
if otpData.OTP != req.Otp {
otpData.Attempts++
utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt))
return nil, fmt.Errorf("kode OTP salah")
}
normalizedRole := strings.ToLower(req.RoleName)
user, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole)
if err != nil {
return nil, fmt.Errorf("user tidak ditemukan")
}
utils.DeleteCache(otpKey)
tokenResponse, err := utils.GenerateTokenPair(
user.ID,
normalizedRole,
req.DeviceID,
user.RegistrationStatus,
int(user.RegistrationProgress),
)
if err != nil {
return nil, fmt.Errorf("gagal generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(
normalizedRole,
int(user.RegistrationProgress),
user.RegistrationStatus,
)
var message string
if user.RegistrationStatus == utils.RegStatusComplete {
message = "verif pin"
nextStep = "verif_pin"
} else {
message = "otp berhasil diverifikasi"
}
return &AuthResponse{
Message: message,
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: user.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *authenticationService) LogoutAuthentication(ctx context.Context, userID, deviceID string) error {
if err := utils.RevokeRefreshToken(userID, deviceID); err != nil {
return fmt.Errorf("failed to revoke token: %w", err)
}
return nil
}
func maskPhoneNumber(phone string) string {
if len(phone) < 4 {
return phone
}
return phone[:4] + strings.Repeat("*", len(phone)-8) + phone[len(phone)-4:]
}
func isRateLimited(key string, maxAttempts int, duration time.Duration) bool {
var count int
err := utils.GetCache(key, &count)
if err != nil {
count = 0
}
if count >= maxAttempts {
return true
}
count++
utils.SetCache(key, count, duration)
return false
}
func sendOTP(phone, otp string) error {
fmt.Printf("Sending OTP %s to %s\n", otp, phone)
return nil
}
// func convertUserToResponse(user *model.User) *UserResponse {
// return &UserResponse{
// ID: user.ID,
// Name: user.Name,
// Phone: user.Phone,
// Email: user.Email,
// Role: user.Role.RoleName,
// RegistrationStatus: user.RegistrationStatus,
// RegistrationProgress: user.RegistrationProgress,
// PhoneVerified: user.PhoneVerified,
// Avatar: user.Avatar,
// }
// }
func IsRegistrationComplete(role string, progress int) bool {
switch role {
case "masyarakat":
return progress >= 1
case "pengepul":
return progress >= 2
case "pengelola":
return progress >= 3
}
return false
}

49
internal/cart/cart_dto.go Normal file
View File

@ -0,0 +1,49 @@
package cart
import (
"fmt"
"strings"
)
type RequestCartItemDTO struct {
TrashID string `json:"trash_id"`
Amount float64 `json:"amount"`
}
type RequestCartDTO struct {
CartItems []RequestCartItemDTO `json:"cart_items"`
}
type ResponseCartDTO struct {
ID string `json:"id"`
UserID string `json:"user_id"`
TotalAmount float64 `json:"total_amount"`
EstimatedTotalPrice float64 `json:"estimated_total_price"`
CartItems []ResponseCartItemDTO `json:"cart_items"`
}
type ResponseCartItemDTO struct {
ID string `json:"id"`
TrashID string `json:"trash_id"`
TrashName string `json:"trash_name"`
TrashIcon string `json:"trash_icon"`
TrashPrice float64 `json:"trash_price"`
Amount float64 `json:"amount"`
SubTotalEstimatedPrice float64 `json:"subtotal_estimated_price"`
}
func (r *RequestCartDTO) ValidateRequestCartDTO() (map[string][]string, bool) {
errors := make(map[string][]string)
for i, item := range r.CartItems {
if strings.TrimSpace(item.TrashID) == "" {
errors[fmt.Sprintf("cart_items[%d].trash_id", i)] = append(errors[fmt.Sprintf("cart_items[%d].trash_id", i)], "trash_id tidak boleh kosong")
}
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,91 @@
package cart
import (
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type CartHandler struct {
cartService CartService
}
func NewCartHandler(cartService CartService) *CartHandler {
return &CartHandler{cartService: cartService}
}
func (h *CartHandler) AddOrUpdateItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
var req RequestCartItemDTO
if err := c.BodyParser(&req); err != nil {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Payload tidak valid", map[string][]string{
"request": {"Payload tidak valid"},
})
}
hasErrors := req.Amount <= 0 || req.TrashID == ""
if hasErrors {
errs := make(map[string][]string)
if req.Amount <= 0 {
errs["amount"] = append(errs["amount"], "Amount harus lebih dari 0")
}
if req.TrashID == "" {
errs["trash_id"] = append(errs["trash_id"], "Trash ID tidak boleh kosong")
}
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validasi gagal", errs)
}
if err := h.cartService.AddOrUpdateItem(c.Context(), userID, req); err != nil {
return utils.InternalServerError(c, "Gagal menambahkan item ke keranjang")
}
return utils.Success(c, "Item berhasil ditambahkan ke keranjang")
}
func (h *CartHandler) GetCart(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
cart, err := h.cartService.GetCart(c.Context(), userID)
if err != nil {
return utils.InternalServerError(c, "Gagal mengambil data keranjang")
}
return utils.SuccessWithData(c, "Berhasil mengambil data keranjang", cart)
}
func (h *CartHandler) DeleteItem(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
trashID := c.Params("trash_id")
if trashID == "" {
return utils.BadRequest(c, "Trash ID tidak boleh kosong")
}
if err := h.cartService.DeleteItem(c.Context(), userID, trashID); err != nil {
return utils.InternalServerError(c, "Gagal menghapus item dari keranjang")
}
return utils.Success(c, "Item berhasil dihapus dari keranjang")
}
func (h *CartHandler) Checkout(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
if err := h.cartService.Checkout(c.Context(), userID); err != nil {
return utils.InternalServerError(c, "Gagal melakukan checkout keranjang")
}
return utils.Success(c, "Checkout berhasil. Permintaan pickup telah dibuat.")
}
func (h *CartHandler) ClearCart(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
err := h.cartService.ClearCart(c.Context(), userID)
if err != nil {
return utils.InternalServerError(c, "Gagal menghapus keranjang")
}
return utils.Success(c, "Keranjang berhasil dikosongkan")
}

View File

@ -0,0 +1,72 @@
package cart
import (
"context"
"encoding/json"
"fmt"
"time"
"rijig/config"
"github.com/go-redis/redis/v8"
)
const CartTTL = 30 * time.Minute
const CartKeyPrefix = "cart:"
func buildCartKey(userID string) string {
return fmt.Sprintf("%s%s", CartKeyPrefix, userID)
}
func SetCartToRedis(ctx context.Context, userID string, cart RequestCartDTO) error {
data, err := json.Marshal(cart)
if err != nil {
return err
}
return config.RedisClient.Set(ctx, buildCartKey(userID), data, CartTTL).Err()
}
func RefreshCartTTL(ctx context.Context, userID string) error {
return config.RedisClient.Expire(ctx, buildCartKey(userID), CartTTL).Err()
}
func GetCartFromRedis(ctx context.Context, userID string) (*RequestCartDTO, error) {
val, err := config.RedisClient.Get(ctx, buildCartKey(userID)).Result()
if err == redis.Nil {
return nil, nil
} else if err != nil {
return nil, err
}
var cart RequestCartDTO
if err := json.Unmarshal([]byte(val), &cart); err != nil {
return nil, err
}
return &cart, nil
}
func DeleteCartFromRedis(ctx context.Context, userID string) error {
return config.RedisClient.Del(ctx, buildCartKey(userID)).Err()
}
func GetExpiringCartKeys(ctx context.Context, threshold time.Duration) ([]string, error) {
keys, err := config.RedisClient.Keys(ctx, CartKeyPrefix+"*").Result()
if err != nil {
return nil, err
}
var expiringKeys []string
for _, key := range keys {
ttl, err := config.RedisClient.TTL(ctx, key).Result()
if err != nil {
continue
}
if ttl > 0 && ttl <= threshold {
expiringKeys = append(expiringKeys, key)
}
}
return expiringKeys, nil
}

View File

@ -0,0 +1,162 @@
package cart
import (
"context"
"errors"
"fmt"
"rijig/config"
"rijig/model"
"gorm.io/gorm"
)
type CartRepository interface {
FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error)
AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error
DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error
GetCartByUser(ctx context.Context, userID string) (*model.Cart, error)
UpdateCartTotals(ctx context.Context, cartID string) error
DeleteCart(ctx context.Context, userID string) error
CreateCartWithItems(ctx context.Context, cart *model.Cart) error
HasExistingCart(ctx context.Context, userID string) (bool, error)
}
type cartRepository struct{}
func NewCartRepository() CartRepository {
return &cartRepository{}
}
func (r *cartRepository) FindOrCreateCart(ctx context.Context, userID string) (*model.Cart, error) {
var cart model.Cart
db := config.DB.WithContext(ctx)
err := db.
Preload("CartItems.TrashCategory").
Where("user_id = ?", userID).
First(&cart).Error
if err == nil {
return &cart, nil
}
if errors.Is(err, gorm.ErrRecordNotFound) {
newCart := model.Cart{
UserID: userID,
TotalAmount: 0,
EstimatedTotalPrice: 0,
}
if err := db.Create(&newCart).Error; err != nil {
return nil, err
}
return &newCart, nil
}
return nil, err
}
func (r *cartRepository) AddOrUpdateCartItem(ctx context.Context, cartID, trashCategoryID string, amount float64, estimatedPrice float64) error {
db := config.DB.WithContext(ctx)
var item model.CartItem
err := db.
Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID).
First(&item).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
newItem := model.CartItem{
CartID: cartID,
TrashCategoryID: trashCategoryID,
Amount: amount,
SubTotalEstimatedPrice: amount * estimatedPrice,
}
return db.Create(&newItem).Error
}
if err != nil {
return err
}
item.Amount = amount
item.SubTotalEstimatedPrice = amount * estimatedPrice
return db.Save(&item).Error
}
func (r *cartRepository) DeleteCartItem(ctx context.Context, cartID, trashCategoryID string) error {
db := config.DB.WithContext(ctx)
return db.Where("cart_id = ? AND trash_category_id = ?", cartID, trashCategoryID).
Delete(&model.CartItem{}).Error
}
func (r *cartRepository) GetCartByUser(ctx context.Context, userID string) (*model.Cart, error) {
var cart model.Cart
db := config.DB.WithContext(ctx)
err := db.
Preload("CartItems.TrashCategory").
Where("user_id = ?", userID).
First(&cart).Error
if err != nil {
return nil, err
}
return &cart, nil
}
func (r *cartRepository) UpdateCartTotals(ctx context.Context, cartID string) error {
db := config.DB.WithContext(ctx)
var items []model.CartItem
if err := db.Where("cart_id = ?", cartID).Find(&items).Error; err != nil {
return err
}
var totalAmount float64
var totalPrice float64
for _, item := range items {
totalAmount += item.Amount
totalPrice += item.SubTotalEstimatedPrice
}
return db.Model(&model.Cart{}).
Where("id = ?", cartID).
Updates(map[string]interface{}{
"total_amount": totalAmount,
"estimated_total_price": totalPrice,
}).Error
}
func (r *cartRepository) DeleteCart(ctx context.Context, userID string) error {
db := config.DB.WithContext(ctx)
var cart model.Cart
if err := db.Where("user_id = ?", userID).First(&cart).Error; err != nil {
return err
}
return db.Delete(&cart).Error
}
func (r *cartRepository) CreateCartWithItems(ctx context.Context, cart *model.Cart) error {
db := config.DB.WithContext(ctx)
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(cart).Error; err != nil {
return fmt.Errorf("failed to create cart: %w", err)
}
return nil
})
}
func (r *cartRepository) HasExistingCart(ctx context.Context, userID string) (bool, error) {
db := config.DB.WithContext(ctx)
var count int64
err := db.Model(&model.Cart{}).Where("user_id = ?", userID).Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}

View File

@ -0,0 +1,24 @@
package cart
import (
"rijig/config"
"rijig/internal/trash"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func TrashCartRouter(api fiber.Router) {
repo := NewCartRepository()
trashRepo := trash.NewTrashRepository(config.DB)
cartService := NewCartService(repo, trashRepo)
cartHandler := NewCartHandler(cartService)
cart := api.Group("/cart")
cart.Use(middleware.AuthMiddleware())
cart.Get("/", cartHandler.GetCart)
cart.Post("/item", cartHandler.AddOrUpdateItem)
cart.Delete("/item/:trash_id", cartHandler.DeleteItem)
cart.Delete("/clear", cartHandler.ClearCart)
}

View File

@ -0,0 +1,267 @@
package cart
import (
"context"
"errors"
"log"
// "rijig/dto"
// "rijig/internal/repositories"
"rijig/internal/trash"
"rijig/model"
)
type CartService interface {
AddOrUpdateItem(ctx context.Context, userID string, req RequestCartItemDTO) error
GetCart(ctx context.Context, userID string) (*ResponseCartDTO, error)
DeleteItem(ctx context.Context, userID string, trashID string) error
ClearCart(ctx context.Context, userID string) error
Checkout(ctx context.Context, userID string) error
}
type cartService struct {
repo CartRepository
trashRepo trash.TrashRepositoryInterface
}
func NewCartService(repo CartRepository, trashRepo trash.TrashRepositoryInterface) CartService {
return &cartService{repo, trashRepo}
}
func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req RequestCartItemDTO) error {
if req.Amount <= 0 {
return errors.New("amount harus lebih dari 0")
}
_, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID)
if err != nil {
return err
}
existingCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if existingCart == nil {
existingCart = &RequestCartDTO{
CartItems: []RequestCartItemDTO{},
}
}
updated := false
for i, item := range existingCart.CartItems {
if item.TrashID == req.TrashID {
existingCart.CartItems[i].Amount = req.Amount
updated = true
break
}
}
if !updated {
existingCart.CartItems = append(existingCart.CartItems, RequestCartItemDTO{
TrashID: req.TrashID,
Amount: req.Amount,
})
}
return SetCartToRedis(ctx, userID, *existingCart)
}
func (s *cartService) GetCart(ctx context.Context, userID string) (*ResponseCartDTO, error) {
cached, err := GetCartFromRedis(ctx, userID)
if err != nil {
return nil, err
}
if cached != nil {
if err := RefreshCartTTL(ctx, userID); err != nil {
log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err)
}
return s.buildResponseFromCache(ctx, userID, cached)
}
cart, err := s.repo.GetCartByUser(ctx, userID)
if err != nil {
return &ResponseCartDTO{
ID: "",
UserID: userID,
TotalAmount: 0,
EstimatedTotalPrice: 0,
CartItems: []ResponseCartItemDTO{},
}, nil
}
response := s.buildResponseFromDB(cart)
cacheData := RequestCartDTO{CartItems: []RequestCartItemDTO{}}
for _, item := range cart.CartItems {
cacheData.CartItems = append(cacheData.CartItems, RequestCartItemDTO{
TrashID: item.TrashCategoryID,
Amount: item.Amount,
})
}
if err := SetCartToRedis(ctx, userID, cacheData); err != nil {
log.Printf("Warning: Failed to cache cart for user %s: %v", userID, err)
}
return response, nil
}
func (s *cartService) DeleteItem(ctx context.Context, userID string, trashID string) error {
existingCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if existingCart == nil {
return errors.New("keranjang tidak ditemukan")
}
filtered := []RequestCartItemDTO{}
for _, item := range existingCart.CartItems {
if item.TrashID != trashID {
filtered = append(filtered, item)
}
}
existingCart.CartItems = filtered
return SetCartToRedis(ctx, userID, *existingCart)
}
func (s *cartService) ClearCart(ctx context.Context, userID string) error {
if err := DeleteCartFromRedis(ctx, userID); err != nil {
return err
}
return s.repo.DeleteCart(ctx, userID)
}
func (s *cartService) Checkout(ctx context.Context, userID string) error {
cachedCart, err := GetCartFromRedis(ctx, userID)
if err != nil {
return err
}
if cachedCart != nil {
if err := s.commitCartFromRedis(ctx, userID, cachedCart); err != nil {
return err
}
}
_, err = s.repo.GetCartByUser(ctx, userID)
if err != nil {
return err
}
DeleteCartFromRedis(ctx, userID)
return s.repo.DeleteCart(ctx, userID)
}
func (s *cartService) buildResponseFromCache(ctx context.Context, userID string, cached *RequestCartDTO) (*ResponseCartDTO, error) {
totalQty := 0.0
totalPrice := 0.0
items := []ResponseCartItemDTO{}
for _, item := range cached.CartItems {
trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID)
if err != nil {
log.Printf("Warning: Trash category %s not found for cached cart item", item.TrashID)
continue
}
subtotal := item.Amount * trash.EstimatedPrice
totalQty += item.Amount
totalPrice += subtotal
items = append(items, ResponseCartItemDTO{
ID: "",
TrashID: item.TrashID,
TrashName: trash.Name,
TrashIcon: trash.IconTrash,
TrashPrice: trash.EstimatedPrice,
Amount: item.Amount,
SubTotalEstimatedPrice: subtotal,
})
}
return &ResponseCartDTO{
ID: "-",
UserID: userID,
TotalAmount: totalQty,
EstimatedTotalPrice: totalPrice,
CartItems: items,
}, nil
}
func (s *cartService) buildResponseFromDB(cart *model.Cart) *ResponseCartDTO {
var items []ResponseCartItemDTO
for _, item := range cart.CartItems {
items = append(items, ResponseCartItemDTO{
ID: item.ID,
TrashID: item.TrashCategoryID,
TrashName: item.TrashCategory.Name,
TrashIcon: item.TrashCategory.IconTrash,
TrashPrice: item.TrashCategory.EstimatedPrice,
Amount: item.Amount,
SubTotalEstimatedPrice: item.SubTotalEstimatedPrice,
})
}
return &ResponseCartDTO{
ID: cart.ID,
UserID: cart.UserID,
TotalAmount: cart.TotalAmount,
EstimatedTotalPrice: cart.EstimatedTotalPrice,
CartItems: items,
}
}
func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *RequestCartDTO) error {
if len(cachedCart.CartItems) == 0 {
return nil
}
totalAmount := 0.0
totalPrice := 0.0
var cartItems []model.CartItem
for _, item := range cachedCart.CartItems {
trash, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashID)
if err != nil {
log.Printf("Warning: Skipping invalid trash category %s during commit", item.TrashID)
continue
}
subtotal := item.Amount * trash.EstimatedPrice
totalAmount += item.Amount
totalPrice += subtotal
cartItems = append(cartItems, model.CartItem{
TrashCategoryID: item.TrashID,
Amount: item.Amount,
SubTotalEstimatedPrice: subtotal,
})
}
if len(cartItems) == 0 {
return nil
}
newCart := &model.Cart{
UserID: userID,
TotalAmount: totalAmount,
EstimatedTotalPrice: totalPrice,
CartItems: cartItems,
}
return s.repo.CreateCartWithItems(ctx, newCart)
}

View File

@ -0,0 +1 @@
package model

View File

@ -0,0 +1,266 @@
package collector
import (
"fmt"
"rijig/internal/address"
"rijig/internal/trash"
"strings"
"time"
)
type NearbyCollectorDTO struct {
CollectorID string `json:"collector_id"`
Name string `json:"name"`
Phone string `json:"phone"`
Rating float32 `json:"rating"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
DistanceKm float64 `json:"distance_km"`
MatchedTrash []string `json:"matched_trash_ids"`
}
type SelectCollectorRequest struct {
Collector_id string `json:"collector_id"`
}
type CreateCollectorRequest struct {
UserID string `json:"user_id" binding:"required"`
JobStatus string `json:"job_status,omitempty"`
AddressID string `json:"address_id" binding:"required"`
AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items,omitempty"`
}
type UpdateCollectorRequest struct {
JobStatus string `json:"job_status,omitempty"`
AddressID string `json:"address_id,omitempty"`
AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items,omitempty"`
}
type CreateAvailableTrashRequest struct {
TrashCategoryID string `json:"trash_category_id" binding:"required"`
Price float32 `json:"price" binding:"required"`
}
type UpdateAvailableTrashRequest struct {
ID string `json:"id,omitempty"`
TrashCategoryID string `json:"trash_category_id,omitempty"`
Price float32 `json:"price,omitempty"`
}
type CollectorResponse struct {
ID string `json:"id"`
UserID string `json:"user_id"`
JobStatus string `json:"job_status"`
Rating float32 `json:"rating"`
AddressID string `json:"address_id"`
Address *address.AddressResponseDTO `json:"address,omitempty"`
AvailableTrash []AvailableTrashResponse `json:"available_trash"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type AvailableTrashResponse struct {
ID string `json:"id"`
CollectorID string `json:"collector_id"`
TrashCategoryID string `json:"trash_category_id"`
TrashCategory *trash.ResponseTrashCategoryDTO `json:"trash_category,omitempty"`
Price float32 `json:"price"`
}
func (r *CreateCollectorRequest) ValidateCreateCollectorRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.UserID) == "" {
errors["user_id"] = append(errors["user_id"], "User ID tidak boleh kosong")
}
if strings.TrimSpace(r.AddressID) == "" {
errors["address_id"] = append(errors["address_id"], "Address ID tidak boleh kosong")
}
if r.JobStatus != "" {
r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus))
if r.JobStatus != "active" && r.JobStatus != "inactive" {
errors["job_status"] = append(errors["job_status"], "Job status hanya boleh 'active' atau 'inactive'")
}
} else {
r.JobStatus = "inactive"
}
if len(r.AvailableTrashItems) > 0 {
trashCategoryMap := make(map[string]bool)
for i, item := range r.AvailableTrashItems {
fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i)
if strings.TrimSpace(item.TrashCategoryID) == "" {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong")
} else {
if trashCategoryMap[item.TrashCategoryID] {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar")
} else {
trashCategoryMap[item.TrashCategoryID] = true
}
}
if item.Price <= 0 {
errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0")
}
}
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *UpdateCollectorRequest) ValidateUpdateCollectorRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if r.JobStatus != "" {
r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus))
if r.JobStatus != "active" && r.JobStatus != "inactive" {
errors["job_status"] = append(errors["job_status"], "Job status hanya boleh 'active' atau 'inactive'")
}
}
if r.AddressID != "" && strings.TrimSpace(r.AddressID) == "" {
errors["address_id"] = append(errors["address_id"], "Address ID tidak boleh kosong jika disediakan")
}
if len(r.AvailableTrashItems) > 0 {
trashCategoryMap := make(map[string]bool)
for i, item := range r.AvailableTrashItems {
fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i)
if strings.TrimSpace(item.TrashCategoryID) == "" {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong")
} else {
if trashCategoryMap[item.TrashCategoryID] {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar")
} else {
trashCategoryMap[item.TrashCategoryID] = true
}
}
if item.Price <= 0 {
errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0")
}
}
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *CreateAvailableTrashRequest) ValidateCreateAvailableTrashRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.TrashCategoryID) == "" {
errors["trash_category_id"] = append(errors["trash_category_id"], "Trash category ID tidak boleh kosong")
}
if r.Price <= 0 {
errors["price"] = append(errors["price"], "Harga harus lebih dari 0")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *UpdateAvailableTrashRequest) ValidateUpdateAvailableTrashRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if r.TrashCategoryID != "" && strings.TrimSpace(r.TrashCategoryID) == "" {
errors["trash_category_id"] = append(errors["trash_category_id"], "Trash category ID tidak boleh kosong jika disediakan")
}
if r.Price != 0 && r.Price <= 0 {
errors["price"] = append(errors["price"], "Harga harus lebih dari 0 jika disediakan")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
func (r *CreateCollectorRequest) IsValidJobStatus(status string) bool {
status = strings.ToLower(strings.TrimSpace(status))
return status == "active" || status == "inactive"
}
func (r *UpdateCollectorRequest) IsValidJobStatus(status string) bool {
status = strings.ToLower(strings.TrimSpace(status))
return status == "active" || status == "inactive"
}
func (r *CollectorResponse) FormatTimestamp(t time.Time) string {
return t.Format(time.RFC3339)
}
func (r *CreateCollectorRequest) SetDefaults() {
if r.JobStatus == "" {
r.JobStatus = "inactive"
} else {
r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus))
}
}
func (r *UpdateCollectorRequest) NormalizeJobStatus() {
if r.JobStatus != "" {
r.JobStatus = strings.ToLower(strings.TrimSpace(r.JobStatus))
}
}
type BulkUpdateAvailableTrashRequest struct {
CollectorID string `json:"collector_id" binding:"required"`
AvailableTrashItems []CreateAvailableTrashRequest `json:"available_trash_items" binding:"required"`
}
func (r *BulkUpdateAvailableTrashRequest) ValidateBulkUpdateAvailableTrashRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.CollectorID) == "" {
errors["collector_id"] = append(errors["collector_id"], "Collector ID tidak boleh kosong")
}
if len(r.AvailableTrashItems) == 0 {
errors["available_trash_items"] = append(errors["available_trash_items"], "Minimal harus ada 1 item sampah")
} else {
trashCategoryMap := make(map[string]bool)
for i, item := range r.AvailableTrashItems {
fieldPrefix := fmt.Sprintf("available_trash_items[%d]", i)
if strings.TrimSpace(item.TrashCategoryID) == "" {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID tidak boleh kosong")
} else {
if trashCategoryMap[item.TrashCategoryID] {
errors[fieldPrefix+".trash_category_id"] = append(errors[fieldPrefix+".trash_category_id"], "Trash category ID sudah ada dalam daftar")
} else {
trashCategoryMap[item.TrashCategoryID] = true
}
}
if item.Price <= 0 {
errors[fieldPrefix+".price"] = append(errors[fieldPrefix+".price"], "Harga harus lebih dari 0")
}
}
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,360 @@
package collector
import (
"rijig/middleware"
"rijig/utils"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
)
type CollectorHandler struct {
collectorService CollectorService
}
func NewCollectorHandler(collectorService CollectorService) *CollectorHandler {
return &CollectorHandler{
collectorService: collectorService,
}
}
func (h *CollectorHandler) CreateCollector(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
var req CreateCollectorRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
errors, isValid := req.ValidateCreateCollectorRequest()
if !isValid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
req.SetDefaults()
collector, err := h.collectorService.CreateCollector(c.Context(), &req, claims.UserID)
if err != nil {
if strings.Contains(err.Error(), "already exists") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to create collector")
}
return utils.CreateSuccessWithData(c, "Collector created successfully", collector)
}
func (h *CollectorHandler) GetCollectorByID(c *fiber.Ctx) error {
id := c.Params("id")
if strings.TrimSpace(id) == "" {
return utils.BadRequest(c, "Collector ID is required")
}
collector, err := h.collectorService.GetCollectorByID(c.Context(), id)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to get collector")
}
return utils.SuccessWithData(c, "Collector retrieved successfully", collector)
}
func (h *CollectorHandler) GetCollectorByUserID(c *fiber.Ctx) error {
// userID := c.Params("userID")
// if strings.TrimSpace(userID) == "" {
// return utils.BadRequest(c, "User ID is required")
// }
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
collector, err := h.collectorService.GetCollectorByUserID(c.Context(), claims.UserID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found for this user")
}
return utils.InternalServerError(c, "Failed to get collector")
}
return utils.SuccessWithData(c, "Collector retrieved successfully", collector)
}
func (h *CollectorHandler) UpdateCollector(c *fiber.Ctx) error {
/* id := c.Params("id")
if strings.TrimSpace(id) == "" {
return utils.BadRequest(c, "Collector ID is required")
} */
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
var req UpdateCollectorRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
errors, isValid := req.ValidateUpdateCollectorRequest()
if !isValid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
req.NormalizeJobStatus()
collector, err := h.collectorService.UpdateCollector(c.Context(), claims.UserID, &req)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to update collector")
}
return utils.SuccessWithData(c, "Collector updated successfully", collector)
}
func (h *CollectorHandler) DeleteCollector(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
err = h.collectorService.DeleteCollector(c.Context(), claims.UserID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to delete collector")
}
return utils.Success(c, "Collector deleted successfully")
}
func (h *CollectorHandler) ListCollectors(c *fiber.Ctx) error {
limit, offset, page := h.parsePaginationParams(c)
collectors, total, err := h.collectorService.ListCollectors(c.Context(), limit, offset)
if err != nil {
return utils.InternalServerError(c, "Failed to get collectors")
}
responseData := map[string]interface{}{
"collectors": collectors,
"total": total,
}
return utils.SuccessWithPagination(c, "Collectors retrieved successfully", responseData, page, limit)
}
func (h *CollectorHandler) GetActiveCollectors(c *fiber.Ctx) error {
limit, offset, page := h.parsePaginationParams(c)
collectors, total, err := h.collectorService.GetActiveCollectors(c.Context(), limit, offset)
if err != nil {
return utils.InternalServerError(c, "Failed to get active collectors")
}
responseData := map[string]interface{}{
"collectors": collectors,
"total": total,
}
return utils.SuccessWithPagination(c, "Active collectors retrieved successfully", responseData, page, limit)
}
func (h *CollectorHandler) GetCollectorsByAddress(c *fiber.Ctx) error {
addressID := c.Params("addressID")
if strings.TrimSpace(addressID) == "" {
return utils.BadRequest(c, "Address ID is required")
}
limit, offset, page := h.parsePaginationParams(c)
collectors, total, err := h.collectorService.GetCollectorsByAddress(c.Context(), addressID, limit, offset)
if err != nil {
return utils.InternalServerError(c, "Failed to get collectors by address")
}
responseData := map[string]interface{}{
"collectors": collectors,
"total": total,
"address_id": addressID,
}
return utils.SuccessWithPagination(c, "Collectors by address retrieved successfully", responseData, page, limit)
}
func (h *CollectorHandler) GetCollectorsByTrashCategory(c *fiber.Ctx) error {
trashCategoryID := c.Params("trashCategoryID")
if strings.TrimSpace(trashCategoryID) == "" {
return utils.BadRequest(c, "Trash category ID is required")
}
limit, offset, page := h.parsePaginationParams(c)
collectors, total, err := h.collectorService.GetCollectorsByTrashCategory(c.Context(), trashCategoryID, limit, offset)
if err != nil {
return utils.InternalServerError(c, "Failed to get collectors by trash category")
}
responseData := map[string]interface{}{
"collectors": collectors,
"total": total,
"trash_category_id": trashCategoryID,
}
return utils.SuccessWithPagination(c, "Collectors by trash category retrieved successfully", responseData, page, limit)
}
func (h *CollectorHandler) UpdateJobStatus(c *fiber.Ctx) error {
id := c.Params("id")
if strings.TrimSpace(id) == "" {
return utils.BadRequest(c, "Collector ID is required")
}
var req struct {
JobStatus string `json:"job_status" binding:"required"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if strings.TrimSpace(req.JobStatus) == "" {
return utils.BadRequest(c, "Job status is required")
}
jobStatus := strings.ToLower(strings.TrimSpace(req.JobStatus))
validStatuses := []string{"active", "inactive", "busy"}
if !h.isValidJobStatus(jobStatus, validStatuses) {
return utils.BadRequest(c, "Invalid job status. Valid statuses: active, inactive, busy")
}
err := h.collectorService.UpdateJobStatus(c.Context(), id, jobStatus)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to update job status")
}
return utils.Success(c, "Job status updated successfully")
}
func (h *CollectorHandler) UpdateRating(c *fiber.Ctx) error {
id := c.Params("id")
if strings.TrimSpace(id) == "" {
return utils.BadRequest(c, "Collector ID is required")
}
var req struct {
Rating float32 `json:"rating" binding:"required"`
}
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
if req.Rating < 1.0 || req.Rating > 5.0 {
return utils.BadRequest(c, "Rating must be between 1.0 and 5.0")
}
err := h.collectorService.UpdateRating(c.Context(), id, req.Rating)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to update rating")
}
return utils.Success(c, "Rating updated successfully")
}
func (h *CollectorHandler) UpdateAvailableTrash(c *fiber.Ctx) error {
id := c.Params("id")
if strings.TrimSpace(id) == "" {
return utils.BadRequest(c, "Collector ID is required")
}
var req BulkUpdateAvailableTrashRequest
req.CollectorID = id
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
errors, isValid := req.ValidateBulkUpdateAvailableTrashRequest()
if !isValid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", errors)
}
err := h.collectorService.UpdateAvailableTrash(c.Context(), id, req.AvailableTrashItems)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Collector not found")
}
return utils.InternalServerError(c, "Failed to update available trash")
}
return utils.Success(c, "Available trash updated successfully")
}
func (h *CollectorHandler) parsePaginationParams(c *fiber.Ctx) (limit, offset, page int) {
limitStr := c.Query("limit", "10")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
limit = 10
}
if limit > 100 {
limit = 100
}
pageStr := c.Query("page", "1")
page, err = strconv.Atoi(pageStr)
if err != nil || page <= 0 {
page = 1
}
offset = (page - 1) * limit
return limit, offset, page
}
func (h *CollectorHandler) isValidJobStatus(status string, validStatuses []string) bool {
for _, validStatus := range validStatuses {
if status == validStatus {
return true
}
}
return false
}
func (h *CollectorHandler) RegisterRoutes(app *fiber.App) {
collectors := app.Group("/api/v1/collectors")
collectors.Post("/", h.CreateCollector)
collectors.Get("/:id", h.GetCollectorByID)
collectors.Put("/:id", h.UpdateCollector)
collectors.Delete("/:id", h.DeleteCollector)
collectors.Get("/", h.ListCollectors)
collectors.Get("/active", h.GetActiveCollectors)
collectors.Get("/user/:userID", h.GetCollectorByUserID)
collectors.Get("/address/:addressID", h.GetCollectorsByAddress)
collectors.Get("/trash-category/:trashCategoryID", h.GetCollectorsByTrashCategory)
collectors.Patch("/:id/job-status", h.UpdateJobStatus)
collectors.Patch("/:id/rating", h.UpdateRating)
collectors.Put("/:id/available-trash", h.UpdateAvailableTrash)
}

View File

@ -0,0 +1,370 @@
package collector
import (
"context"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type CollectorRepository interface {
Create(ctx context.Context, collector *model.Collector) error
GetByID(ctx context.Context, id string) (*model.Collector, error)
GetByUserID(ctx context.Context, userID string) (*model.Collector, error)
Update(ctx context.Context, collector *model.Collector) error
Delete(ctx context.Context, UserID string) error
List(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error)
GetActiveCollectors(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error)
GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*model.Collector, int64, error)
GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*model.Collector, int64, error)
UpdateJobStatus(ctx context.Context, id string, jobStatus string) error
UpdateRating(ctx context.Context, id string, rating float32) error
CreateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error
GetAvailableTrashByCollectorID(ctx context.Context, collectorID string) ([]*model.AvaibleTrashByCollector, error)
UpdateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error
DeleteAvailableTrash(ctx context.Context, id string) error
BulkCreateAvailableTrash(ctx context.Context, availableTrashList []*model.AvaibleTrashByCollector) error
BulkUpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashList []*model.AvaibleTrashByCollector) error
DeleteAvailableTrashByCollectorID(ctx context.Context, collectorID string) error
GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error)
GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error)
WithTx(tx *gorm.DB) CollectorRepository
}
type collectorRepository struct {
db *gorm.DB
}
func NewCollectorRepository(db *gorm.DB) CollectorRepository {
return &collectorRepository{
db: db,
}
}
func (r *collectorRepository) WithTx(tx *gorm.DB) CollectorRepository {
return &collectorRepository{
db: tx,
}
}
func (r *collectorRepository) Create(ctx context.Context, collector *model.Collector) error {
if err := r.db.WithContext(ctx).Create(collector).Error; err != nil {
return fmt.Errorf("failed to create collector: %w", err)
}
return nil
}
func (r *collectorRepository) GetActiveCollectorsWithTrashAndAddress(ctx context.Context) ([]model.Collector, error) {
var collectors []model.Collector
err := r.db.WithContext(ctx).
Preload("User").
Preload("Address").
Preload("AvaibleTrashbyCollector.TrashCategory").
Where("job_status = ?", "active").
Find(&collectors).Error
if err != nil {
return nil, err
}
return collectors, nil
}
func (r *collectorRepository) GetCollectorWithAddressAndTrash(ctx context.Context, collectorID string) (*model.Collector, error) {
var collector model.Collector
err := r.db.WithContext(ctx).
Preload("Address").
Preload("AvaibleTrashbyCollector").
Where("id = ?", collectorID).
First(&collector).Error
if err != nil {
return nil, err
}
return &collector, nil
}
func (r *collectorRepository) GetByID(ctx context.Context, id string) (*model.Collector, error) {
var collector model.Collector
err := r.db.WithContext(ctx).
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Where("id = ?", id).
First(&collector).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("collector with id %s not found", id)
}
return nil, fmt.Errorf("failed to get collector by id: %w", err)
}
return &collector, nil
}
func (r *collectorRepository) GetByUserID(ctx context.Context, userID string) (*model.Collector, error) {
var collector model.Collector
err := r.db.WithContext(ctx).
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Where("user_id = ?", userID).
First(&collector).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("collector with user_id %s not found", userID)
}
return nil, fmt.Errorf("failed to get collector by user_id: %w", err)
}
return &collector, nil
}
func (r *collectorRepository) Update(ctx context.Context, collector *model.Collector) error {
if err := r.db.WithContext(ctx).Save(collector).Error; err != nil {
return fmt.Errorf("failed to update collector: %w", err)
}
return nil
}
func (r *collectorRepository) Delete(ctx context.Context, UserID string) error {
result := r.db.WithContext(ctx).Delete(&model.Collector{}, "user_id = ?", UserID)
if result.Error != nil {
return fmt.Errorf("failed to delete collector: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("collector with user_id %s not found", UserID)
}
return nil
}
func (r *collectorRepository) List(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) {
var collectors []*model.Collector
var total int64
if err := r.db.WithContext(ctx).Model(&model.Collector{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count collectors: %w", err)
}
err := r.db.WithContext(ctx).
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Limit(limit).
Offset(offset).
Find(&collectors).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to list collectors: %w", err)
}
return collectors, total, nil
}
func (r *collectorRepository) GetActiveCollectors(ctx context.Context, limit, offset int) ([]*model.Collector, int64, error) {
var collectors []*model.Collector
var total int64
query := r.db.WithContext(ctx).Where("job_status = ?", "active")
if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count active collectors: %w", err)
}
err := query.
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Limit(limit).
Offset(offset).
Find(&collectors).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to get active collectors: %w", err)
}
return collectors, total, nil
}
func (r *collectorRepository) GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*model.Collector, int64, error) {
var collectors []*model.Collector
var total int64
query := r.db.WithContext(ctx).Where("address_id = ?", addressID)
if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count collectors by address: %w", err)
}
err := query.
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Limit(limit).
Offset(offset).
Find(&collectors).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to get collectors by address: %w", err)
}
return collectors, total, nil
}
func (r *collectorRepository) GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*model.Collector, int64, error) {
var collectors []*model.Collector
var total int64
subQuery := r.db.WithContext(ctx).
Table("avaible_trash_by_collectors").
Select("collector_id").
Where("trash_category_id = ?", trashCategoryID)
query := r.db.WithContext(ctx).
Where("id IN (?)", subQuery)
if err := query.Model(&model.Collector{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count collectors by trash category: %w", err)
}
err := query.
Preload("Address").
Preload("AvaibleTrashByCollector").
Preload("AvaibleTrashByCollector.TrashCategory").
Limit(limit).
Offset(offset).
Find(&collectors).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to get collectors by trash category: %w", err)
}
return collectors, total, nil
}
func (r *collectorRepository) UpdateJobStatus(ctx context.Context, id string, jobStatus string) error {
result := r.db.WithContext(ctx).
Model(&model.Collector{}).
Where("id = ?", id).
Update("job_status", jobStatus)
if result.Error != nil {
return fmt.Errorf("failed to update job status: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("collector with id %s not found", id)
}
return nil
}
func (r *collectorRepository) UpdateRating(ctx context.Context, id string, rating float32) error {
result := r.db.WithContext(ctx).
Model(&model.Collector{}).
Where("id = ?", id).
Update("rating", rating)
if result.Error != nil {
return fmt.Errorf("failed to update rating: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("collector with id %s not found", id)
}
return nil
}
func (r *collectorRepository) CreateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error {
if err := r.db.WithContext(ctx).Create(availableTrash).Error; err != nil {
return fmt.Errorf("failed to create available trash: %w", err)
}
return nil
}
func (r *collectorRepository) GetAvailableTrashByCollectorID(ctx context.Context, collectorID string) ([]*model.AvaibleTrashByCollector, error) {
var availableTrash []*model.AvaibleTrashByCollector
err := r.db.WithContext(ctx).
Preload("TrashCategory").
Where("collector_id = ?", collectorID).
Find(&availableTrash).Error
if err != nil {
return nil, fmt.Errorf("failed to get available trash by collector id: %w", err)
}
return availableTrash, nil
}
func (r *collectorRepository) UpdateAvailableTrash(ctx context.Context, availableTrash *model.AvaibleTrashByCollector) error {
if err := r.db.WithContext(ctx).Save(availableTrash).Error; err != nil {
return fmt.Errorf("failed to update available trash: %w", err)
}
return nil
}
func (r *collectorRepository) DeleteAvailableTrash(ctx context.Context, id string) error {
result := r.db.WithContext(ctx).Delete(&model.AvaibleTrashByCollector{}, "id = ?", id)
if result.Error != nil {
return fmt.Errorf("failed to delete available trash: %w", result.Error)
}
if result.RowsAffected == 0 {
return fmt.Errorf("available trash with id %s not found", id)
}
return nil
}
func (r *collectorRepository) BulkCreateAvailableTrash(ctx context.Context, availableTrashList []*model.AvaibleTrashByCollector) error {
if len(availableTrashList) == 0 {
return nil
}
if err := r.db.WithContext(ctx).CreateInBatches(availableTrashList, 100).Error; err != nil {
return fmt.Errorf("failed to bulk create available trash: %w", err)
}
return nil
}
func (r *collectorRepository) BulkUpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashList []*model.AvaibleTrashByCollector) error {
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("collector_id = ?", collectorID).Delete(&model.AvaibleTrashByCollector{}).Error; err != nil {
return fmt.Errorf("failed to delete existing available trash: %w", err)
}
if len(availableTrashList) > 0 {
for _, item := range availableTrashList {
item.CollectorID = collectorID
}
if err := tx.CreateInBatches(availableTrashList, 100).Error; err != nil {
return fmt.Errorf("failed to create new available trash: %w", err)
}
}
return nil
})
}
func (r *collectorRepository) DeleteAvailableTrashByCollectorID(ctx context.Context, collectorID string) error {
if err := r.db.WithContext(ctx).Where("collector_id = ?", collectorID).Delete(&model.AvaibleTrashByCollector{}).Error; err != nil {
return fmt.Errorf("failed to delete available trash by collector id: %w", err)
}
return nil
}

View File

@ -0,0 +1 @@
package collector

View File

@ -0,0 +1,349 @@
package collector
import (
"context"
"fmt"
"rijig/internal/address"
"rijig/internal/trash"
"rijig/model"
"strings"
"time"
"gorm.io/gorm"
)
type CollectorService interface {
CreateCollector(ctx context.Context, req *CreateCollectorRequest, UserID string) (*CollectorResponse, error)
GetCollectorByID(ctx context.Context, id string) (*CollectorResponse, error)
GetCollectorByUserID(ctx context.Context, userID string) (*CollectorResponse, error)
UpdateCollector(ctx context.Context, UserID string, req *UpdateCollectorRequest) (*CollectorResponse, error)
DeleteCollector(ctx context.Context, UserID string) error
ListCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error)
GetActiveCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error)
GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*CollectorResponse, int64, error)
GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*CollectorResponse, int64, error)
UpdateJobStatus(ctx context.Context, id string, jobStatus string) error
UpdateRating(ctx context.Context, id string, rating float32) error
UpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashItems []CreateAvailableTrashRequest) error
}
type collectorService struct {
collectorRepo CollectorRepository
db *gorm.DB
}
func NewCollectorService(collectorRepo CollectorRepository, db *gorm.DB) CollectorService {
return &collectorService{
collectorRepo: collectorRepo,
db: db,
}
}
func (s *collectorService) CreateCollector(ctx context.Context, req *CreateCollectorRequest, UserID string) (*CollectorResponse, error) {
existingCollector, err := s.collectorRepo.GetByUserID(ctx, UserID)
if err != nil && !strings.Contains(err.Error(), "not found") {
return nil, fmt.Errorf("failed to check existing collector: %w", err)
}
if existingCollector != nil {
return nil, fmt.Errorf("collector already exists for user_id: %s", req.UserID)
}
collector := &model.Collector{
UserID: UserID,
JobStatus: "inactive",
AddressID: req.AddressID,
Rating: 5.0,
}
if req.JobStatus != "" {
collector.JobStatus = req.JobStatus
}
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
collectorRepoTx := s.collectorRepo.WithTx(tx)
if err := collectorRepoTx.Create(ctx, collector); err != nil {
return fmt.Errorf("failed to create collector: %w", err)
}
if len(req.AvailableTrashItems) > 0 {
availableTrashList := s.buildAvailableTrashList(collector.ID, req.AvailableTrashItems)
if err := collectorRepoTx.BulkCreateAvailableTrash(ctx, availableTrashList); err != nil {
return fmt.Errorf("failed to create available trash items: %w", err)
}
}
return nil
})
if err != nil {
return nil, err
}
createdCollector, err := s.collectorRepo.GetByID(ctx, collector.ID)
if err != nil {
return nil, fmt.Errorf("failed to fetch created collector: %w", err)
}
return s.toCollectorResponse(createdCollector), nil
}
func (s *collectorService) GetCollectorByID(ctx context.Context, id string) (*CollectorResponse, error) {
collector, err := s.collectorRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
return s.toCollectorResponse(collector), nil
}
func (s *collectorService) GetCollectorByUserID(ctx context.Context, userID string) (*CollectorResponse, error) {
collector, err := s.collectorRepo.GetByUserID(ctx, userID)
if err != nil {
return nil, err
}
return s.toCollectorResponse(collector), nil
}
func (s *collectorService) UpdateCollector(ctx context.Context, UserID string, req *UpdateCollectorRequest) (*CollectorResponse, error) {
collector, err := s.collectorRepo.GetByUserID(ctx, UserID)
if err != nil {
return nil, fmt.Errorf("failed to get collector: %w", err)
}
needsUpdate := s.checkCollectorNeedsUpdate(collector, req)
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
collectorRepoTx := s.collectorRepo.WithTx(tx)
if needsUpdate {
s.applyCollectorUpdates(collector, req)
collector.UpdatedAt = time.Now()
if err := collectorRepoTx.Update(ctx, collector); err != nil {
return fmt.Errorf("failed to update collector: %w", err)
}
}
if len(req.AvailableTrashItems) > 0 {
availableTrashList := s.buildAvailableTrashList(collector.ID, req.AvailableTrashItems)
if err := collectorRepoTx.BulkUpdateAvailableTrash(ctx, collector.ID, availableTrashList); err != nil {
return fmt.Errorf("failed to update available trash items: %w", err)
}
}
return nil
})
if err != nil {
return nil, err
}
updatedCollector, err := s.collectorRepo.GetByUserID(ctx, UserID)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated collector: %w", err)
}
return s.toCollectorResponse(updatedCollector), nil
}
func (s *collectorService) DeleteCollector(ctx context.Context, UserID string) error {
_, err := s.collectorRepo.GetByUserID(ctx, UserID)
if err != nil {
return fmt.Errorf("collector not found: %w", err)
}
if err := s.collectorRepo.Delete(ctx, UserID); err != nil {
return fmt.Errorf("failed to delete collector: %w", err)
}
return nil
}
func (s *collectorService) ListCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) {
limit, offset = s.normalizePagination(limit, offset)
collectors, total, err := s.collectorRepo.List(ctx, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list collectors: %w", err)
}
return s.buildCollectorResponseList(collectors), total, nil
}
func (s *collectorService) GetActiveCollectors(ctx context.Context, limit, offset int) ([]*CollectorResponse, int64, error) {
limit, offset = s.normalizePagination(limit, offset)
collectors, total, err := s.collectorRepo.GetActiveCollectors(ctx, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get active collectors: %w", err)
}
return s.buildCollectorResponseList(collectors), total, nil
}
func (s *collectorService) GetCollectorsByAddress(ctx context.Context, addressID string, limit, offset int) ([]*CollectorResponse, int64, error) {
limit, offset = s.normalizePagination(limit, offset)
collectors, total, err := s.collectorRepo.GetCollectorsByAddress(ctx, addressID, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get collectors by address: %w", err)
}
return s.buildCollectorResponseList(collectors), total, nil
}
func (s *collectorService) GetCollectorsByTrashCategory(ctx context.Context, trashCategoryID string, limit, offset int) ([]*CollectorResponse, int64, error) {
limit, offset = s.normalizePagination(limit, offset)
collectors, total, err := s.collectorRepo.GetCollectorsByTrashCategory(ctx, trashCategoryID, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get collectors by trash category: %w", err)
}
return s.buildCollectorResponseList(collectors), total, nil
}
func (s *collectorService) UpdateJobStatus(ctx context.Context, id string, jobStatus string) error {
if err := s.collectorRepo.UpdateJobStatus(ctx, id, jobStatus); err != nil {
return fmt.Errorf("failed to update job status: %w", err)
}
return nil
}
func (s *collectorService) UpdateRating(ctx context.Context, id string, rating float32) error {
if err := s.collectorRepo.UpdateRating(ctx, id, rating); err != nil {
return fmt.Errorf("failed to update rating: %w", err)
}
return nil
}
func (s *collectorService) UpdateAvailableTrash(ctx context.Context, collectorID string, availableTrashItems []CreateAvailableTrashRequest) error {
availableTrashList := s.buildAvailableTrashList(collectorID, availableTrashItems)
if err := s.collectorRepo.BulkUpdateAvailableTrash(ctx, collectorID, availableTrashList); err != nil {
return fmt.Errorf("failed to update available trash: %w", err)
}
return nil
}
func (s *collectorService) buildAvailableTrashList(collectorID string, items []CreateAvailableTrashRequest) []*model.AvaibleTrashByCollector {
availableTrashList := make([]*model.AvaibleTrashByCollector, 0, len(items))
for _, item := range items {
availableTrash := &model.AvaibleTrashByCollector{
CollectorID: collectorID,
TrashCategoryID: item.TrashCategoryID,
Price: item.Price,
}
availableTrashList = append(availableTrashList, availableTrash)
}
return availableTrashList
}
func (s *collectorService) checkCollectorNeedsUpdate(collector *model.Collector, req *UpdateCollectorRequest) bool {
if req.JobStatus != "" && req.JobStatus != collector.JobStatus {
return true
}
if req.AddressID != "" && req.AddressID != collector.AddressID {
return true
}
return false
}
func (s *collectorService) applyCollectorUpdates(collector *model.Collector, req *UpdateCollectorRequest) {
if req.JobStatus != "" {
collector.JobStatus = req.JobStatus
}
if req.AddressID != "" {
collector.AddressID = req.AddressID
}
}
func (s *collectorService) normalizePagination(limit, offset int) (int, int) {
if limit <= 0 {
limit = 10
}
if offset < 0 {
offset = 0
}
if limit > 100 {
limit = 100
}
return limit, offset
}
func (s *collectorService) buildCollectorResponseList(collectors []*model.Collector) []*CollectorResponse {
responses := make([]*CollectorResponse, 0, len(collectors))
for _, collector := range collectors {
responses = append(responses, s.toCollectorResponse(collector))
}
return responses
}
func (s *collectorService) toCollectorResponse(collector *model.Collector) *CollectorResponse {
response := &CollectorResponse{
ID: collector.ID,
UserID: collector.UserID,
JobStatus: collector.JobStatus,
Rating: collector.Rating,
AddressID: collector.AddressID,
AvailableTrash: make([]AvailableTrashResponse, 0),
CreatedAt: collector.CreatedAt.Format(time.RFC3339),
UpdatedAt: collector.UpdatedAt.Format(time.RFC3339),
}
if collector.Address.ID != "" {
response.Address = &address.AddressResponseDTO{
ID: collector.Address.ID,
UserID: collector.Address.UserID,
Province: collector.Address.Province,
Regency: collector.Address.Regency,
District: collector.Address.District,
Village: collector.Address.Village,
PostalCode: collector.Address.PostalCode,
Detail: collector.Address.Detail,
Latitude: collector.Address.Latitude,
Longitude: collector.Address.Longitude,
CreatedAt: collector.Address.CreatedAt.Format(time.RFC3339),
UpdatedAt: collector.Address.UpdatedAt.Format(time.RFC3339),
}
}
for _, availableTrash := range collector.AvaibleTrashByCollector {
trashResponse := AvailableTrashResponse{
ID: availableTrash.ID,
CollectorID: availableTrash.CollectorID,
TrashCategoryID: availableTrash.TrashCategoryID,
Price: availableTrash.Price,
}
if availableTrash.TrashCategory.ID != "" {
trashResponse.TrashCategory = &trash.ResponseTrashCategoryDTO{
ID: availableTrash.TrashCategory.ID,
TrashName: availableTrash.TrashCategory.Name,
TrashIcon: availableTrash.TrashCategory.IconTrash,
EstimatedPrice: availableTrash.TrashCategory.EstimatedPrice,
Variety: availableTrash.TrashCategory.Variety,
CreatedAt: availableTrash.TrashCategory.CreatedAt.Format(time.RFC3339),
UpdatedAt: availableTrash.TrashCategory.UpdatedAt.Format(time.RFC3339),
}
}
response.AvailableTrash = append(response.AvailableTrash, trashResponse)
}
return response
}

View File

@ -0,0 +1,62 @@
package company
import (
"rijig/utils"
"strings"
)
type ResponseCompanyProfileDTO struct {
ID string `json:"id"`
UserID string `json:"userId"`
CompanyName string `json:"company_name"`
CompanyAddress string `json:"company_address"`
CompanyPhone string `json:"company_phone"`
CompanyEmail string `json:"company_email"`
CompanyLogo string `json:"company_logo,omitempty"`
CompanyWebsite string `json:"company_website,omitempty"`
TaxID string `json:"taxId,omitempty"`
FoundedDate string `json:"founded_date,omitempty"`
CompanyType string `json:"company_type,omitempty"`
CompanyDescription string `json:"company_description"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestCompanyProfileDTO struct {
CompanyName string `json:"company_name"`
CompanyAddress string `json:"company_address"`
CompanyPhone string `json:"company_phone"`
CompanyEmail string `json:"company_email"`
CompanyLogo string `json:"company_logo,omitempty"`
CompanyWebsite string `json:"company_website,omitempty"`
TaxID string `json:"taxId,omitempty"`
FoundedDate string `json:"founded_date,omitempty"`
CompanyType string `json:"company_type,omitempty"`
CompanyDescription string `json:"company_description"`
}
func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.CompanyName) == "" {
errors["company_name"] = append(errors["company_name"], "Company name is required")
}
if strings.TrimSpace(r.CompanyAddress) == "" {
errors["company_address"] = append(errors["company_address"], "Company address is required")
}
if !utils.IsValidPhoneNumber(r.CompanyPhone) {
errors["company_phone"] = append(errors["company_phone"], "nomor harus dimulai 62.. dan 8-14 digit")
}
if strings.TrimSpace(r.CompanyDescription) == "" {
errors["company_description"] = append(errors["company_description"], "Company description is required")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,125 @@
package company
import (
"context"
"log"
"rijig/middleware"
"rijig/utils"
"strings"
"github.com/gofiber/fiber/v2"
)
type CompanyProfileHandler struct {
service CompanyProfileService
}
func NewCompanyProfileHandler(service CompanyProfileService) *CompanyProfileHandler {
return &CompanyProfileHandler{
service: service,
}
}
func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "unauthorized access")
}
var req RequestCompanyProfileDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if errors, valid := req.ValidateCompanyProfileInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors)
}
companyLogo, err := c.FormFile("company_logo")
if err != nil {
log.Printf("Error getting company logo: %v", err)
return utils.BadRequest(c, "company logo is required")
}
res, err := h.service.CreateCompanyProfile(c.Context(), claims.UserID, claims.DeviceID, &req, companyLogo)
if err != nil {
log.Printf("Error creating identity card: %v", err)
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to create company logo")
}
return utils.SuccessWithData(c, "company profile created successfully", res)
}
func (h *CompanyProfileHandler) GetCompanyProfileByID(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "id is required")
}
res, err := h.service.GetCompanyProfileByID(context.Background(), id)
if err != nil {
return utils.NotFound(c, err.Error())
}
return utils.SuccessWithData(c, "company profile retrieved", res)
}
func (h *CompanyProfileHandler) GetCompanyProfilesByUserID(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
res, err := h.service.GetCompanyProfilesByUserID(context.Background(), userID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "company profiles retrieved", res)
}
func (h *CompanyProfileHandler) UpdateCompanyProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
var req RequestCompanyProfileDTO
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if errors, valid := req.ValidateCompanyProfileInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors)
}
res, err := h.service.UpdateCompanyProfile(context.Background(), userID, &req)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "company profile updated", res)
}
func (h *CompanyProfileHandler) DeleteCompanyProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("user_id").(string)
if !ok || userID == "" {
return utils.Unauthorized(c, "User not authenticated")
}
err := h.service.DeleteCompanyProfile(context.Background(), userID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.Success(c, "company profile deleted")
}

View File

@ -0,0 +1,89 @@
package company
import (
"context"
"fmt"
"rijig/model"
"gorm.io/gorm"
)
type CompanyProfileRepository interface {
CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error)
GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error)
GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error)
UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error
DeleteCompanyProfileByUserID(ctx context.Context, userID string) error
ExistsByUserID(ctx context.Context, userID string) (bool, error)
}
type companyProfileRepository struct {
db *gorm.DB
}
func NewCompanyProfileRepository(db *gorm.DB) CompanyProfileRepository {
return &companyProfileRepository{db}
}
func (r *companyProfileRepository) CreateCompanyProfile(ctx context.Context, companyProfile *model.CompanyProfile) (*model.CompanyProfile, error) {
err := r.db.WithContext(ctx).Create(companyProfile).Error
if err != nil {
return nil, fmt.Errorf("failed to create company profile: %v", err)
}
return companyProfile, nil
}
func (r *companyProfileRepository) GetCompanyProfileByID(ctx context.Context, id string) (*model.CompanyProfile, error) {
var companyProfile model.CompanyProfile
err := r.db.WithContext(ctx).Preload("User").First(&companyProfile, "id = ?", id).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("company profile with ID %s not found", id)
}
return nil, fmt.Errorf("error fetching company profile: %v", err)
}
return &companyProfile, nil
}
func (r *companyProfileRepository) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]model.CompanyProfile, error) {
var companyProfiles []model.CompanyProfile
err := r.db.WithContext(ctx).Preload("User").Where("user_id = ?", userID).Find(&companyProfiles).Error
if err != nil {
return nil, fmt.Errorf("error fetching company profiles for userID %s: %v", userID, err)
}
return companyProfiles, nil
}
func (r *companyProfileRepository) UpdateCompanyProfile(ctx context.Context, company *model.CompanyProfile) error {
var existing model.CompanyProfile
if err := r.db.WithContext(ctx).First(&existing, "user_id = ?", company.UserID).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return fmt.Errorf("company profile not found for user_id %s", company.UserID)
}
return fmt.Errorf("failed to fetch company profile: %v", err)
}
err := r.db.WithContext(ctx).Model(&existing).Updates(company).Error
if err != nil {
return fmt.Errorf("failed to update company profile: %v", err)
}
return nil
}
func (r *companyProfileRepository) DeleteCompanyProfileByUserID(ctx context.Context, userID string) error {
err := r.db.WithContext(ctx).Delete(&model.CompanyProfile{}, "user_id = ?", userID).Error
if err != nil {
return fmt.Errorf("failed to delete company profile: %v", err)
}
return nil
}
func (r *companyProfileRepository) ExistsByUserID(ctx context.Context, userID string) (bool, error) {
var count int64
err := r.db.WithContext(ctx).Model(&model.CompanyProfile{}).
Where("user_id = ?", userID).Count(&count).Error
if err != nil {
return false, fmt.Errorf("failed to check existence: %v", err)
}
return count > 0, nil
}

View File

@ -0,0 +1,27 @@
package company
import (
"rijig/config"
"rijig/internal/authentication"
"rijig/internal/userprofile"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func CompanyRouter(api fiber.Router) {
companyProfileRepo := NewCompanyProfileRepository(config.DB)
authRepo := authentication.NewAuthenticationRepository(config.DB)
userRepo := userprofile.NewUserProfileRepository(config.DB)
companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo, userRepo)
companyProfileHandler := NewCompanyProfileHandler(companyProfileService)
companyProfileAPI := api.Group("/companyprofile")
companyProfileAPI.Use(middleware.AuthMiddleware())
companyProfileAPI.Post("/create", companyProfileHandler.CreateCompanyProfile)
companyProfileAPI.Get("/get/:id", companyProfileHandler.GetCompanyProfileByID)
companyProfileAPI.Get("/get", companyProfileHandler.GetCompanyProfilesByUserID)
companyProfileAPI.Put("/update", companyProfileHandler.UpdateCompanyProfile)
companyProfileAPI.Delete("/delete", companyProfileHandler.DeleteCompanyProfile)
}

View File

@ -0,0 +1,373 @@
package company
import (
"context"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/internal/authentication"
"rijig/internal/role"
"rijig/internal/userprofile"
"rijig/model"
"rijig/utils"
"time"
)
type CompanyProfileService interface {
CreateCompanyProfile(ctx context.Context, userID, deviceID string, request *RequestCompanyProfileDTO, companyLogo *multipart.FileHeader) (*authentication.AuthResponse, error)
GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error)
GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error)
UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error)
DeleteCompanyProfile(ctx context.Context, userID string) error
GetAllCompanyProfilesByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseCompanyProfileDTO, error)
UpdateUserRegistrationStatusByCompany(ctx context.Context, companyUserID string, newStatus string) error
}
type companyProfileService struct {
companyRepo CompanyProfileRepository
authRepo authentication.AuthenticationRepository
userRepo userprofile.UserProfileRepository
}
func NewCompanyProfileService(companyRepo CompanyProfileRepository,
authRepo authentication.AuthenticationRepository,
userRepo userprofile.UserProfileRepository) CompanyProfileService {
return &companyProfileService{
companyRepo, authRepo, userRepo,
}
}
func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*ResponseCompanyProfileDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(companyProfile.UpdatedAt)
return &ResponseCompanyProfileDTO{
ID: companyProfile.ID,
UserID: companyProfile.UserID,
CompanyName: companyProfile.CompanyName,
CompanyAddress: companyProfile.CompanyAddress,
CompanyPhone: companyProfile.CompanyPhone,
CompanyEmail: companyProfile.CompanyEmail,
CompanyLogo: companyProfile.CompanyLogo,
CompanyWebsite: companyProfile.CompanyWebsite,
TaxID: companyProfile.TaxID,
FoundedDate: companyProfile.FoundedDate,
CompanyType: companyProfile.CompanyType,
CompanyDescription: companyProfile.CompanyDescription,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *companyProfileService) saveCompanyLogo(userID string, companyLogo *multipart.FileHeader) (string, error) {
pathImage := "/uploads/companyprofile/"
companyLogoDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(companyLogoDir); os.IsNotExist(err) {
if err := os.MkdirAll(companyLogoDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for company logo: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
extension := filepath.Ext(companyLogo.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
companyLogoFileName := fmt.Sprintf("%s_companylogo%s", userID, extension)
companyLogoPath := filepath.Join(companyLogoDir, companyLogoFileName)
src, err := companyLogo.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(companyLogoPath)
if err != nil {
return "", fmt.Errorf("failed to create company logo: %v", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return "", fmt.Errorf("failed to save company logo: %v", err)
}
companyLogoURL := fmt.Sprintf("%s%s", pathImage, companyLogoFileName)
return companyLogoURL, nil
}
func deleteIdentityCardImage(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID, deviceID string, request *RequestCompanyProfileDTO, companyLogo *multipart.FileHeader) (*authentication.AuthResponse, error) {
companyLogoPath, err := s.saveCompanyLogo(userID, companyLogo)
if err != nil {
return nil, fmt.Errorf("failed to save company logo: %v", err)
}
companyProfile := &model.CompanyProfile{
UserID: userID,
CompanyName: request.CompanyName,
CompanyAddress: request.CompanyAddress,
CompanyPhone: request.CompanyPhone,
CompanyEmail: request.CompanyEmail,
CompanyLogo: companyLogoPath,
CompanyWebsite: request.CompanyWebsite,
TaxID: request.TaxID,
FoundedDate: request.FoundedDate,
CompanyType: request.CompanyType,
CompanyDescription: request.CompanyDescription,
}
_, err = s.companyRepo.CreateCompanyProfile(ctx, companyProfile)
if err != nil {
return nil, err
}
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to find user: %v", err)
}
if user.Role.RoleName == "" {
return nil, fmt.Errorf("user role not found")
}
updates := map[string]interface{}{
"registration_progress": utils.ProgressDataSubmitted,
"registration_status": utils.RegStatusPending,
}
err = s.authRepo.PatchUser(ctx, userID, updates)
if err != nil {
return nil, fmt.Errorf("failed to update user: %v", err)
}
updated, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
if errors.Is(err, userprofile.ErrUserNotFound) {
return nil, fmt.Errorf("user not found")
}
return nil, fmt.Errorf("failed to get updated user: %w", err)
}
log.Printf("Token Generation Parameters:")
log.Printf("- UserID: '%s'", user.ID)
log.Printf("- Role: '%s'", user.Role.RoleName)
log.Printf("- DeviceID: '%s'", deviceID)
log.Printf("- Registration Status: '%s'", utils.RegStatusPending)
tokenResponse, err := utils.GenerateTokenPair(
updated.ID,
updated.Role.RoleName,
deviceID,
updated.RegistrationStatus,
int(updated.RegistrationProgress),
)
if err != nil {
log.Printf("GenerateTokenPair error: %v", err)
return nil, fmt.Errorf("failed to generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(
updated.Role.RoleName,
int(updated.RegistrationProgress),
updated.RegistrationStatus,
)
return &authentication.AuthResponse{
Message: "data usaha anda berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: updated.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *companyProfileService) GetCompanyProfileByID(ctx context.Context, id string) (*ResponseCompanyProfileDTO, error) {
profile, err := s.companyRepo.GetCompanyProfileByID(ctx, id)
if err != nil {
return nil, err
}
return FormatResponseCompanyProfile(profile)
}
func (s *companyProfileService) GetCompanyProfilesByUserID(ctx context.Context, userID string) ([]ResponseCompanyProfileDTO, error) {
profiles, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID)
if err != nil {
return nil, err
}
var responses []ResponseCompanyProfileDTO
for _, p := range profiles {
dto, err := FormatResponseCompanyProfile(&p)
if err != nil {
continue
}
responses = append(responses, *dto)
}
return responses, nil
}
func (s *companyProfileService) UpdateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) {
if errors, valid := request.ValidateCompanyProfileInput(); !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
company := &model.CompanyProfile{
UserID: userID,
CompanyName: request.CompanyName,
CompanyAddress: request.CompanyAddress,
CompanyPhone: request.CompanyPhone,
CompanyEmail: request.CompanyEmail,
CompanyLogo: request.CompanyLogo,
CompanyWebsite: request.CompanyWebsite,
TaxID: request.TaxID,
FoundedDate: request.FoundedDate,
CompanyType: request.CompanyType,
CompanyDescription: request.CompanyDescription,
}
if err := s.companyRepo.UpdateCompanyProfile(ctx, company); err != nil {
return nil, err
}
updated, err := s.companyRepo.GetCompanyProfilesByUserID(ctx, userID)
if err != nil || len(updated) == 0 {
return nil, fmt.Errorf("failed to retrieve updated company profile")
}
return FormatResponseCompanyProfile(&updated[0])
}
func (s *companyProfileService) DeleteCompanyProfile(ctx context.Context, userID string) error {
return s.companyRepo.DeleteCompanyProfileByUserID(ctx, userID)
}
func (s *companyProfileService) GetAllCompanyProfilesByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseCompanyProfileDTO, error) {
companyProfiles, err := s.authRepo.GetCompanyProfilesByUserRegStatus(ctx, userRegStatus)
if err != nil {
log.Printf("Error getting company profiles by registration status: %v", err)
return nil, fmt.Errorf("failed to get company profiles: %w", err)
}
var response []ResponseCompanyProfileDTO
for _, profile := range companyProfiles {
dto := ResponseCompanyProfileDTO{
ID: profile.ID,
UserID: profile.UserID,
CompanyName: profile.CompanyName,
CompanyAddress: profile.CompanyAddress,
CompanyPhone: profile.CompanyPhone,
CompanyEmail: profile.CompanyEmail,
CompanyLogo: profile.CompanyLogo,
CompanyWebsite: profile.CompanyWebsite,
TaxID: profile.TaxID,
FoundedDate: profile.FoundedDate,
CompanyType: profile.CompanyType,
CompanyDescription: profile.CompanyDescription,
CreatedAt: profile.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: profile.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
response = append(response, dto)
}
return response, nil
}
func (s *companyProfileService) UpdateUserRegistrationStatusByCompany(ctx context.Context, companyUserID string, newStatus string) error {
user, err := s.authRepo.FindUserByID(ctx, companyUserID)
if err != nil {
log.Printf("Error finding user by ID %s: %v", companyUserID, err)
return fmt.Errorf("user not found: %w", err)
}
updates := map[string]interface{}{
"registration_status": newStatus,
"updated_at": time.Now(),
}
switch newStatus {
case utils.RegStatusConfirmed:
updates["registration_progress"] = utils.ProgressDataSubmitted
case utils.RegStatusRejected:
updates["registration_progress"] = utils.ProgressOTPVerified
}
err = s.authRepo.PatchUser(ctx, user.ID, updates)
if err != nil {
log.Printf("Error updating user registration status for user ID %s: %v", user.ID, err)
return fmt.Errorf("failed to update user registration status: %w", err)
}
log.Printf("Successfully updated registration status for user ID %s to %s", user.ID, newStatus)
return nil
}
func (s *companyProfileService) GetUserProfile(ctx context.Context, userID string) (*userprofile.UserProfileResponseDTO, error) {
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
log.Printf("Error getting user profile for ID %s: %v", userID, err)
return nil, fmt.Errorf("failed to get user profile: %w", err)
}
response := &userprofile.UserProfileResponseDTO{
ID: user.ID,
Name: user.Name,
Gender: user.Gender,
Dateofbirth: user.Dateofbirth,
Placeofbirth: user.Placeofbirth,
Phone: user.Phone,
Email: user.Email,
PhoneVerified: user.PhoneVerified,
CreatedAt: user.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if user.Avatar != nil {
response.Avatar = *user.Avatar
}
if user.Role != nil {
response.Role = role.RoleResponseDTO{
ID: user.Role.ID,
RoleName: user.Role.RoleName,
CreatedAt: user.Role.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
UpdatedAt: user.Role.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
}
return response, nil
}

View File

@ -1,92 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type AddressHandler struct {
AddressService services.AddressService
}
func NewAddressHandler(addressService services.AddressService) *AddressHandler {
return &AddressHandler{AddressService: addressService}
}
func (h *AddressHandler) CreateAddress(c *fiber.Ctx) error {
var requestAddressDTO dto.CreateAddressDTO
if err := c.BodyParser(&requestAddressDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := requestAddressDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
addressResponse, err := h.AddressService.CreateAddress(c.Locals("userID").(string), requestAddressDTO)
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error())
}
return utils.CreateResponse(c, addressResponse, "user address created successfully")
}
func (h *AddressHandler) GetAddressByUserID(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
addresses, err := h.AddressService.GetAddressByUserID(userID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, addresses, "User addresses fetched successfully")
}
func (h *AddressHandler) GetAddressByID(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
addressID := c.Params("address_id")
address, err := h.AddressService.GetAddressByID(userID, addressID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, address, "Address fetched successfully")
}
func (h *AddressHandler) UpdateAddress(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
addressID := c.Params("address_id")
var addressDTO dto.CreateAddressDTO
if err := c.BodyParser(&addressDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := addressDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
updatedAddress, err := h.AddressService.UpdateAddress(userID, addressID, addressDTO)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, updatedAddress, "User address updated successfully")
}
func (h *AddressHandler) DeleteAddress(c *fiber.Ctx) error {
userID := c.Locals("userID").(string)
addressID := c.Params("address_id")
err := h.AddressService.DeleteAddress(userID, addressID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusForbidden, err.Error())
}
return utils.SuccessResponse(c, nil, "Address deleted successfully")
}

View File

@ -1,137 +0,0 @@
package handler
import (
"fmt"
"mime/multipart"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type ArticleHandler struct {
ArticleService services.ArticleService
}
func NewArticleHandler(articleService services.ArticleService) *ArticleHandler {
return &ArticleHandler{ArticleService: articleService}
}
func (h *ArticleHandler) CreateArticle(c *fiber.Ctx) error {
var request dto.RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
coverImage, err := c.FormFile("coverImage")
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required")
}
articleResponse, err := h.ArticleService.CreateArticle(request, coverImage)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.CreateResponse(c, articleResponse, "Article created successfully")
}
func (h *ArticleHandler) GetAllArticles(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil || page < 1 {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil || limit < 1 {
limit = 0
}
articles, totalArticles, err := h.ArticleService.GetAllArticles(page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch articles")
}
fmt.Printf("Total Articles: %d\n", totalArticles)
if page == 0 && limit == 0 {
return utils.NonPaginatedResponse(c, articles, totalArticles, "Articles fetched successfully")
}
return utils.PaginatedResponse(c, articles, page, limit, totalArticles, "Articles fetched successfully")
}
func (h *ArticleHandler) GetArticleByID(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required")
}
article, err := h.ArticleService.GetArticleByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "Article not found")
}
return utils.SuccessResponse(c, article, "Article fetched successfully")
}
func (h *ArticleHandler) UpdateArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required")
}
var request dto.RequestArticleDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
var coverImage *multipart.FileHeader
coverImage, err := c.FormFile("coverImage")
if err != nil && err.Error() != "no such file" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Cover image is required")
}
articleResponse, err := h.ArticleService.UpdateArticle(id, request, coverImage)
if err != nil {
if err.Error() == fmt.Sprintf("article with ID %s not found", id) {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, articleResponse, "Article updated successfully")
}
func (h *ArticleHandler) DeleteArticle(c *fiber.Ctx) error {
id := c.Params("article_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Article ID is required")
}
err := h.ArticleService.DeleteArticle(id)
if err != nil {
if err.Error() == fmt.Sprintf("article with ID %s not found", id) {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, "Article deleted successfully")
}

View File

@ -1,72 +0,0 @@
package handler
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type UserHandler struct {
UserService services.UserService
}
func NewUserHandler(userService services.UserService) *UserHandler {
return &UserHandler{UserService: userService}
}
func (h *UserHandler) Login(c *fiber.Ctx) error {
var loginDTO dto.LoginDTO
if err := c.BodyParser(&loginDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
validationErrors, valid := loginDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, validationErrors)
}
user, err := h.UserService.Login(loginDTO)
if err != nil {
return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error())
}
return utils.SuccessResponse(c, user, "Login successful")
}
func (h *UserHandler) Register(c *fiber.Ctx) error {
var registerDTO dto.RegisterDTO
if err := c.BodyParser(&registerDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}})
}
errors, valid := registerDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userResponse, err := h.UserService.Register(registerDTO)
if err != nil {
return utils.GenericResponse(c, fiber.StatusConflict, err.Error())
}
return utils.CreateResponse(c, userResponse, "Registration successful")
}
func (h *UserHandler) Logout(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
log.Println("Unauthorized access: User ID not found in session")
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
err := utils.DeleteSessionData(userID)
if err != nil {
return utils.InternalServerErrorResponse(c, "Error logging out")
}
return utils.SuccessResponse(c, nil, "Logout successful")
}

View File

@ -1,108 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type BannerHandler struct {
BannerService services.BannerService
}
func NewBannerHandler(bannerService services.BannerService) *BannerHandler {
return &BannerHandler{BannerService: bannerService}
}
func (h *BannerHandler) CreateBanner(c *fiber.Ctx) error {
var request dto.RequestBannerDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateBannerInput()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
bannerImage, err := c.FormFile("bannerimage")
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required")
}
bannerResponse, err := h.BannerService.CreateBanner(request, bannerImage)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.CreateResponse(c, bannerResponse, "Banner created successfully")
}
func (h *BannerHandler) GetAllBanners(c *fiber.Ctx) error {
banners, err := h.BannerService.GetAllBanners()
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch banners")
}
return utils.NonPaginatedResponse(c, banners, len(banners), "Banners fetched successfully")
}
func (h *BannerHandler) GetBannerByID(c *fiber.Ctx) error {
id := c.Params("banner_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required")
}
banner, err := h.BannerService.GetBannerByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "invalid banner id")
}
return utils.SuccessResponse(c, banner, "Banner fetched successfully")
}
func (h *BannerHandler) UpdateBanner(c *fiber.Ctx) error {
id := c.Params("banner_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required")
}
var request dto.RequestBannerDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateBannerInput()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
bannerImage, err := c.FormFile("bannerimage")
if err != nil && err.Error() != "no such file" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner image is required")
}
bannerResponse, err := h.BannerService.UpdateBanner(id, request, bannerImage)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, bannerResponse, "Banner updated successfully")
}
func (h *BannerHandler) DeleteBanner(c *fiber.Ctx) error {
id := c.Params("banner_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Banner ID is required")
}
err := h.BannerService.DeleteBanner(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, "Banner deleted successfully")
}

View File

@ -1,98 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type InitialCointHandler struct {
InitialCointService services.InitialCointService
}
func NewInitialCointHandler(initialCointService services.InitialCointService) *InitialCointHandler {
return &InitialCointHandler{InitialCointService: initialCointService}
}
func (h *InitialCointHandler) CreateInitialCoint(c *fiber.Ctx) error {
var request dto.RequestInitialCointDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateCointInput()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
initialCointResponse, err := h.InitialCointService.CreateInitialCoint(request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.CreateResponse(c, initialCointResponse, "Initial coint created successfully")
}
func (h *InitialCointHandler) GetAllInitialCoints(c *fiber.Ctx) error {
initialCoints, err := h.InitialCointService.GetAllInitialCoints()
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch initial coints")
}
return utils.NonPaginatedResponse(c, initialCoints, len(initialCoints), "Initial coints fetched successfully")
}
func (h *InitialCointHandler) GetInitialCointByID(c *fiber.Ctx) error {
id := c.Params("coin_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required")
}
initialCoint, err := h.InitialCointService.GetInitialCointByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "Invalid coin ID")
}
return utils.SuccessResponse(c, initialCoint, "Initial coint fetched successfully")
}
func (h *InitialCointHandler) UpdateInitialCoint(c *fiber.Ctx) error {
id := c.Params("coin_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required")
}
var request dto.RequestInitialCointDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := request.ValidateCointInput()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
initialCointResponse, err := h.InitialCointService.UpdateInitialCoint(id, request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, initialCointResponse, "Initial coint updated successfully")
}
func (h *InitialCointHandler) DeleteInitialCoint(c *fiber.Ctx) error {
id := c.Params("coin_id")
if id == "" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required")
}
err := h.InitialCointService.DeleteInitialCoint(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, "Initial coint deleted successfully")
}

View File

@ -1,46 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type RoleHandler struct {
RoleService services.RoleService
}
func NewRoleHandler(roleService services.RoleService) *RoleHandler {
return &RoleHandler{RoleService: roleService}
}
func (h *RoleHandler) GetRoles(c *fiber.Ctx) error {
roleID, ok := c.Locals("roleID").(string)
if !ok || roleID != utils.RoleAdministrator {
return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource")
}
roles, err := h.RoleService.GetRoles()
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.SuccessResponse(c, roles, "Roles fetched successfully")
}
func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error {
roleID := c.Params("role_id")
roleIDFromSession, ok := c.Locals("roleID").(string)
if !ok || roleIDFromSession != utils.RoleAdministrator {
return utils.GenericResponse(c, fiber.StatusForbidden, "Forbidden: You don't have permission to access this resource")
}
role, err := h.RoleService.GetRoleByID(roleID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "role id tidak ditemukan")
}
return utils.SuccessResponse(c, role, "Role fetched successfully")
}

View File

@ -1,128 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type TrashHandler struct {
TrashService services.TrashService
}
func NewTrashHandler(trashService services.TrashService) *TrashHandler {
return &TrashHandler{TrashService: trashService}
}
func (h *TrashHandler) CreateCategory(c *fiber.Ctx) error {
var request dto.RequestTrashCategoryDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
categoryResponse, err := h.TrashService.CreateCategory(request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to create category: "+err.Error())
}
return utils.CreateResponse(c, categoryResponse, "Category created successfully")
}
func (h *TrashHandler) AddDetailToCategory(c *fiber.Ctx) error {
var request dto.RequestTrashDetailDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
detailResponse, err := h.TrashService.AddDetailToCategory(request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to add detail to category: "+err.Error())
}
return utils.CreateResponse(c, detailResponse, "Trash detail added successfully")
}
func (h *TrashHandler) GetCategories(c *fiber.Ctx) error {
categories, err := h.TrashService.GetCategories()
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch categories: "+err.Error())
}
return utils.NonPaginatedResponse(c, categories, len(categories), "Categories retrieved successfully")
}
func (h *TrashHandler) GetCategoryByID(c *fiber.Ctx) error {
id := c.Params("category_id")
category, err := h.TrashService.GetCategoryByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "Category not found: "+err.Error())
}
return utils.SuccessResponse(c, category, "Category retrieved successfully")
}
func (h *TrashHandler) GetTrashDetailByID(c *fiber.Ctx) error {
id := c.Params("detail_id")
detail, err := h.TrashService.GetTrashDetailByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, "Trash detail not found: "+err.Error())
}
return utils.SuccessResponse(c, detail, "Trash detail retrieved successfully")
}
func (h *TrashHandler) UpdateCategory(c *fiber.Ctx) error {
id := c.Params("category_id")
var request dto.RequestTrashCategoryDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}})
}
updatedCategory, err := h.TrashService.UpdateCategory(id, request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating category: "+err.Error())
}
return utils.SuccessResponse(c, updatedCategory, "Category updated successfully")
}
func (h *TrashHandler) UpdateDetail(c *fiber.Ctx) error {
id := c.Params("detail_id")
var request dto.RequestTrashDetailDTO
if err := c.BodyParser(&request); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid request body"}})
}
updatedDetail, err := h.TrashService.UpdateDetail(id, request)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error updating detail: "+err.Error())
}
return utils.SuccessResponse(c, updatedDetail, "Trash detail updated successfully")
}
func (h *TrashHandler) DeleteCategory(c *fiber.Ctx) error {
id := c.Params("category_id")
if err := h.TrashService.DeleteCategory(id); err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error deleting category: "+err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, "Category deleted successfully")
}
func (h *TrashHandler) DeleteDetail(c *fiber.Ctx) error {
id := c.Params("detail_id")
if err := h.TrashService.DeleteDetail(id); err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Error deleting detail: "+err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, "Trash detail deleted successfully")
}

View File

@ -1,98 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type UserProfileHandler struct {
UserProfileService services.UserProfileService
}
func NewUserProfileHandler(userProfileService services.UserProfileService) *UserProfileHandler {
return &UserProfileHandler{UserProfileService: userProfileService}
}
func (h *UserProfileHandler) GetUserProfile(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
userProfile, err := h.UserProfileService.GetUserProfile(userID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusNotFound, err.Error())
}
return utils.SuccessResponse(c, userProfile, "User profile retrieved successfully")
}
func (h *UserProfileHandler) UpdateUserProfile(c *fiber.Ctx) error {
var updateData dto.UpdateUserDTO
if err := c.BodyParser(&updateData); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
errors, valid := updateData.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userResponse, err := h.UserProfileService.UpdateUserProfile(userID, updateData)
if err != nil {
return utils.GenericResponse(c, fiber.StatusConflict, err.Error())
}
return utils.SuccessResponse(c, userResponse, "User profile updated successfully")
}
func (h *UserProfileHandler) UpdateUserPassword(c *fiber.Ctx) error {
var passwordData dto.UpdatePasswordDTO
if err := c.BodyParser(&passwordData); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
errors, valid := passwordData.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
message, err := h.UserProfileService.UpdateUserPassword(userID, passwordData)
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, message)
}
func (h *UserProfileHandler) UpdateUserAvatar(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
file, err := c.FormFile("avatar")
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, "No avatar file uploaded")
}
message, err := h.UserProfileService.UpdateUserAvatar(userID, file)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, message)
}

View File

@ -1,101 +0,0 @@
package handler
import (
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type UserPinHandler struct {
UserPinService services.UserPinService
}
func NewUserPinHandler(userPinService services.UserPinService) *UserPinHandler {
return &UserPinHandler{UserPinService: userPinService}
}
func (h *UserPinHandler) VerifyUserPin(c *fiber.Ctx) error {
var requestUserPinDTO dto.RequestUserPinDTO
if err := c.BodyParser(&requestUserPinDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := requestUserPinDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
message, err := h.UserPinService.VerifyUserPin(userID, requestUserPinDTO.Pin)
if err != nil {
return utils.GenericResponse(c, fiber.StatusUnauthorized, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, message)
}
func (h *UserPinHandler) CheckPinStatus(c *fiber.Ctx) error {
userID, ok := c.Locals("userID").(string)
if !ok || userID == "" {
return utils.GenericResponse(c, fiber.StatusUnauthorized, "Unauthorized: User session not found")
}
status, err := h.UserPinService.CheckPinStatus(userID)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
if status == "Pin not created" {
return utils.GenericResponse(c, fiber.StatusBadRequest, "Pin belum dibuat")
}
return utils.GenericResponse(c, fiber.StatusOK, "Pin sudah dibuat")
}
func (h *UserPinHandler) CreateUserPin(c *fiber.Ctx) error {
var requestUserPinDTO dto.RequestUserPinDTO
if err := c.BodyParser(&requestUserPinDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := requestUserPinDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userID := c.Locals("userID").(string)
message, err := h.UserPinService.CreateUserPin(userID, requestUserPinDTO.Pin)
if err != nil {
return utils.GenericResponse(c, fiber.StatusConflict, err.Error())
}
return utils.GenericResponse(c, fiber.StatusCreated, message)
}
func (h *UserPinHandler) UpdateUserPin(c *fiber.Ctx) error {
var requestUserPinDTO dto.UpdateUserPinDTO
if err := c.BodyParser(&requestUserPinDTO); err != nil {
return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}})
}
errors, valid := requestUserPinDTO.Validate()
if !valid {
return utils.ValidationErrorResponse(c, errors)
}
userID := c.Locals("userID").(string)
message, err := h.UserPinService.UpdateUserPin(userID, requestUserPinDTO.OldPin, requestUserPinDTO.NewPin)
if err != nil {
return utils.GenericResponse(c, fiber.StatusBadRequest, err.Error())
}
return utils.GenericResponse(c, fiber.StatusOK, message)
}

View File

@ -1,199 +0,0 @@
package handler
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/pahmiudahgede/senggoldong/internal/services"
"github.com/pahmiudahgede/senggoldong/utils"
)
type WilayahIndonesiaHandler struct {
WilayahService services.WilayahIndonesiaService
}
func NewWilayahImportHandler(wilayahService services.WilayahIndonesiaService) *WilayahIndonesiaHandler {
return &WilayahIndonesiaHandler{WilayahService: wilayahService}
}
func (h *WilayahIndonesiaHandler) ImportWilayahData(c *fiber.Ctx) error {
err := h.WilayahService.ImportDataFromCSV()
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.SuccessResponse(c, fiber.StatusCreated, "Data imported successfully")
}
func (h *WilayahIndonesiaHandler) GetProvinces(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
provinces, totalProvinces, err := h.WilayahService.GetAllProvinces(page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch provinces")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, provinces, page, limit, totalProvinces, "Provinces fetched successfully")
}
return utils.NonPaginatedResponse(c, provinces, totalProvinces, "Provinces fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetProvinceByID(c *fiber.Ctx) error {
provinceID := c.Params("provinceid")
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
province, totalRegencies, err := h.WilayahService.GetProvinceByID(provinceID, page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch province")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, province, page, limit, totalRegencies, "Province fetched successfully")
}
return utils.NonPaginatedResponse(c, province, totalRegencies, "Province fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetAllRegencies(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
regencies, totalRegencies, err := h.WilayahService.GetAllRegencies(page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch regency")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, regencies, page, limit, totalRegencies, "regency fetched successfully")
}
return utils.NonPaginatedResponse(c, regencies, totalRegencies, "Provinces fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetRegencyByID(c *fiber.Ctx) error {
regencyId := c.Params("regencyid")
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
regency, totalDistrict, err := h.WilayahService.GetRegencyByID(regencyId, page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch regency")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, regency, page, limit, totalDistrict, "regency fetched successfully")
}
return utils.NonPaginatedResponse(c, regency, totalDistrict, "regency fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetAllDistricts(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
districts, totalDistricts, err := h.WilayahService.GetAllDistricts(page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch districts")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, districts, page, limit, totalDistricts, "districts fetched successfully")
}
return utils.NonPaginatedResponse(c, districts, totalDistricts, "districts fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetDistrictByID(c *fiber.Ctx) error {
districtId := c.Params("districtid")
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
district, totalVillages, err := h.WilayahService.GetDistrictByID(districtId, page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch district")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, district, page, limit, totalVillages, "district fetched successfully")
}
return utils.NonPaginatedResponse(c, district, totalVillages, "district fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetAllVillages(c *fiber.Ctx) error {
page, err := strconv.Atoi(c.Query("page", "0"))
if err != nil {
page = 0
}
limit, err := strconv.Atoi(c.Query("limit", "0"))
if err != nil {
limit = 0
}
villages, totalVillages, err := h.WilayahService.GetAllVillages(page, limit)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch villages")
}
if page > 0 && limit > 0 {
return utils.PaginatedResponse(c, villages, page, limit, totalVillages, "villages fetched successfully")
}
return utils.NonPaginatedResponse(c, villages, totalVillages, "villages fetched successfully")
}
func (h *WilayahIndonesiaHandler) GetVillageByID(c *fiber.Ctx) error {
id := c.Params("villageid")
village, err := h.WilayahService.GetVillageByID(id)
if err != nil {
return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error())
}
return utils.SuccessResponse(c, village, "Village fetched successfully")
}

View File

@ -0,0 +1,150 @@
package identitycart
import (
"rijig/utils"
"strings"
)
type ResponseIdentityCardDTO struct {
ID string `json:"id"`
UserID string `json:"userId"`
Identificationumber string `json:"identificationumber"`
Fullname string `json:"fullname"`
Placeofbirth string `json:"placeofbirth"`
Dateofbirth string `json:"dateofbirth"`
Gender string `json:"gender"`
BloodType string `json:"bloodtype"`
Province string `json:"province"`
District string `json:"district"`
SubDistrict string `json:"subdistrict"`
Hamlet string `json:"hamlet"`
Village string `json:"village"`
Neighbourhood string `json:"neighbourhood"`
PostalCode string `json:"postalcode"`
Religion string `json:"religion"`
Maritalstatus string `json:"maritalstatus"`
Job string `json:"job"`
Citizenship string `json:"citizenship"`
Validuntil string `json:"validuntil"`
Cardphoto string `json:"cardphoto"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type RequestIdentityCardDTO struct {
// DeviceID string `json:"device_id"`
UserID string `json:"userId"`
Identificationumber string `json:"identificationumber"`
Fullname string `json:"fullname"`
Placeofbirth string `json:"placeofbirth"`
Dateofbirth string `json:"dateofbirth"`
Gender string `json:"gender"`
BloodType string `json:"bloodtype"`
Province string `json:"province"`
District string `json:"district"`
SubDistrict string `json:"subdistrict"`
Hamlet string `json:"hamlet"`
Village string `json:"village"`
Neighbourhood string `json:"neighbourhood"`
PostalCode string `json:"postalcode"`
Religion string `json:"religion"`
Maritalstatus string `json:"maritalstatus"`
Job string `json:"job"`
Citizenship string `json:"citizenship"`
Validuntil string `json:"validuntil"`
Cardphoto string `json:"cardphoto"`
}
func (r *RequestIdentityCardDTO) ValidateIdentityCardInput() (map[string][]string, bool) {
errors := make(map[string][]string)
r.Placeofbirth = strings.ToLower(r.Placeofbirth)
r.Dateofbirth = strings.ToLower(r.Dateofbirth)
r.Gender = strings.ToLower(r.Gender)
r.BloodType = strings.ToUpper(r.BloodType)
r.Province = strings.ToLower(r.Province)
r.District = strings.ToLower(r.District)
r.SubDistrict = strings.ToLower(r.SubDistrict)
r.Hamlet = strings.ToLower(r.Hamlet)
r.Village = strings.ToLower(r.Village)
r.Neighbourhood = strings.ToLower(r.Neighbourhood)
r.PostalCode = strings.ToLower(r.PostalCode)
r.Religion = strings.ToLower(r.Religion)
r.Maritalstatus = strings.ToLower(r.Maritalstatus)
r.Job = strings.ToLower(r.Job)
r.Citizenship = strings.ToLower(r.Citizenship)
r.Validuntil = strings.ToLower(r.Validuntil)
nikData := utils.FetchNIKData(r.Identificationumber)
if strings.ToLower(nikData.Status) != "sukses" {
errors["identificationumber"] = append(errors["identificationumber"], "NIK yang anda masukkan tidak valid")
} else {
if r.Dateofbirth != strings.ToLower(nikData.Ttl) {
errors["dateofbirth"] = append(errors["dateofbirth"], "Tanggal lahir tidak sesuai dengan NIK")
}
if r.Gender != strings.ToLower(nikData.Sex) {
errors["gender"] = append(errors["gender"], "Jenis kelamin tidak sesuai dengan NIK")
}
if r.Province != strings.ToLower(nikData.Provinsi) {
errors["province"] = append(errors["province"], "Provinsi tidak sesuai dengan NIK")
}
if r.District != strings.ToLower(nikData.Kabkot) {
errors["district"] = append(errors["district"], "Kabupaten/Kota tidak sesuai dengan NIK")
}
if r.SubDistrict != strings.ToLower(nikData.Kecamatan) {
errors["subdistrict"] = append(errors["subdistrict"], "Kecamatan tidak sesuai dengan NIK")
}
if r.PostalCode != strings.ToLower(nikData.KodPos) {
errors["postalcode"] = append(errors["postalcode"], "Kode pos tidak sesuai dengan NIK")
}
}
if r.Placeofbirth == "" {
errors["placeofbirth"] = append(errors["placeofbirth"], "Tempat lahir wajib diisi")
}
if r.Hamlet == "" {
errors["hamlet"] = append(errors["hamlet"], "Dusun/RW wajib diisi")
}
if r.Village == "" {
errors["village"] = append(errors["village"], "Desa/Kelurahan wajib diisi")
}
if r.Neighbourhood == "" {
errors["neighbourhood"] = append(errors["neighbourhood"], "RT wajib diisi")
}
if r.Job == "" {
errors["job"] = append(errors["job"], "Pekerjaan wajib diisi")
}
if r.Citizenship == "" {
errors["citizenship"] = append(errors["citizenship"], "Kewarganegaraan wajib diisi")
}
if r.Validuntil == "" {
errors["validuntil"] = append(errors["validuntil"], "Berlaku hingga wajib diisi")
}
validBloodTypes := map[string]bool{"A": true, "B": true, "O": true, "AB": true}
if _, ok := validBloodTypes[r.BloodType]; !ok {
errors["bloodtype"] = append(errors["bloodtype"], "Golongan darah harus A, B, O, atau AB")
}
validReligions := map[string]bool{
"islam": true, "kristen": true, "katolik": true, "hindu": true, "buddha": true, "konghucu": true,
}
if _, ok := validReligions[r.Religion]; !ok {
errors["religion"] = append(errors["religion"], "Agama harus salah satu dari Islam, Kristen, Katolik, Hindu, Buddha, atau Konghucu")
}
if r.Maritalstatus != "kawin" && r.Maritalstatus != "belum kawin" {
errors["maritalstatus"] = append(errors["maritalstatus"], "Status perkawinan harus 'kawin' atau 'belum kawin'")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1,212 @@
package identitycart
import (
"log"
"rijig/middleware"
"rijig/utils"
"strings"
"github.com/gofiber/fiber/v2"
)
type IdentityCardHandler struct {
service IdentityCardService
}
func NewIdentityCardHandler(service IdentityCardService) *IdentityCardHandler {
return &IdentityCardHandler{service: service}
}
func (h *IdentityCardHandler) CreateIdentityCardHandler(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "unauthorized access")
}
var input RequestIdentityCardDTO
if err := c.BodyParser(&input); err != nil {
log.Printf("Error parsing body: %v", err)
return utils.BadRequest(c, "Invalid input format")
}
if errs, valid := input.ValidateIdentityCardInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs)
}
cardPhoto, err := c.FormFile("cardphoto")
if err != nil {
log.Printf("Error getting card photo: %v", err)
return utils.BadRequest(c, "KTP photo is required")
}
response, err := h.service.CreateIdentityCard(c.Context(), claims.UserID, claims.DeviceID, &input, cardPhoto)
if err != nil {
log.Printf("Error creating identity card: %v", err)
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to create identity card")
}
return utils.SuccessWithData(c, "KTP successfully submitted", response)
}
func (h *IdentityCardHandler) GetIdentityByID(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "ID is required")
}
result, err := h.service.GetIdentityCardByID(c.Context(), id)
if err != nil {
log.Printf("Error getting identity card by ID %s: %v", id, err)
return utils.NotFound(c, "Identity card not found")
}
return utils.SuccessWithData(c, "Successfully retrieved identity card", result)
}
func (h *IdentityCardHandler) GetIdentityByUserId(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "Unauthorized access")
}
result, err := h.service.GetIdentityCardsByUserID(c.Context(), claims.UserID)
if err != nil {
log.Printf("Error getting identity cards for user %s: %v", claims.UserID, err)
return utils.InternalServerError(c, "Failed to fetch your identity card data")
}
return utils.SuccessWithData(c, "Successfully retrieved your identity cards", result)
}
func (h *IdentityCardHandler) UpdateIdentityCardHandler(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "Unauthorized access")
}
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Identity card ID is required")
}
var input RequestIdentityCardDTO
if err := c.BodyParser(&input); err != nil {
log.Printf("Error parsing body: %v", err)
return utils.BadRequest(c, "Invalid input format")
}
if errs, valid := input.ValidateIdentityCardInput(); !valid {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Input validation failed", errs)
}
cardPhoto, err := c.FormFile("cardphoto")
if err != nil && err.Error() != "there is no uploaded file associated with the given key" {
log.Printf("Error getting card photo: %v", err)
return utils.BadRequest(c, "Invalid card photo")
}
if cardPhoto != nil && cardPhoto.Size > 5*1024*1024 {
return utils.BadRequest(c, "File size must be less than 5MB")
}
response, err := h.service.UpdateIdentityCard(c.Context(), claims.UserID, id, &input, cardPhoto)
if err != nil {
log.Printf("Error updating identity card: %v", err)
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "Identity card not found")
}
if strings.Contains(err.Error(), "invalid file type") {
return utils.BadRequest(c, err.Error())
}
return utils.InternalServerError(c, "Failed to update identity card")
}
return utils.SuccessWithData(c, "Identity card successfully updated", response)
}
func (h *IdentityCardHandler) GetAllIdentityCardsByRegStatus(c *fiber.Ctx) error {
_, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "Unauthorized access")
}
// if claims.Role != "admin" {
// return utils.Forbidden(c, "Access denied: admin role required")
// }
status := c.Query("status", utils.RegStatusPending)
validStatuses := map[string]bool{
utils.RegStatusPending: true,
"confirmed": true,
"rejected": true,
}
if !validStatuses[status] {
return utils.BadRequest(c, "Invalid status. Valid values: pending, confirmed, rejected")
}
result, err := h.service.GetAllIdentityCardsByRegStatus(c.Context(), status)
if err != nil {
log.Printf("Error getting identity cards by status %s: %v", status, err)
return utils.InternalServerError(c, "Failed to fetch identity cards")
}
return utils.SuccessWithData(c, "Successfully retrieved identity cards", result)
}
func (h *IdentityCardHandler) UpdateUserRegistrationStatusByIdentityCard(c *fiber.Ctx) error {
_, err := middleware.GetUserFromContext(c)
if err != nil {
log.Printf("Error getting user from context: %v", err)
return utils.Unauthorized(c, "Unauthorized access")
}
userID := c.Params("userId")
if userID == "" {
return utils.BadRequest(c, "User ID is required")
}
type StatusUpdateRequest struct {
Status string `json:"status" validate:"required,oneof=confirmed rejected"`
}
var input StatusUpdateRequest
if err := c.BodyParser(&input); err != nil {
log.Printf("Error parsing body: %v", err)
return utils.BadRequest(c, "Invalid input format")
}
if input.Status != "confirmed" && input.Status != "rejected" {
return utils.BadRequest(c, "Invalid status. Valid values: confirmed, rejected")
}
err = h.service.UpdateUserRegistrationStatusByIdentityCard(c.Context(), userID, input.Status)
if err != nil {
log.Printf("Error updating user registration status: %v", err)
if strings.Contains(err.Error(), "not found") {
return utils.NotFound(c, "User not found")
}
return utils.InternalServerError(c, "Failed to update registration status")
}
message := "User registration status successfully updated to " + input.Status
return utils.Success(c, message)
}
func (h *IdentityCardHandler) DeleteIdentityCardHandler(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return utils.BadRequest(c, "Identity card ID is required")
}
return utils.Success(c, "Identity card successfully deleted")
}

View File

@ -0,0 +1,64 @@
package identitycart
import (
"context"
"errors"
"fmt"
"log"
"rijig/model"
"gorm.io/gorm"
)
type IdentityCardRepository interface {
CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error)
GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error)
GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error)
UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error
}
type identityCardRepository struct {
db *gorm.DB
}
func NewIdentityCardRepository(db *gorm.DB) IdentityCardRepository {
return &identityCardRepository{
db: db,
}
}
func (r *identityCardRepository) CreateIdentityCard(ctx context.Context, identityCard *model.IdentityCard) (*model.IdentityCard, error) {
if err := r.db.WithContext(ctx).Create(identityCard).Error; err != nil {
log.Printf("Error creating identity card: %v", err)
return nil, fmt.Errorf("failed to create identity card: %w", err)
}
return identityCard, nil
}
func (r *identityCardRepository) GetIdentityCardByID(ctx context.Context, id string) (*model.IdentityCard, error) {
var identityCard model.IdentityCard
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&identityCard).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("identity card not found with id %s", id)
}
log.Printf("Error fetching identity card by ID: %v", err)
return nil, fmt.Errorf("error fetching identity card by ID: %w", err)
}
return &identityCard, nil
}
func (r *identityCardRepository) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]model.IdentityCard, error) {
var identityCards []model.IdentityCard
if err := r.db.WithContext(ctx).Where("user_id = ?", userID).Find(&identityCards).Error; err != nil {
log.Printf("Error fetching identity cards by userID: %v", err)
return nil, fmt.Errorf("error fetching identity cards by userID: %w", err)
}
return identityCards, nil
}
func (r *identityCardRepository) UpdateIdentityCard(ctx context.Context, identity *model.IdentityCard) error {
return r.db.WithContext(ctx).
Model(&model.IdentityCard{}).
Where("user_id = ?", identity.UserID).
Updates(identity).Error
}

View File

@ -0,0 +1,46 @@
package identitycart
import (
"rijig/config"
"rijig/internal/authentication"
"rijig/internal/userprofile"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
func UserIdentityCardRoute(api fiber.Router) {
identityRepo := NewIdentityCardRepository(config.DB)
authRepo := authentication.NewAuthenticationRepository(config.DB)
userRepo := userprofile.NewUserProfileRepository(config.DB)
identityService := NewIdentityCardService(identityRepo, authRepo, userRepo)
identityHandler := NewIdentityCardHandler(identityService)
identity := api.Group("/identity")
identity.Post("/create",
middleware.AuthMiddleware(),
middleware.RequireRoles(utils.RolePengepul),
identityHandler.CreateIdentityCardHandler,
)
identity.Get("/:id",
middleware.AuthMiddleware(),
identityHandler.GetIdentityByID,
)
identity.Get("/s",
middleware.AuthMiddleware(),
identityHandler.GetIdentityByUserId,
)
identity.Get("/",
middleware.AuthMiddleware(),
middleware.RequireRoles(utils.RoleAdministrator),
identityHandler.GetAllIdentityCardsByRegStatus,
)
identity.Patch("/:userId/status",
middleware.AuthMiddleware(),
middleware.RequireRoles(utils.RoleAdministrator),
identityHandler.UpdateUserRegistrationStatusByIdentityCard,
)
}

View File

@ -0,0 +1,478 @@
package identitycart
import (
"context"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"os"
"path/filepath"
"rijig/internal/authentication"
"rijig/internal/role"
"rijig/internal/userprofile"
"rijig/model"
"rijig/utils"
"time"
)
type IdentityCardService interface {
CreateIdentityCard(ctx context.Context, userID, deviceID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error)
GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error)
GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error)
UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error)
GetAllIdentityCardsByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseIdentityCardDTO, error)
UpdateUserRegistrationStatusByIdentityCard(ctx context.Context, identityCardUserID string, newStatus string) error
}
type identityCardService struct {
identityRepo IdentityCardRepository
authRepo authentication.AuthenticationRepository
userRepo userprofile.UserProfileRepository
}
func NewIdentityCardService(identityRepo IdentityCardRepository, authRepo authentication.AuthenticationRepository, userRepo userprofile.UserProfileRepository) IdentityCardService {
return &identityCardService{
identityRepo,
authRepo, userRepo,
}
}
type IdentityCardWithUserDTO struct {
IdentityCard ResponseIdentityCardDTO `json:"identity_card"`
User userprofile.UserProfileResponseDTO `json:"user"`
}
func FormatResponseIdentityCard(identityCard *model.IdentityCard) (*ResponseIdentityCardDTO, error) {
createdAt, _ := utils.FormatDateToIndonesianFormat(identityCard.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(identityCard.UpdatedAt)
return &ResponseIdentityCardDTO{
ID: identityCard.ID,
UserID: identityCard.UserID,
Identificationumber: identityCard.Identificationumber,
Placeofbirth: identityCard.Placeofbirth,
Dateofbirth: identityCard.Dateofbirth,
Gender: identityCard.Gender,
BloodType: identityCard.BloodType,
Province: identityCard.Province,
District: identityCard.District,
SubDistrict: identityCard.SubDistrict,
Hamlet: identityCard.Hamlet,
Village: identityCard.Village,
Neighbourhood: identityCard.Neighbourhood,
PostalCode: identityCard.PostalCode,
Religion: identityCard.Religion,
Maritalstatus: identityCard.Maritalstatus,
Job: identityCard.Job,
Citizenship: identityCard.Citizenship,
Validuntil: identityCard.Validuntil,
Cardphoto: identityCard.Cardphoto,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}, nil
}
func (s *identityCardService) saveIdentityCardImage(userID string, cardPhoto *multipart.FileHeader) (string, error) {
pathImage := "/uploads/identitycards/"
cardPhotoDir := "./public" + os.Getenv("BASE_URL") + pathImage
if _, err := os.Stat(cardPhotoDir); os.IsNotExist(err) {
if err := os.MkdirAll(cardPhotoDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for identity card photo: %v", err)
}
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
extension := filepath.Ext(cardPhoto.Filename)
if !allowedExtensions[extension] {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
cardPhotoFileName := fmt.Sprintf("%s_cardphoto%s", userID, extension)
cardPhotoPath := filepath.Join(cardPhotoDir, cardPhotoFileName)
src, err := cardPhoto.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(cardPhotoPath)
if err != nil {
return "", fmt.Errorf("failed to create card photo file: %v", err)
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return "", fmt.Errorf("failed to save card photo: %v", err)
}
cardPhotoURL := fmt.Sprintf("%s%s", pathImage, cardPhotoFileName)
return cardPhotoURL, nil
}
func deleteIdentityCardImage(imagePath string) error {
if imagePath == "" {
return nil
}
baseDir := "./public/" + os.Getenv("BASE_URL")
absolutePath := baseDir + imagePath
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
return fmt.Errorf("image file not found: %v", err)
}
err := os.Remove(absolutePath)
if err != nil {
return fmt.Errorf("failed to delete image: %v", err)
}
log.Printf("Image deleted successfully: %s", absolutePath)
return nil
}
func (s *identityCardService) CreateIdentityCard(ctx context.Context, userID, deviceID string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*authentication.AuthResponse, error) {
cardPhotoPath, err := s.saveIdentityCardImage(userID, cardPhoto)
if err != nil {
return nil, fmt.Errorf("failed to save card photo: %v", err)
}
identityCard := &model.IdentityCard{
UserID: userID,
Identificationumber: request.Identificationumber,
Placeofbirth: request.Placeofbirth,
Dateofbirth: request.Dateofbirth,
Gender: request.Gender,
BloodType: request.BloodType,
Province: request.Province,
District: request.District,
SubDistrict: request.SubDistrict,
Hamlet: request.Hamlet,
Village: request.Village,
Neighbourhood: request.Neighbourhood,
PostalCode: request.PostalCode,
Religion: request.Religion,
Maritalstatus: request.Maritalstatus,
Job: request.Job,
Citizenship: request.Citizenship,
Validuntil: request.Validuntil,
Cardphoto: cardPhotoPath,
}
_, err = s.identityRepo.CreateIdentityCard(ctx, identityCard)
if err != nil {
log.Printf("Error creating identity card: %v", err)
return nil, fmt.Errorf("failed to create identity card: %v", err)
}
user, err := s.authRepo.FindUserByID(ctx, userID)
if err != nil {
return nil, fmt.Errorf("failed to find user: %v", err)
}
if user.Role.RoleName == "" {
return nil, fmt.Errorf("user role not found")
}
updates := map[string]interface{}{
"registration_progress": utils.ProgressDataSubmitted,
"registration_status": utils.RegStatusPending,
}
err = s.authRepo.PatchUser(ctx, userID, updates)
if err != nil {
return nil, fmt.Errorf("failed to update user: %v", err)
}
updated, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
if errors.Is(err, userprofile.ErrUserNotFound) {
return nil, fmt.Errorf("user not found")
}
return nil, fmt.Errorf("failed to get updated user: %w", err)
}
log.Printf("Token Generation Parameters:")
log.Printf("- UserID: '%s'", user.ID)
log.Printf("- Role: '%s'", user.Role.RoleName)
log.Printf("- DeviceID: '%s'", deviceID)
log.Printf("- Registration Status: '%s'", utils.RegStatusPending)
tokenResponse, err := utils.GenerateTokenPair(
updated.ID,
updated.Role.RoleName,
deviceID,
updated.RegistrationStatus,
int(updated.RegistrationProgress),
)
if err != nil {
log.Printf("GenerateTokenPair error: %v", err)
return nil, fmt.Errorf("failed to generate token: %v", err)
}
nextStep := utils.GetNextRegistrationStep(
updated.Role.RoleName,
int(updated.RegistrationProgress),
updated.RegistrationStatus,
)
return &authentication.AuthResponse{
Message: "identity card berhasil diunggah, silakan tunggu konfirmasi dari admin dalam 1x24 jam",
AccessToken: tokenResponse.AccessToken,
RefreshToken: tokenResponse.RefreshToken,
TokenType: string(tokenResponse.TokenType),
ExpiresIn: tokenResponse.ExpiresIn,
RegistrationStatus: updated.RegistrationStatus,
NextStep: nextStep,
SessionID: tokenResponse.SessionID,
}, nil
}
func (s *identityCardService) GetIdentityCardByID(ctx context.Context, id string) (*ResponseIdentityCardDTO, error) {
identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id)
if err != nil {
log.Printf("Error fetching identity card: %v", err)
return nil, fmt.Errorf("failed to fetch identity card")
}
return FormatResponseIdentityCard(identityCard)
}
func (s *identityCardService) GetIdentityCardsByUserID(ctx context.Context, userID string) ([]ResponseIdentityCardDTO, error) {
identityCards, err := s.identityRepo.GetIdentityCardsByUserID(ctx, userID)
if err != nil {
log.Printf("Error fetching identity cards by userID: %v", err)
return nil, fmt.Errorf("failed to fetch identity cards by userID")
}
var response []ResponseIdentityCardDTO
for _, card := range identityCards {
dto, _ := FormatResponseIdentityCard(&card)
response = append(response, *dto)
}
return response, nil
}
func (s *identityCardService) UpdateIdentityCard(ctx context.Context, userID string, id string, request *RequestIdentityCardDTO, cardPhoto *multipart.FileHeader) (*ResponseIdentityCardDTO, error) {
errors, valid := request.ValidateIdentityCardInput()
if !valid {
return nil, fmt.Errorf("validation failed: %v", errors)
}
identityCard, err := s.identityRepo.GetIdentityCardByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("identity card not found: %v", err)
}
if identityCard.Cardphoto != "" {
err := deleteIdentityCardImage(identityCard.Cardphoto)
if err != nil {
return nil, fmt.Errorf("failed to delete old image: %v", err)
}
}
var cardPhotoPath string
if cardPhoto != nil {
cardPhotoPath, err = s.saveIdentityCardImage(userID, cardPhoto)
if err != nil {
return nil, fmt.Errorf("failed to save card photo: %v", err)
}
}
identityCard.Identificationumber = request.Identificationumber
identityCard.Placeofbirth = request.Placeofbirth
identityCard.Dateofbirth = request.Dateofbirth
identityCard.Gender = request.Gender
identityCard.BloodType = request.BloodType
identityCard.Province = request.Province
identityCard.District = request.District
identityCard.SubDistrict = request.SubDistrict
identityCard.Hamlet = request.Hamlet
identityCard.Village = request.Village
identityCard.Neighbourhood = request.Neighbourhood
identityCard.PostalCode = request.PostalCode
identityCard.Religion = request.Religion
identityCard.Maritalstatus = request.Maritalstatus
identityCard.Job = request.Job
identityCard.Citizenship = request.Citizenship
identityCard.Validuntil = request.Validuntil
if cardPhotoPath != "" {
identityCard.Cardphoto = cardPhotoPath
}
if err != nil {
log.Printf("Error updating identity card: %v", err)
return nil, fmt.Errorf("failed to update identity card: %v", err)
}
idcardResponseDTO, _ := FormatResponseIdentityCard(identityCard)
return idcardResponseDTO, nil
}
func (s *identityCardService) GetAllIdentityCardsByRegStatus(ctx context.Context, userRegStatus string) ([]ResponseIdentityCardDTO, error) {
identityCards, err := s.authRepo.GetIdentityCardsByUserRegStatus(ctx, userRegStatus)
if err != nil {
log.Printf("Error getting identity cards by registration status: %v", err)
return nil, fmt.Errorf("failed to get identity cards: %w", err)
}
var response []ResponseIdentityCardDTO
for _, card := range identityCards {
createdAt, _ := utils.FormatDateToIndonesianFormat(card.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(card.UpdatedAt)
dto := ResponseIdentityCardDTO{
ID: card.ID,
UserID: card.UserID,
Identificationumber: card.Identificationumber,
Placeofbirth: card.Placeofbirth,
Dateofbirth: card.Dateofbirth,
Gender: card.Gender,
BloodType: card.BloodType,
Province: card.Province,
District: card.District,
SubDistrict: card.SubDistrict,
Hamlet: card.Hamlet,
Village: card.Village,
Neighbourhood: card.Neighbourhood,
PostalCode: card.PostalCode,
Religion: card.Religion,
Maritalstatus: card.Maritalstatus,
Job: card.Job,
Citizenship: card.Citizenship,
Validuntil: card.Validuntil,
Cardphoto: card.Cardphoto,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
response = append(response, dto)
}
return response, nil
}
func (s *identityCardService) UpdateUserRegistrationStatusByIdentityCard(ctx context.Context, identityCardUserID string, newStatus string) error {
user, err := s.authRepo.FindUserByID(ctx, identityCardUserID)
if err != nil {
log.Printf("Error finding user by ID %s: %v", identityCardUserID, err)
return fmt.Errorf("user not found: %w", err)
}
updates := map[string]interface{}{
"registration_status": newStatus,
"updated_at": time.Now(),
}
switch newStatus {
case utils.RegStatusConfirmed:
updates["registration_progress"] = utils.ProgressDataSubmitted
identityCards, err := s.GetIdentityCardsByUserID(ctx, identityCardUserID)
if err != nil {
log.Printf("Error fetching identity cards for user ID %s: %v", identityCardUserID, err)
return fmt.Errorf("failed to fetch identity card data: %w", err)
}
if len(identityCards) == 0 {
log.Printf("No identity card found for user ID %s", identityCardUserID)
return fmt.Errorf("no identity card found for user")
}
identityCard := identityCards[0]
updates["name"] = identityCard.Fullname
updates["gender"] = identityCard.Gender
updates["dateofbirth"] = identityCard.Dateofbirth
updates["placeofbirth"] = identityCard.District
log.Printf("Syncing user data for ID %s: name=%s, gender=%s, dob=%s, pob=%s",
identityCardUserID, identityCard.Fullname, identityCard.Gender,
identityCard.Dateofbirth, identityCard.District)
case utils.RegStatusRejected:
updates["registration_progress"] = utils.ProgressOTPVerified
}
err = s.authRepo.PatchUser(ctx, user.ID, updates)
if err != nil {
log.Printf("Error updating user registration status for user ID %s: %v", user.ID, err)
return fmt.Errorf("failed to update user registration status: %w", err)
}
log.Printf("Successfully updated registration status for user ID %s to %s", user.ID, newStatus)
if newStatus == utils.RegStatusConfirmed {
log.Printf("User profile data synced successfully for user ID %s", user.ID)
}
return nil
}
func (s *identityCardService) mapIdentityCardToDTO(card model.IdentityCard) ResponseIdentityCardDTO {
createdAt, _ := utils.FormatDateToIndonesianFormat(card.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(card.UpdatedAt)
return ResponseIdentityCardDTO{
ID: card.ID,
UserID: card.UserID,
Identificationumber: card.Identificationumber,
Placeofbirth: card.Placeofbirth,
Dateofbirth: card.Dateofbirth,
Gender: card.Gender,
BloodType: card.BloodType,
Province: card.Province,
District: card.District,
SubDistrict: card.SubDistrict,
Hamlet: card.Hamlet,
Village: card.Village,
Neighbourhood: card.Neighbourhood,
PostalCode: card.PostalCode,
Religion: card.Religion,
Maritalstatus: card.Maritalstatus,
Job: card.Job,
Citizenship: card.Citizenship,
Validuntil: card.Validuntil,
Cardphoto: card.Cardphoto,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}
func (s *identityCardService) mapUserToDTO(user model.User) userprofile.UserProfileResponseDTO {
avatar := ""
if user.Avatar != nil {
avatar = *user.Avatar
}
var roleDTO role.RoleResponseDTO
if user.Role != nil {
roleDTO = role.RoleResponseDTO{
ID: user.Role.ID,
RoleName: user.Role.RoleName,
}
}
createdAt, _ := utils.FormatDateToIndonesianFormat(user.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(user.UpdatedAt)
return userprofile.UserProfileResponseDTO{
ID: user.ID,
Avatar: avatar,
Name: user.Name,
Gender: user.Gender,
Dateofbirth: user.Dateofbirth,
Placeofbirth: user.Placeofbirth,
Phone: user.Phone,
Email: user.Email,
PhoneVerified: user.PhoneVerified,
Role: roleDTO,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
}

View File

@ -1,61 +0,0 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type AddressRepository interface {
CreateAddress(address *model.Address) error
FindAddressByUserID(userID string) ([]model.Address, error)
FindAddressByID(id string) (*model.Address, error)
UpdateAddress(address *model.Address) error
DeleteAddress(id string) error
}
type addressRepository struct {
DB *gorm.DB
}
func NewAddressRepository(db *gorm.DB) AddressRepository {
return &addressRepository{DB: db}
}
func (r *addressRepository) CreateAddress(address *model.Address) error {
return r.DB.Create(address).Error
}
func (r *addressRepository) FindAddressByUserID(userID string) ([]model.Address, error) {
var addresses []model.Address
err := r.DB.Where("user_id = ?", userID).Find(&addresses).Error
if err != nil {
return nil, err
}
return addresses, nil
}
func (r *addressRepository) FindAddressByID(id string) (*model.Address, error) {
var address model.Address
err := r.DB.Where("id = ?", id).First(&address).Error
if err != nil {
return nil, err
}
return &address, nil
}
func (r *addressRepository) UpdateAddress(address *model.Address) error {
err := r.DB.Save(address).Error
if err != nil {
return err
}
return nil
}
func (r *addressRepository) DeleteAddress(id string) error {
err := r.DB.Where("id = ?", id).Delete(&model.Address{}).Error
if err != nil {
return err
}
return nil
}

View File

@ -1,74 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type ArticleRepository interface {
CreateArticle(article *model.Article) error
FindArticleByID(id string) (*model.Article, error)
FindAllArticles(page, limit int) ([]model.Article, int, error)
UpdateArticle(id string, article *model.Article) error
DeleteArticle(id string) error
}
type articleRepository struct {
DB *gorm.DB
}
func NewArticleRepository(db *gorm.DB) ArticleRepository {
return &articleRepository{DB: db}
}
func (r *articleRepository) CreateArticle(article *model.Article) error {
return r.DB.Create(article).Error
}
func (r *articleRepository) FindArticleByID(id string) (*model.Article, error) {
var article model.Article
err := r.DB.Where("id = ?", id).First(&article).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("article with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch article: %v", err)
}
return &article, nil
}
func (r *articleRepository) FindAllArticles(page, limit int) ([]model.Article, int, error) {
var articles []model.Article
var total int64
if err := r.DB.Model(&model.Article{}).Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count articles: %v", err)
}
fmt.Printf("Total Articles Count: %d\n", total)
if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&articles).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
}
} else {
err := r.DB.Find(&articles).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
}
}
return articles, int(total), nil
}
func (r *articleRepository) UpdateArticle(id string, article *model.Article) error {
return r.DB.Model(&model.Article{}).Where("id = ?", id).Updates(article).Error
}
func (r *articleRepository) DeleteArticle(id string) error {
result := r.DB.Delete(&model.Article{}, "id = ?", id)
return result.Error
}

View File

@ -1,82 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type UserRepository interface {
FindByIdentifierAndRole(identifier, roleID string) (*model.User, error)
FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error)
FindByUsername(username string) (*model.User, error)
FindByPhoneAndRole(phone, roleID string) (*model.User, error)
FindByEmailAndRole(email, roleID string) (*model.User, error)
Create(user *model.User) error
}
type userRepository struct {
DB *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{DB: db}
}
func (r *userRepository) FindByIdentifierAndRole(identifier, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Preload("Role").Where("(email = ? OR username = ? OR phone = ?) AND role_id = ?", identifier, identifier, identifier, roleID).First(&user).Error
if err != nil {
return nil, err
}
if user.Role == nil {
return nil, fmt.Errorf("role not found for this user")
}
return &user, nil
}
func (r *userRepository) FindByUsername(username string) (*model.User, error) {
var user model.User
err := r.DB.Where("username = ?", username).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByPhoneAndRole(phone, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Where("phone = ? AND role_id = ?", phone, roleID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmailAndRole(email, roleID string) (*model.User, error) {
var user model.User
err := r.DB.Where("email = ? AND role_id = ?", email, roleID).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) FindByEmailOrUsernameOrPhone(identifier string) (*model.User, error) {
var user model.User
err := r.DB.Where("email = ? OR username = ? OR phone = ?", identifier, identifier, identifier).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) Create(user *model.User) error {
err := r.DB.Create(user).Error
if err != nil {
return err
}
return nil
}

View File

@ -1,69 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type BannerRepository interface {
CreateBanner(banner *model.Banner) error
FindBannerByID(id string) (*model.Banner, error)
FindAllBanners() ([]model.Banner, error)
UpdateBanner(id string, banner *model.Banner) error
DeleteBanner(id string) error
}
type bannerRepository struct {
DB *gorm.DB
}
func NewBannerRepository(db *gorm.DB) BannerRepository {
return &bannerRepository{DB: db}
}
func (r *bannerRepository) CreateBanner(banner *model.Banner) error {
if err := r.DB.Create(banner).Error; err != nil {
return fmt.Errorf("failed to create banner: %v", err)
}
return nil
}
func (r *bannerRepository) FindBannerByID(id string) (*model.Banner, error) {
var banner model.Banner
err := r.DB.Where("id = ?", id).First(&banner).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("banner with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch banner by ID: %v", err)
}
return &banner, nil
}
func (r *bannerRepository) FindAllBanners() ([]model.Banner, error) {
var banners []model.Banner
err := r.DB.Find(&banners).Error
if err != nil {
return nil, fmt.Errorf("failed to fetch banners: %v", err)
}
return banners, nil
}
func (r *bannerRepository) UpdateBanner(id string, banner *model.Banner) error {
err := r.DB.Model(&model.Banner{}).Where("id = ?", id).Updates(banner).Error
if err != nil {
return fmt.Errorf("failed to update banner: %v", err)
}
return nil
}
func (r *bannerRepository) DeleteBanner(id string) error {
result := r.DB.Delete(&model.Banner{}, "id = ?", id)
if result.Error != nil {
return fmt.Errorf("failed to delete banner: %v", result.Error)
}
return nil
}

View File

@ -1,67 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type InitialCointRepository interface {
CreateInitialCoint(coint *model.InitialCoint) error
FindInitialCointByID(id string) (*model.InitialCoint, error)
FindAllInitialCoints() ([]model.InitialCoint, error)
UpdateInitialCoint(id string, coint *model.InitialCoint) error
DeleteInitialCoint(id string) error
}
type initialCointRepository struct {
DB *gorm.DB
}
func NewInitialCointRepository(db *gorm.DB) InitialCointRepository {
return &initialCointRepository{DB: db}
}
func (r *initialCointRepository) CreateInitialCoint(coint *model.InitialCoint) error {
if err := r.DB.Create(coint).Error; err != nil {
return fmt.Errorf("failed to create initial coint: %v", err)
}
return nil
}
func (r *initialCointRepository) FindInitialCointByID(id string) (*model.InitialCoint, error) {
var coint model.InitialCoint
err := r.DB.Where("id = ?", id).First(&coint).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("initial coint with ID %s not found", id)
}
return nil, fmt.Errorf("failed to fetch initial coint by ID: %v", err)
}
return &coint, nil
}
func (r *initialCointRepository) FindAllInitialCoints() ([]model.InitialCoint, error) {
var coints []model.InitialCoint
err := r.DB.Find(&coints).Error
if err != nil {
return nil, fmt.Errorf("failed to fetch initial coints: %v", err)
}
return coints, nil
}
func (r *initialCointRepository) UpdateInitialCoint(id string, coint *model.InitialCoint) error {
err := r.DB.Model(&model.InitialCoint{}).Where("id = ?", id).Updates(coint).Error
if err != nil {
return fmt.Errorf("failed to update initial coint: %v", err)
}
return nil
}
func (r *initialCointRepository) DeleteInitialCoint(id string) error {
result := r.DB.Delete(&model.InitialCoint{}, "id = ?", id)
if result.Error != nil {
return fmt.Errorf("failed to delete initial coint: %v", result.Error)
}
return nil
}

View File

@ -1,37 +0,0 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type RoleRepository interface {
FindByID(id string) (*model.Role, error)
FindAll() ([]model.Role, error)
}
type roleRepository struct {
DB *gorm.DB
}
func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{DB: db}
}
func (r *roleRepository) FindByID(id string) (*model.Role, error) {
var role model.Role
err := r.DB.Where("id = ?", id).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindAll() ([]model.Role, error) {
var roles []model.Role
err := r.DB.Find(&roles).Error
if err != nil {
return nil, err
}
return roles, nil
}

View File

@ -1,105 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type TrashRepository interface {
CreateCategory(category *model.TrashCategory) error
AddDetailToCategory(detail *model.TrashDetail) error
GetCategories() ([]model.TrashCategory, error)
GetCategoryByID(id string) (*model.TrashCategory, error)
GetTrashDetailByID(id string) (*model.TrashDetail, error)
GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error)
UpdateCategoryName(id string, newName string) error
UpdateTrashDetail(id string, description string, price float64) error
DeleteCategory(id string) error
DeleteTrashDetail(id string) error
}
type trashRepository struct {
DB *gorm.DB
}
func NewTrashRepository(db *gorm.DB) TrashRepository {
return &trashRepository{DB: db}
}
func (r *trashRepository) CreateCategory(category *model.TrashCategory) error {
if err := r.DB.Create(category).Error; err != nil {
return fmt.Errorf("failed to create category: %v", err)
}
return nil
}
func (r *trashRepository) AddDetailToCategory(detail *model.TrashDetail) error {
if err := r.DB.Create(detail).Error; err != nil {
return fmt.Errorf("failed to add detail to category: %v", err)
}
return nil
}
func (r *trashRepository) GetCategories() ([]model.TrashCategory, error) {
var categories []model.TrashCategory
if err := r.DB.Preload("Details").Find(&categories).Error; err != nil {
return nil, fmt.Errorf("failed to fetch categories: %v", err)
}
return categories, nil
}
func (r *trashRepository) GetCategoryByID(id string) (*model.TrashCategory, error) {
var category model.TrashCategory
if err := r.DB.Preload("Details").First(&category, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("category not found: %v", err)
}
return &category, nil
}
func (r *trashRepository) GetTrashDetailByID(id string) (*model.TrashDetail, error) {
var detail model.TrashDetail
if err := r.DB.First(&detail, "id = ?", id).Error; err != nil {
return nil, fmt.Errorf("trash detail not found: %v", err)
}
return &detail, nil
}
func (r *trashRepository) GetDetailsByCategoryID(categoryID string) ([]model.TrashDetail, error) {
var details []model.TrashDetail
if err := r.DB.Where("category_id = ?", categoryID).Find(&details).Error; err != nil {
return nil, fmt.Errorf("failed to fetch details for category %s: %v", categoryID, err)
}
return details, nil
}
func (r *trashRepository) UpdateCategoryName(id string, newName string) error {
if err := r.DB.Model(&model.TrashCategory{}).Where("id = ?", id).Update("name", newName).Error; err != nil {
return fmt.Errorf("failed to update category name: %v", err)
}
return nil
}
func (r *trashRepository) UpdateTrashDetail(id string, description string, price float64) error {
if err := r.DB.Model(&model.TrashDetail{}).Where("id = ?", id).Updates(model.TrashDetail{Description: description, Price: price}).Error; err != nil {
return fmt.Errorf("failed to update trash detail: %v", err)
}
return nil
}
func (r *trashRepository) DeleteCategory(id string) error {
if err := r.DB.Delete(&model.TrashCategory{}, "id = ?", id).Error; err != nil {
return fmt.Errorf("failed to delete category: %v", err)
}
return nil
}
func (r *trashRepository) DeleteTrashDetail(id string) error {
if err := r.DB.Delete(&model.TrashDetail{}, "id = ?", id).Error; err != nil {
return fmt.Errorf("failed to delete trash detail: %v", err)
}
return nil
}

View File

@ -1,56 +0,0 @@
package repositories
import (
"fmt"
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type UserProfileRepository interface {
FindByID(userID string) (*model.User, error)
Update(user *model.User) error
UpdateAvatar(userID, avatarURL string) error
}
type userProfileRepository struct {
DB *gorm.DB
}
func NewUserProfileRepository(db *gorm.DB) UserProfileRepository {
return &userProfileRepository{DB: db}
}
func (r *userProfileRepository) FindByID(userID string) (*model.User, error) {
var user model.User
err := r.DB.Preload("Role").Where("id = ?", userID).First(&user).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("user with ID %s not found", userID)
}
return nil, err
}
if user.Role == nil {
return nil, fmt.Errorf("role not found for this user")
}
return &user, nil
}
func (r *userProfileRepository) Update(user *model.User) error {
err := r.DB.Save(user).Error
if err != nil {
return err
}
return nil
}
func (r *userProfileRepository) UpdateAvatar(userID, avatarURL string) error {
var user model.User
err := r.DB.Model(&user).Where("id = ?", userID).Update("avatar", avatarURL).Error
if err != nil {
return err
}
return nil
}

View File

@ -1,59 +0,0 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type UserPinRepository interface {
FindByUserID(userID string) (*model.UserPin, error)
FindByPin(userPin string) (*model.UserPin, error)
Create(userPin *model.UserPin) error
Update(userPin *model.UserPin) error
}
type userPinRepository struct {
DB *gorm.DB
}
func NewUserPinRepository(db *gorm.DB) UserPinRepository {
return &userPinRepository{DB: db}
}
func (r *userPinRepository) FindByUserID(userID string) (*model.UserPin, error) {
var userPin model.UserPin
err := r.DB.Where("user_id = ?", userID).First(&userPin).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil
}
return nil, err
}
return &userPin, nil
}
func (r *userPinRepository) FindByPin(pin string) (*model.UserPin, error) {
var userPin model.UserPin
err := r.DB.Where("pin = ?", pin).First(&userPin).Error
if err != nil {
return nil, err
}
return &userPin, nil
}
func (r *userPinRepository) Create(userPin *model.UserPin) error {
err := r.DB.Create(userPin).Error
if err != nil {
return err
}
return nil
}
func (r *userPinRepository) Update(userPin *model.UserPin) error {
err := r.DB.Save(userPin).Error
if err != nil {
return err
}
return nil
}

View File

@ -1,243 +0,0 @@
package repositories
import (
"github.com/pahmiudahgede/senggoldong/model"
"gorm.io/gorm"
)
type WilayahIndonesiaRepository interface {
ImportProvinces(provinces []model.Province) error
ImportRegencies(regencies []model.Regency) error
ImportDistricts(districts []model.District) error
ImportVillages(villages []model.Village) error
FindAllProvinces(page, limit int) ([]model.Province, int, error)
FindProvinceByID(id string, page, limit int) (*model.Province, int, error)
FindAllRegencies(page, limit int) ([]model.Regency, int, error)
FindRegencyByID(id string, page, limit int) (*model.Regency, int, error)
FindAllDistricts(page, limit int) ([]model.District, int, error)
FindDistrictByID(id string, page, limit int) (*model.District, int, error)
FindAllVillages(page, limit int) ([]model.Village, int, error)
FindVillageByID(id string) (*model.Village, error)
}
type wilayahIndonesiaRepository struct {
DB *gorm.DB
}
func NewWilayahIndonesiaRepository(db *gorm.DB) WilayahIndonesiaRepository {
return &wilayahIndonesiaRepository{DB: db}
}
func (r *wilayahIndonesiaRepository) ImportProvinces(provinces []model.Province) error {
for _, province := range provinces {
if err := r.DB.Create(&province).Error; err != nil {
return err
}
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportRegencies(regencies []model.Regency) error {
for _, regency := range regencies {
if err := r.DB.Create(&regency).Error; err != nil {
return err
}
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportDistricts(districts []model.District) error {
for _, district := range districts {
if err := r.DB.Create(&district).Error; err != nil {
return err
}
}
return nil
}
func (r *wilayahIndonesiaRepository) ImportVillages(villages []model.Village) error {
for _, village := range villages {
if err := r.DB.Create(&village).Error; err != nil {
return err
}
}
return nil
}
func (r *wilayahIndonesiaRepository) FindAllProvinces(page, limit int) ([]model.Province, int, error) {
var provinces []model.Province
var total int64
err := r.DB.Model(&model.Province{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&provinces).Error
if err != nil {
return nil, 0, err
}
} else {
err := r.DB.Find(&provinces).Error
if err != nil {
return nil, 0, err
}
}
return provinces, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindProvinceByID(id string, page, limit int) (*model.Province, int, error) {
var province model.Province
err := r.DB.Preload("Regencies", func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}).Where("id = ?", id).First(&province).Error
if err != nil {
return nil, 0, err
}
var totalRegencies int64
r.DB.Model(&model.Regency{}).Where("province_id = ?", id).Count(&totalRegencies)
return &province, int(totalRegencies), nil
}
func (r *wilayahIndonesiaRepository) FindAllRegencies(page, limit int) ([]model.Regency, int, error) {
var regencies []model.Regency
var total int64
err := r.DB.Model(&model.Regency{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&regencies).Error
if err != nil {
return nil, 0, err
}
} else {
err := r.DB.Find(&regencies).Error
if err != nil {
return nil, 0, err
}
}
return regencies, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindRegencyByID(id string, page, limit int) (*model.Regency, int, error) {
var regency model.Regency
err := r.DB.Preload("Districts", func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}).Where("id = ?", id).First(&regency).Error
if err != nil {
return nil, 0, err
}
var totalDistricts int64
err = r.DB.Model(&model.District{}).Where("regency_id = ?", id).Count(&totalDistricts).Error
if err != nil {
return nil, 0, err
}
return &regency, int(totalDistricts), nil
}
func (r *wilayahIndonesiaRepository) FindAllDistricts(page, limit int) ([]model.District, int, error) {
var district []model.District
var total int64
err := r.DB.Model(&model.District{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&district).Error
if err != nil {
return nil, 0, err
}
} else {
err := r.DB.Find(&district).Error
if err != nil {
return nil, 0, err
}
}
return district, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindDistrictByID(id string, page, limit int) (*model.District, int, error) {
var district model.District
err := r.DB.Preload("Villages", func(db *gorm.DB) *gorm.DB {
if page > 0 && limit > 0 {
return db.Offset((page - 1) * limit).Limit(limit)
}
return db
}).Where("id = ?", id).First(&district).Error
if err != nil {
return nil, 0, err
}
var totalVillage int64
r.DB.Model(&model.Village{}).Where("district_id = ?", id).Count(&totalVillage)
return &district, int(totalVillage), nil
}
func (r *wilayahIndonesiaRepository) FindAllVillages(page, limit int) ([]model.Village, int, error) {
var villages []model.Village
var total int64
err := r.DB.Model(&model.Village{}).Count(&total).Error
if err != nil {
return nil, 0, err
}
if page > 0 && limit > 0 {
err := r.DB.Offset((page - 1) * limit).Limit(limit).Find(&villages).Error
if err != nil {
return nil, 0, err
}
} else {
err := r.DB.Find(&villages).Error
if err != nil {
return nil, 0, err
}
}
return villages, int(total), nil
}
func (r *wilayahIndonesiaRepository) FindVillageByID(id string) (*model.Village, error) {
var village model.Village
err := r.DB.Where("id = ?", id).First(&village).Error
if err != nil {
return nil, err
}
return &village, nil
}

View File

@ -0,0 +1,34 @@
package requestpickup
import (
"context"
"rijig/config"
"rijig/model"
)
type PickupStatusHistoryRepository interface {
CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error
GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error)
}
type pickupStatusHistoryRepository struct{}
func NewPickupStatusHistoryRepository() PickupStatusHistoryRepository {
return &pickupStatusHistoryRepository{}
}
func (r *pickupStatusHistoryRepository) CreateStatusHistory(ctx context.Context, history model.PickupStatusHistory) error {
return config.DB.WithContext(ctx).Create(&history).Error
}
func (r *pickupStatusHistoryRepository) GetStatusHistoryByRequestID(ctx context.Context, requestID string) ([]model.PickupStatusHistory, error) {
var histories []model.PickupStatusHistory
err := config.DB.WithContext(ctx).
Where("request_id = ?", requestID).
Order("changed_at asc").
Find(&histories).Error
if err != nil {
return nil, err
}
return histories, nil
}

View File

@ -0,0 +1,146 @@
package requestpickup
import (
"context"
"fmt"
"rijig/internal/collector"
"rijig/utils"
)
type PickupMatchingService interface {
FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]collector.NearbyCollectorDTO, error)
FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]PickupRequestForCollectorDTO, error)
}
type pickupMatchingService struct {
pickupRepo RequestPickupRepository
collectorRepo collector.CollectorRepository
}
func NewPickupMatchingService(pickupRepo RequestPickupRepository,
collectorRepo collector.CollectorRepository) PickupMatchingService {
return &pickupMatchingService{
pickupRepo: pickupRepo,
collectorRepo: collectorRepo,
}
}
func (s *pickupMatchingService) FindNearbyCollectorsForPickup(ctx context.Context, pickupID string) ([]collector.NearbyCollectorDTO, error) {
pickup, err := s.pickupRepo.GetPickupWithItemsAndAddress(ctx, pickupID)
if err != nil {
return nil, fmt.Errorf("pickup tidak ditemukan: %w", err)
}
userCoord := utils.Coord{
Lat: pickup.Address.Latitude,
Lon: pickup.Address.Longitude,
}
requestedTrash := make(map[string]bool)
for _, item := range pickup.RequestItems {
requestedTrash[item.TrashCategoryId] = true
}
collectors, err := s.collectorRepo.GetActiveCollectorsWithTrashAndAddress(ctx)
if err != nil {
return nil, fmt.Errorf("gagal mengambil data collector: %w", err)
}
var result []collector.NearbyCollectorDTO
for _, col := range collectors {
coord := utils.Coord{
Lat: col.Address.Latitude,
Lon: col.Address.Longitude,
}
_, km := utils.Distance(userCoord, coord)
if km > 10 {
continue
}
var matchedTrash []string
for _, item := range col.AvaibleTrashByCollector {
if requestedTrash[item.TrashCategoryID] {
matchedTrash = append(matchedTrash, item.TrashCategoryID)
}
}
if len(matchedTrash) == 0 {
continue
}
result = append(result, collector.NearbyCollectorDTO{
CollectorID: col.ID,
Name: col.User.Name,
Phone: col.User.Phone,
Rating: col.Rating,
Latitude: col.Address.Latitude,
Longitude: col.Address.Longitude,
DistanceKm: km,
MatchedTrash: matchedTrash,
})
}
return result, nil
}
// terdpaat error seperti ini: "undefined: dto.PickupRequestForCollectorDTO" dan seprti ini: s.collectorRepo.GetCollectorWithAddressAndTrash undefined (type repositories.CollectorRepository has no field or method GetCollectorWithAddressAndTrash) pada kode berikut:
func (s *pickupMatchingService) FindAvailableRequestsForCollector(ctx context.Context, collectorID string) ([]PickupRequestForCollectorDTO, error) {
collector, err := s.collectorRepo.GetCollectorWithAddressAndTrash(ctx, collectorID)
if err != nil {
return nil, fmt.Errorf("collector tidak ditemukan: %w", err)
}
pickupList, err := s.pickupRepo.GetAllAutomaticRequestsWithAddress(ctx)
if err != nil {
return nil, fmt.Errorf("gagal mengambil pickup otomatis: %w", err)
}
collectorCoord := utils.Coord{
Lat: collector.Address.Latitude,
Lon: collector.Address.Longitude,
}
// map trash collector
collectorTrash := make(map[string]bool)
for _, t := range collector.AvaibleTrashByCollector {
collectorTrash[t.TrashCategoryID] = true
}
var results []PickupRequestForCollectorDTO
for _, p := range pickupList {
if p.StatusPickup != "waiting_collector" {
continue
}
coord := utils.Coord{
Lat: p.Address.Latitude,
Lon: p.Address.Longitude,
}
_, km := utils.Distance(collectorCoord, coord)
if km > 10 {
continue
}
match := false
var matchedTrash []string
for _, item := range p.RequestItems {
if collectorTrash[item.TrashCategoryId] {
match = true
matchedTrash = append(matchedTrash, item.TrashCategoryId)
}
}
if match {
results = append(results, PickupRequestForCollectorDTO{
PickupID: p.ID,
UserID: p.UserId,
Latitude: p.Address.Latitude,
Longitude: p.Address.Longitude,
DistanceKm: km,
MatchedTrash: matchedTrash,
})
}
}
return results, nil
}

View File

@ -0,0 +1,50 @@
package requestpickup
import (
"context"
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type PickupMatchingHandler struct {
service PickupMatchingService
}
func NewPickupMatchingHandler(service PickupMatchingService) *PickupMatchingHandler {
return &PickupMatchingHandler{
service: service,
}
}
func (h *PickupMatchingHandler) GetNearbyCollectorsForPickup(c *fiber.Ctx) error {
pickupID := c.Params("pickupID")
if pickupID == "" {
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validasi gagal", map[string][]string{
"pickup_id": {"pickup ID harus disertakan"},
})
}
collectors, err := h.service.FindNearbyCollectorsForPickup(context.Background(), pickupID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Data collector terdekat berhasil diambil", collectors)
}
func (h *PickupMatchingHandler) GetAvailablePickupForCollector(c *fiber.Ctx) error {
claims, err := middleware.GetUserFromContext(c)
if err != nil {
return err
}
pickups, err := h.service.FindAvailableRequestsForCollector(context.Background(), claims.UserID)
if err != nil {
return utils.InternalServerError(c, err.Error())
}
return utils.SuccessWithData(c, "Data request pickup otomatis berhasil diambil", pickups)
}

View File

@ -0,0 +1,74 @@
package requestpickup
import (
"strings"
)
type SelectCollectorDTO struct {
CollectorID string `json:"collector_id"`
}
type UpdateRequestPickupItemDTO struct {
ItemID string `json:"item_id"`
Amount float64 `json:"actual_amount"`
}
type UpdatePickupItemsRequest struct {
Items []UpdateRequestPickupItemDTO `json:"items"`
}
func (r *SelectCollectorDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.CollectorID) == "" {
errors["collector_id"] = append(errors["collector_id"], "collector_id tidak boleh kosong")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}
type AssignedPickupDTO struct {
PickupID string `json:"pickup_id"`
UserID string `json:"user_id"`
UserName string `json:"user_name"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Notes string `json:"notes"`
MatchedTrash []string `json:"matched_trash"`
}
type PickupRequestForCollectorDTO struct {
PickupID string `json:"pickup_id"`
UserID string `json:"user_id"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
DistanceKm float64 `json:"distance_km"`
MatchedTrash []string `json:"matched_trash"`
}
type RequestPickupDTO struct {
AddressID string `json:"address_id"`
RequestMethod string `json:"request_method"`
Notes string `json:"notes,omitempty"`
}
func (r *RequestPickupDTO) Validate() (map[string][]string, bool) {
errors := make(map[string][]string)
if strings.TrimSpace(r.AddressID) == "" {
errors["address_id"] = append(errors["address_id"], "alamat harus dipilih")
}
method := strings.ToLower(strings.TrimSpace(r.RequestMethod))
if method != "manual" && method != "otomatis" {
errors["request_method"] = append(errors["request_method"], "harus manual atau otomatis")
}
if len(errors) > 0 {
return errors, false
}
return nil, true
}

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1,142 @@
package requestpickup
import (
"context"
"rijig/config"
"rijig/model"
"time"
)
type RequestPickupRepository interface {
CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error
GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error)
GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error)
UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error
GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error)
UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error
UpdatePickupStatus(ctx context.Context, pickupID string, status string) error
UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []UpdateRequestPickupItemDTO) error
}
type requestPickupRepository struct{}
func NewRequestPickupRepository() RequestPickupRepository {
return &requestPickupRepository{}
}
func (r *requestPickupRepository) CreateRequestPickup(ctx context.Context, pickup *model.RequestPickup) error {
return config.DB.WithContext(ctx).Create(pickup).Error
}
func (r *requestPickupRepository) GetPickupWithItemsAndAddress(ctx context.Context, id string) (*model.RequestPickup, error) {
var pickup model.RequestPickup
err := config.DB.WithContext(ctx).
Preload("RequestItems").
Preload("Address").
Where("id = ?", id).
First(&pickup).Error
if err != nil {
return nil, err
}
return &pickup, nil
}
func (r *requestPickupRepository) UpdateCollectorID(ctx context.Context, pickupID, collectorID string) error {
return config.DB.WithContext(ctx).
Model(&model.RequestPickup{}).
Where("id = ?", pickupID).
Update("collector_id", collectorID).
Error
}
func (r *requestPickupRepository) GetAllAutomaticRequestsWithAddress(ctx context.Context) ([]model.RequestPickup, error) {
var pickups []model.RequestPickup
err := config.DB.WithContext(ctx).
Preload("RequestItems").
Preload("Address").
Where("request_method = ?", "otomatis").
Find(&pickups).Error
if err != nil {
return nil, err
}
return pickups, nil
}
func (r *requestPickupRepository) GetRequestsAssignedToCollector(ctx context.Context, collectorID string) ([]model.RequestPickup, error) {
var pickups []model.RequestPickup
err := config.DB.WithContext(ctx).
Preload("User").
Preload("Address").
Preload("RequestItems").
Where("collector_id = ? AND status_pickup = ?", collectorID, "waiting_collector").
Find(&pickups).Error
if err != nil {
return nil, err
}
return pickups, nil
}
func (r *requestPickupRepository) UpdatePickupStatusAndConfirmationTime(ctx context.Context, pickupID string, status string, confirmedAt time.Time) error {
return config.DB.WithContext(ctx).
Model(&model.RequestPickup{}).
Where("id = ?", pickupID).
Updates(map[string]interface{}{
"status_pickup": status,
"confirmed_by_collector_at": confirmedAt,
}).Error
}
func (r *requestPickupRepository) UpdatePickupStatus(ctx context.Context, pickupID string, status string) error {
return config.DB.WithContext(ctx).
Model(&model.RequestPickup{}).
Where("id = ?", pickupID).
Update("status_pickup", status).
Error
}
func (r *requestPickupRepository) UpdateRequestPickupItemsAmountAndPrice(ctx context.Context, pickupID string, items []UpdateRequestPickupItemDTO) error {
// ambil collector_id dulu dari pickup
var pickup model.RequestPickup
if err := config.DB.WithContext(ctx).
Select("collector_id").
Where("id = ?", pickupID).
First(&pickup).Error; err != nil {
return err
}
for _, item := range items {
var pickupItem model.RequestPickupItem
err := config.DB.WithContext(ctx).
Where("id = ? AND request_pickup_id = ?", item.ItemID, pickupID).
First(&pickupItem).Error
if err != nil {
return err
}
var price float64
err = config.DB.WithContext(ctx).
Model(&model.AvaibleTrashByCollector{}).
Where("collector_id = ? AND trash_category_id = ?", pickup.CollectorID, pickupItem.TrashCategoryId).
Select("price").
Scan(&price).Error
if err != nil {
return err
}
finalPrice := item.Amount * price
err = config.DB.WithContext(ctx).
Model(&model.RequestPickupItem{}).
Where("id = ?", item.ItemID).
Updates(map[string]interface{}{
"estimated_amount": item.Amount,
"final_price": finalPrice,
}).Error
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -0,0 +1 @@
package requestpickup

View File

@ -1,4 +1,4 @@
package dto
package role
type RoleResponseDTO struct {
ID string `json:"role_id"`

View File

@ -0,0 +1,52 @@
package role
import (
"rijig/middleware"
"rijig/utils"
"github.com/gofiber/fiber/v2"
)
type RoleHandler struct {
roleService RoleService
}
func NewRoleHandler(roleService RoleService) *RoleHandler {
return &RoleHandler{
roleService: roleService,
}
}
func (h *RoleHandler) GetRoles(c *fiber.Ctx) error {
if _, err := middleware.GetUserFromContext(c); err != nil {
return utils.Unauthorized(c, "Unauthorized access")
}
roles, err := h.roleService.GetRoles(c.Context())
if err != nil {
return utils.InternalServerError(c, "Failed to fetch roles")
}
return utils.SuccessWithData(c, "Roles fetched successfully", roles)
}
func (h *RoleHandler) GetRoleByID(c *fiber.Ctx) error {
if _, err := middleware.GetUserFromContext(c); err != nil {
return utils.Unauthorized(c, "Unauthorized access")
}
roleID := c.Params("role_id")
if roleID == "" {
return utils.BadRequest(c, "Role ID is required")
}
role, err := h.roleService.GetRoleByID(c.Context(), roleID)
if err != nil {
return utils.NotFound(c, "Role not found")
}
return utils.SuccessWithData(c, "Role fetched successfully", role)
}

View File

@ -0,0 +1,49 @@
package role
import (
"context"
"rijig/model"
"gorm.io/gorm"
)
type RoleRepository interface {
FindByID(ctx context.Context, id string) (*model.Role, error)
FindRoleByName(ctx context.Context, roleName string) (*model.Role, error)
FindAll(ctx context.Context) ([]model.Role, error)
}
type roleRepository struct {
db *gorm.DB
}
func NewRoleRepository(db *gorm.DB) RoleRepository {
return &roleRepository{db}
}
func (r *roleRepository) FindByID(ctx context.Context, id string) (*model.Role, error) {
var role model.Role
err := r.db.WithContext(ctx).Where("id = ?", id).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindRoleByName(ctx context.Context, roleName string) (*model.Role, error) {
var role model.Role
err := r.db.WithContext(ctx).Where("role_name = ?", roleName).First(&role).Error
if err != nil {
return nil, err
}
return &role, nil
}
func (r *roleRepository) FindAll(ctx context.Context) ([]model.Role, error) {
var roles []model.Role
err := r.db.WithContext(ctx).Find(&roles).Error
if err != nil {
return nil, err
}
return roles, nil
}

View File

@ -0,0 +1,18 @@
package role
import (
"rijig/config"
"rijig/middleware"
"github.com/gofiber/fiber/v2"
)
func UserRoleRouter(api fiber.Router) {
roleRepo := NewRoleRepository(config.DB)
roleService := NewRoleService(roleRepo)
roleHandler := NewRoleHandler(roleService)
roleRoute := api.Group("/role", middleware.AuthMiddleware())
roleRoute.Get("/", roleHandler.GetRoles)
roleRoute.Get("/:role_id", roleHandler.GetRoleByID)
}

View File

@ -0,0 +1,89 @@
package role
import (
"context"
"fmt"
"time"
"rijig/utils"
)
type RoleService interface {
GetRoles(ctx context.Context) ([]RoleResponseDTO, error)
GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error)
}
type roleService struct {
RoleRepo RoleRepository
}
func NewRoleService(roleRepo RoleRepository) RoleService {
return &roleService{roleRepo}
}
func (s *roleService) GetRoles(ctx context.Context) ([]RoleResponseDTO, error) {
cacheKey := "roles_list"
var cachedRoles []RoleResponseDTO
err := utils.GetCache(cacheKey, &cachedRoles)
if err == nil {
return cachedRoles, nil
}
roles, err := s.RoleRepo.FindAll(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch roles: %v", err)
}
var roleDTOs []RoleResponseDTO
for _, role := range roles {
createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt)
roleDTOs = append(roleDTOs, RoleResponseDTO{
ID: role.ID,
RoleName: role.RoleName,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
err = utils.SetCache(cacheKey, roleDTOs, time.Hour*24)
if err != nil {
fmt.Printf("Error caching roles data to Redis: %v\n", err)
}
return roleDTOs, nil
}
func (s *roleService) GetRoleByID(ctx context.Context, roleID string) (*RoleResponseDTO, error) {
cacheKey := fmt.Sprintf("role:%s", roleID)
var cachedRole RoleResponseDTO
err := utils.GetCache(cacheKey, &cachedRole)
if err == nil {
return &cachedRole, nil
}
role, err := s.RoleRepo.FindByID(ctx, roleID)
if err != nil {
return nil, fmt.Errorf("role not found: %v", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(role.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(role.UpdatedAt)
roleDTO := &RoleResponseDTO{
ID: role.ID,
RoleName: role.RoleName,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
err = utils.SetCache(cacheKey, roleDTO, time.Hour*24)
if err != nil {
fmt.Printf("Error caching role data to Redis: %v\n", err)
}
return roleDTO, nil
}

View File

@ -1,402 +0,0 @@
package services
import (
"fmt"
"time"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/model"
"github.com/pahmiudahgede/senggoldong/utils"
)
type AddressService interface {
CreateAddress(userID string, request dto.CreateAddressDTO) (*dto.AddressResponseDTO, error)
GetAddressByUserID(userID string) ([]dto.AddressResponseDTO, error)
GetAddressByID(userID, id string) (*dto.AddressResponseDTO, error)
UpdateAddress(userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error)
DeleteAddress(userID, id string) error
}
type addressService struct {
AddressRepo repositories.AddressRepository
WilayahRepo repositories.WilayahIndonesiaRepository
}
func NewAddressService(addressRepo repositories.AddressRepository, wilayahRepo repositories.WilayahIndonesiaRepository) AddressService {
return &addressService{
AddressRepo: addressRepo,
WilayahRepo: wilayahRepo,
}
}
func (s *addressService) CreateAddress(userID string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) {
province, _, err := s.WilayahRepo.FindProvinceByID(addressDTO.Province, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid province_id")
}
regency, _, err := s.WilayahRepo.FindRegencyByID(addressDTO.Regency, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid regency_id")
}
district, _, err := s.WilayahRepo.FindDistrictByID(addressDTO.District, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid district_id")
}
village, err := s.WilayahRepo.FindVillageByID(addressDTO.Village)
if err != nil {
return nil, fmt.Errorf("invalid village_id")
}
address := model.Address{
UserID: userID,
Province: province.Name,
Regency: regency.Name,
District: district.Name,
Village: village.Name,
PostalCode: addressDTO.PostalCode,
Detail: addressDTO.Detail,
Geography: addressDTO.Geography,
}
err = s.AddressRepo.CreateAddress(&address)
if err != nil {
return nil, fmt.Errorf("failed to create address: %v", err)
}
userCacheKey := fmt.Sprintf("user:%s:addresses", userID)
utils.DeleteData(userCacheKey)
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
addressResponseDTO := &dto.AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Geography: address.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
cacheKey := fmt.Sprintf("address:%s", address.ID)
cacheData := map[string]interface{}{
"data": addressResponseDTO,
}
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching new address to Redis: %v\n", err)
}
addresses, err := s.AddressRepo.FindAddressByUserID(userID)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated addresses for user: %v", err)
}
var addressDTOs []dto.AddressResponseDTO
for _, addr := range addresses {
createdAt, _ := utils.FormatDateToIndonesianFormat(addr.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(addr.UpdatedAt)
addressDTOs = append(addressDTOs, dto.AddressResponseDTO{
UserID: addr.UserID,
ID: addr.ID,
Province: addr.Province,
Regency: addr.Regency,
District: addr.District,
Village: addr.Village,
PostalCode: addr.PostalCode,
Detail: addr.Detail,
Geography: addr.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
cacheData = map[string]interface{}{
"data": addressDTOs,
}
err = utils.SetJSONData(userCacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching updated user addresses to Redis: %v\n", err)
}
return addressResponseDTO, nil
}
func (s *addressService) GetAddressByUserID(userID string) ([]dto.AddressResponseDTO, error) {
cacheKey := fmt.Sprintf("user:%s:addresses", userID)
cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil {
var addresses []dto.AddressResponseDTO
if data, ok := cachedData["data"].([]interface{}); ok {
for _, item := range data {
addressData, ok := item.(map[string]interface{})
if ok {
addresses = append(addresses, dto.AddressResponseDTO{
UserID: addressData["user_id"].(string),
ID: addressData["address_id"].(string),
Province: addressData["province"].(string),
Regency: addressData["regency"].(string),
District: addressData["district"].(string),
Village: addressData["village"].(string),
PostalCode: addressData["postalCode"].(string),
Detail: addressData["detail"].(string),
Geography: addressData["geography"].(string),
CreatedAt: addressData["createdAt"].(string),
UpdatedAt: addressData["updatedAt"].(string),
})
}
}
return addresses, nil
}
}
addresses, err := s.AddressRepo.FindAddressByUserID(userID)
if err != nil {
return nil, fmt.Errorf("failed to fetch addresses: %v", err)
}
var addressDTOs []dto.AddressResponseDTO
for _, address := range addresses {
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
addressDTOs = append(addressDTOs, dto.AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Geography: address.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
cacheData := map[string]interface{}{
"data": addressDTOs,
}
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching addresses to Redis: %v\n", err)
}
return addressDTOs, nil
}
func (s *addressService) GetAddressByID(userID, id string) (*dto.AddressResponseDTO, error) {
address, err := s.AddressRepo.FindAddressByID(id)
if err != nil {
return nil, fmt.Errorf("address not found: %v", err)
}
if address.UserID != userID {
return nil, fmt.Errorf("you are not authorized to update this address")
}
cacheKey := fmt.Sprintf("address:%s", id)
cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil {
addressData, ok := cachedData["data"].(map[string]interface{})
if ok {
address := dto.AddressResponseDTO{
UserID: addressData["user_id"].(string),
ID: addressData["address_id"].(string),
Province: addressData["province"].(string),
Regency: addressData["regency"].(string),
District: addressData["district"].(string),
Village: addressData["village"].(string),
PostalCode: addressData["postalCode"].(string),
Detail: addressData["detail"].(string),
Geography: addressData["geography"].(string),
CreatedAt: addressData["createdAt"].(string),
UpdatedAt: addressData["updatedAt"].(string),
}
return &address, nil
}
}
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
addressDTO := &dto.AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Geography: address.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
cacheData := map[string]interface{}{
"data": addressDTO,
}
err = utils.SetJSONData(cacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching address to Redis: %v\n", err)
}
return addressDTO, nil
}
func (s *addressService) UpdateAddress(userID, id string, addressDTO dto.CreateAddressDTO) (*dto.AddressResponseDTO, error) {
address, err := s.AddressRepo.FindAddressByID(id)
if err != nil {
return nil, fmt.Errorf("address not found: %v", err)
}
if address.UserID != userID {
return nil, fmt.Errorf("you are not authorized to update this address")
}
province, _, err := s.WilayahRepo.FindProvinceByID(addressDTO.Province, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid province_id")
}
regency, _, err := s.WilayahRepo.FindRegencyByID(addressDTO.Regency, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid regency_id")
}
district, _, err := s.WilayahRepo.FindDistrictByID(addressDTO.District, 0, 0)
if err != nil {
return nil, fmt.Errorf("invalid district_id")
}
village, err := s.WilayahRepo.FindVillageByID(addressDTO.Village)
if err != nil {
return nil, fmt.Errorf("invalid village_id")
}
address.Province = province.Name
address.Regency = regency.Name
address.District = district.Name
address.Village = village.Name
address.PostalCode = addressDTO.PostalCode
address.Detail = addressDTO.Detail
address.Geography = addressDTO.Geography
address.UpdatedAt = time.Now()
err = s.AddressRepo.UpdateAddress(address)
if err != nil {
return nil, fmt.Errorf("failed to update address: %v", err)
}
addressCacheKey := fmt.Sprintf("address:%s", id)
utils.DeleteData(addressCacheKey)
userAddressesCacheKey := fmt.Sprintf("user:%s:addresses", userID)
utils.DeleteData(userAddressesCacheKey)
createdAt, _ := utils.FormatDateToIndonesianFormat(address.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(address.UpdatedAt)
addressResponseDTO := &dto.AddressResponseDTO{
UserID: address.UserID,
ID: address.ID,
Province: address.Province,
Regency: address.Regency,
District: address.District,
Village: address.Village,
PostalCode: address.PostalCode,
Detail: address.Detail,
Geography: address.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
}
cacheData := map[string]interface{}{
"data": addressResponseDTO,
}
err = utils.SetJSONData(addressCacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching updated address to Redis: %v\n", err)
}
addresses, err := s.AddressRepo.FindAddressByUserID(address.UserID)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated addresses for user: %v", err)
}
var addressDTOs []dto.AddressResponseDTO
for _, addr := range addresses {
createdAt, _ := utils.FormatDateToIndonesianFormat(addr.CreatedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(addr.UpdatedAt)
addressDTOs = append(addressDTOs, dto.AddressResponseDTO{
UserID: addr.UserID,
ID: addr.ID,
Province: addr.Province,
Regency: addr.Regency,
District: addr.District,
Village: addr.Village,
PostalCode: addr.PostalCode,
Detail: addr.Detail,
Geography: addr.Geography,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
})
}
cacheData = map[string]interface{}{
"data": addressDTOs,
}
err = utils.SetJSONData(userAddressesCacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching updated user addresses to Redis: %v\n", err)
}
return addressResponseDTO, nil
}
func (s *addressService) DeleteAddress(userID, addressID string) error {
address, err := s.AddressRepo.FindAddressByID(addressID)
if err != nil {
return fmt.Errorf("address not found: %v", err)
}
if address.UserID != userID {
return fmt.Errorf("you are not authorized to delete this address")
}
err = s.AddressRepo.DeleteAddress(addressID)
if err != nil {
return fmt.Errorf("failed to delete address: %v", err)
}
addressCacheKey := fmt.Sprintf("address:%s", addressID)
err = utils.DeleteData(addressCacheKey)
if err != nil {
fmt.Printf("Error deleting address cache: %v\n", err)
}
userAddressesCacheKey := fmt.Sprintf("user:%s:addresses", address.UserID)
err = utils.DeleteData(userAddressesCacheKey)
if err != nil {
fmt.Printf("Error deleting user addresses cache: %v\n", err)
}
return nil
}

View File

@ -1,418 +0,0 @@
package services
import (
"encoding/json"
"fmt"
"mime/multipart"
"os"
"path/filepath"
"time"
"github.com/google/uuid"
"github.com/pahmiudahgede/senggoldong/dto"
"github.com/pahmiudahgede/senggoldong/internal/repositories"
"github.com/pahmiudahgede/senggoldong/model"
"github.com/pahmiudahgede/senggoldong/utils"
)
type ArticleService interface {
CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error)
GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error)
GetArticleByID(id string) (*dto.ArticleResponseDTO, error)
UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error)
DeleteArticle(id string) error
}
type articleService struct {
ArticleRepo repositories.ArticleRepository
}
func NewArticleService(articleRepo repositories.ArticleRepository) ArticleService {
return &articleService{ArticleRepo: articleRepo}
}
func (s *articleService) CreateArticle(request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) {
coverImageDir := "./public/uploads/articles"
if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil {
return nil, fmt.Errorf("failed to create directory for cover image: %v", err)
}
allowedExtensions := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
extension := filepath.Ext(coverImage.Filename)
if !allowedExtensions[extension] {
return nil, fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageDir, coverImageFileName)
src, err := coverImage.Open()
if err != nil {
return nil, fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return nil, fmt.Errorf("failed to create cover image file: %v", err)
}
defer dst.Close()
if _, err := dst.ReadFrom(src); err != nil {
return nil, fmt.Errorf("failed to save cover image: %v", err)
}
article := model.Article{
Title: request.Title,
CoverImage: coverImagePath,
Author: request.Author,
Heading: request.Heading,
Content: request.Content,
}
if err := s.ArticleRepo.CreateArticle(&article); err != nil {
return nil, fmt.Errorf("failed to create article: %v", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
articleResponseDTO := &dto.ArticleResponseDTO{
ID: article.ID,
Title: article.Title,
CoverImage: article.CoverImage,
Author: article.Author,
Heading: article.Heading,
Content: article.Content,
PublishedAt: createdAt,
UpdatedAt: updatedAt,
}
cacheKey := fmt.Sprintf("article:%s", article.ID)
cacheData := map[string]interface{}{
"data": articleResponseDTO,
}
if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching article to Redis: %v\n", err)
}
articles, total, err := s.ArticleRepo.FindAllArticles(0, 0)
if err != nil {
fmt.Printf("Error fetching all articles: %v\n", err)
}
var articleDTOs []dto.ArticleResponseDTO
for _, a := range articles {
createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt)
articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{
ID: a.ID,
Title: a.Title,
CoverImage: a.CoverImage,
Author: a.Author,
Heading: a.Heading,
Content: a.Content,
PublishedAt: createdAt,
UpdatedAt: updatedAt,
})
}
articlesCacheKey := "articles:all"
cacheData = map[string]interface{}{
"data": articleDTOs,
"total": total,
}
if err := utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching all articles to Redis: %v\n", err)
}
return articleResponseDTO, nil
}
func (s *articleService) GetAllArticles(page, limit int) ([]dto.ArticleResponseDTO, int, error) {
var cacheKey string
if page == 0 && limit == 0 {
cacheKey = "articles:all"
cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil {
if data, ok := cachedData["data"].([]interface{}); ok {
var articles []dto.ArticleResponseDTO
for _, item := range data {
articleData, ok := item.(map[string]interface{})
if ok {
articles = append(articles, dto.ArticleResponseDTO{
ID: articleData["article_id"].(string),
Title: articleData["title"].(string),
CoverImage: articleData["coverImage"].(string),
Author: articleData["author"].(string),
Heading: articleData["heading"].(string),
Content: articleData["content"].(string),
PublishedAt: articleData["publishedAt"].(string),
UpdatedAt: articleData["updatedAt"].(string),
})
}
}
if total, ok := cachedData["total"].(float64); ok {
fmt.Printf("Cached Total Articles: %f\n", total)
return articles, int(total), nil
} else {
fmt.Println("Total articles not found in cache, using 0 as fallback.")
return articles, 0, nil
}
}
}
}
articles, total, err := s.ArticleRepo.FindAllArticles(page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to fetch articles: %v", err)
}
fmt.Printf("Total Articles from Database: %d\n", total)
var articleDTOs []dto.ArticleResponseDTO
for _, article := range articles {
publishedAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{
ID: article.ID,
Title: article.Title,
CoverImage: article.CoverImage,
Author: article.Author,
Heading: article.Heading,
Content: article.Content,
PublishedAt: publishedAt,
UpdatedAt: updatedAt,
})
}
cacheKey = fmt.Sprintf("articles_page:%d_limit:%d", page, limit)
cacheData := map[string]interface{}{
"data": articleDTOs,
"total": total,
}
fmt.Printf("Setting cache with total: %d\n", total)
if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching articles to Redis: %v\n", err)
}
return articleDTOs, total, nil
}
func (s *articleService) GetArticleByID(id string) (*dto.ArticleResponseDTO, error) {
cacheKey := fmt.Sprintf("article:%s", id)
cachedData, err := utils.GetJSONData(cacheKey)
if err == nil && cachedData != nil {
articleResponse := &dto.ArticleResponseDTO{}
if data, ok := cachedData["data"].(string); ok {
if err := json.Unmarshal([]byte(data), articleResponse); err == nil {
return articleResponse, nil
}
}
}
article, err := s.ArticleRepo.FindArticleByID(id)
if err != nil {
return nil, fmt.Errorf("failed to fetch article by ID: %v", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(article.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(article.UpdatedAt)
articleResponseDTO := &dto.ArticleResponseDTO{
ID: article.ID,
Title: article.Title,
CoverImage: article.CoverImage,
Author: article.Author,
Heading: article.Heading,
Content: article.Content,
PublishedAt: createdAt,
UpdatedAt: updatedAt,
}
cacheData := map[string]interface{}{
"data": articleResponseDTO,
}
if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil {
fmt.Printf("Error caching article to Redis: %v\n", err)
}
return articleResponseDTO, nil
}
func (s *articleService) UpdateArticle(id string, request dto.RequestArticleDTO, coverImage *multipart.FileHeader) (*dto.ArticleResponseDTO, error) {
article, err := s.ArticleRepo.FindArticleByID(id)
if err != nil {
return nil, fmt.Errorf("article not found: %v", id)
}
article.Title = request.Title
article.Heading = request.Heading
article.Content = request.Content
article.Author = request.Author
var coverImagePath string
if coverImage != nil {
coverImagePath, err = s.saveCoverImage(coverImage, article.CoverImage)
if err != nil {
return nil, fmt.Errorf("failed to save cover image: %v", err)
}
article.CoverImage = coverImagePath
}
err = s.ArticleRepo.UpdateArticle(id, article)
if err != nil {
return nil, fmt.Errorf("failed to update article: %v", err)
}
updatedArticle, err := s.ArticleRepo.FindArticleByID(id)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated article: %v", err)
}
createdAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(updatedArticle.UpdatedAt)
articleResponseDTO := &dto.ArticleResponseDTO{
ID: updatedArticle.ID,
Title: updatedArticle.Title,
CoverImage: updatedArticle.CoverImage,
Author: updatedArticle.Author,
Heading: updatedArticle.Heading,
Content: updatedArticle.Content,
PublishedAt: createdAt,
UpdatedAt: updatedAt,
}
articleCacheKey := fmt.Sprintf("article:%s", updatedArticle.ID)
err = utils.SetJSONData(articleCacheKey, map[string]interface{}{"data": articleResponseDTO}, time.Hour*24)
if err != nil {
fmt.Printf("Error caching updated article to Redis: %v\n", err)
}
articlesCacheKey := "articles:all"
err = utils.DeleteData(articlesCacheKey)
if err != nil {
fmt.Printf("Error deleting articles cache: %v\n", err)
}
articles, _, err := s.ArticleRepo.FindAllArticles(0, 0)
if err != nil {
fmt.Printf("Error fetching all articles: %v\n", err)
} else {
var articleDTOs []dto.ArticleResponseDTO
for _, a := range articles {
createdAt, _ := utils.FormatDateToIndonesianFormat(a.PublishedAt)
updatedAt, _ := utils.FormatDateToIndonesianFormat(a.UpdatedAt)
articleDTOs = append(articleDTOs, dto.ArticleResponseDTO{
ID: a.ID,
Title: a.Title,
CoverImage: a.CoverImage,
Author: a.Author,
Heading: a.Heading,
Content: a.Content,
PublishedAt: createdAt,
UpdatedAt: updatedAt,
})
}
cacheData := map[string]interface{}{
"data": articleDTOs,
}
err = utils.SetJSONData(articlesCacheKey, cacheData, time.Hour*24)
if err != nil {
fmt.Printf("Error caching updated articles to Redis: %v\n", err)
}
}
return articleResponseDTO, nil
}
func (s *articleService) saveCoverImage(coverImage *multipart.FileHeader, oldImagePath string) (string, error) {
coverImageDir := "./public/uploads/articles"
if _, err := os.Stat(coverImageDir); os.IsNotExist(err) {
if err := os.MkdirAll(coverImageDir, os.ModePerm); err != nil {
return "", fmt.Errorf("failed to create directory for cover image: %v", err)
}
}
extension := filepath.Ext(coverImage.Filename)
if extension != ".jpg" && extension != ".jpeg" && extension != ".png" {
return "", fmt.Errorf("invalid file type, only .jpg, .jpeg, and .png are allowed")
}
coverImageFileName := fmt.Sprintf("%s_cover%s", uuid.New().String(), extension)
coverImagePath := filepath.Join(coverImageDir, coverImageFileName)
if oldImagePath != "" {
err := os.Remove(oldImagePath)
if err != nil {
fmt.Printf("Failed to delete old cover image: %v\n", err)
} else {
fmt.Printf("Successfully deleted old cover image: %s\n", oldImagePath)
}
}
src, err := coverImage.Open()
if err != nil {
return "", fmt.Errorf("failed to open uploaded file: %v", err)
}
defer src.Close()
dst, err := os.Create(coverImagePath)
if err != nil {
return "", fmt.Errorf("failed to create cover image file: %v", err)
}
defer dst.Close()
_, err = dst.ReadFrom(src)
if err != nil {
return "", fmt.Errorf("failed to save cover image: %v", err)
}
return coverImagePath, nil
}
func (s *articleService) DeleteArticle(id string) error {
article, err := s.ArticleRepo.FindArticleByID(id)
if err != nil {
return fmt.Errorf("failed to find article: %v", id)
}
if article.CoverImage != "" {
err := os.Remove(article.CoverImage)
if err != nil {
fmt.Printf("Failed to delete cover image: %v\n", err)
} else {
fmt.Printf("Successfully deleted cover image: %s\n", article.CoverImage)
}
}
err = s.ArticleRepo.DeleteArticle(id)
if err != nil {
return fmt.Errorf("failed to delete article: %v", err)
}
articleCacheKey := fmt.Sprintf("article:%s", id)
err = utils.DeleteData(articleCacheKey)
if err != nil {
fmt.Printf("Error deleting cache for article: %v\n", err)
}
articlesCacheKey := "articles:all"
err = utils.DeleteData(articlesCacheKey)
if err != nil {
fmt.Printf("Error deleting cache for all articles: %v\n", err)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More