commit
6d6223d13f
|
|
@ -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
|
||||
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
260
README.md
|
|
@ -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.**
|
||||
|
||||
[](https://golang.org/)
|
||||
[](https://gofiber.io/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://redis.io/)
|
||||
[](https://www.docker.com/)
|
||||
[](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>
|
||||
|
|
|
|||
47
cmd/main.go
47
cmd/main.go
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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!")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}()
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
@ -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
|
||||
}
|
||||
121
dto/auth_dto.go
121
dto/auth_dto.go
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
54
go.mod
|
|
@ -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
101
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) == "" {
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package model
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package collector
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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(®isterDTO); 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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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(®ency).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(®encies).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
} else {
|
||||
|
||||
err := r.DB.Find(®encies).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(®ency).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 ®ency, 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package requestpickup
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
package requestpickup
|
||||
|
|
@ -0,0 +1 @@
|
|||
package requestpickup
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dto
|
||||
package role
|
||||
|
||||
type RoleResponseDTO struct {
|
||||
ID string `json:"role_id"`
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue