refact: reafctor code for optimize
This commit is contained in:
parent
c26eee0ab9
commit
baccdd696b
280
README.md
280
README.md
|
@ -1,2 +1,280 @@
|
|||
# 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
|
||||
|
||||
## 🌟 Competitive Advantages
|
||||
|
||||
### **Technical Excellence**
|
||||
- **High Performance**: Sub-100ms response time untuk critical operations
|
||||
- **Scalability**: Ready untuk handle growth hingga millions of users
|
||||
- **Security First**: Multi-layer security dengan encryption dan secure authentication
|
||||
- **Real-time Capabilities**: Instant updates dan notifications untuk better user experience
|
||||
|
||||
### **Business Value**
|
||||
- **Cost Efficiency**: Significant reduction dalam operational cost melalui automation
|
||||
- **Environmental Impact**: Measurable contribution untuk sustainability goals
|
||||
- **Stakeholder Engagement**: User-friendly platform yang mendorong active participation
|
||||
- **Data-Driven Decision**: Comprehensive analytics untuk strategic planning
|
||||
|
||||
### **Innovation Features**
|
||||
- **AI-Ready Architecture**: Prepared untuk integration dengan machine learning models
|
||||
- **IoT Integration**: Ready untuk connect dengan smart waste bins dan sensors
|
||||
- **Blockchain Compatibility**: Architecture yang support untuk blockchain integration
|
||||
- **Multi-tenancy Support**: Scalable untuk multiple cities dan regions
|
||||
|
||||
---
|
||||
|
||||
<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>
|
||||
|
|
32
cmd/main.go
32
cmd/main.go
|
@ -1,7 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"rijig/config"
|
||||
"rijig/internal/cart"
|
||||
"rijig/internal/trash"
|
||||
"rijig/internal/worker"
|
||||
"time"
|
||||
|
||||
// "rijig/internal/repositories"
|
||||
// "rijig/internal/services"
|
||||
|
||||
|
@ -13,21 +19,21 @@ import (
|
|||
|
||||
func main() {
|
||||
config.SetupConfig()
|
||||
// cartRepo := repositories.NewCartRepository()
|
||||
// trashRepo := repositories.NewTrashRepository(config.DB)
|
||||
// cartService := services.NewCartService(cartRepo, trashRepo)
|
||||
// worker := worker.NewCartWorker(cartService, cartRepo, trashRepo)
|
||||
cartRepo := cart.NewCartRepository()
|
||||
trashRepo := trash.NewTrashRepository(config.DB)
|
||||
cartService := cart.NewCartService(cartRepo, trashRepo)
|
||||
worker := worker.NewCartWorker(cartService, cartRepo, trashRepo)
|
||||
|
||||
// go func() {
|
||||
// ticker := time.NewTicker(30 * time.Second)
|
||||
// defer ticker.Stop()
|
||||
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)
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
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 {
|
||||
|
|
|
@ -5,8 +5,6 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"rijig/model"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
@ -14,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"),
|
||||
|
@ -31,59 +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==============
|
||||
// =>user preparation<=
|
||||
&model.User{},
|
||||
&model.Collector{},
|
||||
&model.AvaibleTrashByCollector{},
|
||||
&model.Role{},
|
||||
&model.UserPin{},
|
||||
&model.Address{},
|
||||
&model.IdentityCard{},
|
||||
&model.CompanyProfile{},
|
||||
|
||||
// =>user preparation<=
|
||||
// =>requestpickup preparation<=
|
||||
&model.RequestPickup{},
|
||||
&model.RequestPickupItem{},
|
||||
&model.PickupStatusHistory{},
|
||||
&model.PickupRating{},
|
||||
|
||||
&model.Cart{},
|
||||
&model.CartItem{},
|
||||
// =>requestpickup preparation<=
|
||||
|
||||
// =>store preparation<=
|
||||
&model.Store{},
|
||||
&model.Product{},
|
||||
&model.ProductImage{},
|
||||
// =>store preparation<=
|
||||
// ==============main feature==============
|
||||
|
||||
// ==============additional content========
|
||||
&model.Article{},
|
||||
&model.Banner{},
|
||||
&model.InitialCoint{},
|
||||
&model.About{},
|
||||
&model.AboutDetail{},
|
||||
&model.CoverageArea{},
|
||||
|
||||
// =>Trash Model<=
|
||||
&model.TrashCategory{},
|
||||
&model.TrashDetail{},
|
||||
// =>Trash Model<=
|
||||
// ==============additional content========
|
||||
)
|
||||
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
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
package config
|
||||
|
||||
package config
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
|
|
@ -322,8 +322,9 @@ func (r *RegisterAdminRequest) ValidateRegisterAdminRequest() (map[string][]stri
|
|||
errors["name"] = append(errors["name"], "Name is required")
|
||||
}
|
||||
|
||||
if r.Gender != "male" && r.Gender != "female" {
|
||||
errors["gender"] = append(errors["gender"], "Gender must be either 'male' or 'female'")
|
||||
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) == "" {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"log"
|
||||
"rijig/middleware"
|
||||
"rijig/utils"
|
||||
|
||||
|
@ -16,8 +17,11 @@ func NewAuthenticationHandler(service AuthenticationService) *AuthenticationHand
|
|||
}
|
||||
|
||||
func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error {
|
||||
deviceID := c.Get("X-Device-ID")
|
||||
if deviceID == "" {
|
||||
claims, err := middleware.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if claims.DeviceID == "" {
|
||||
return utils.BadRequest(c, "Device ID is required")
|
||||
}
|
||||
|
||||
|
@ -31,12 +35,11 @@ func (h *AuthenticationHandler) RefreshToken(c *fiber.Ctx) error {
|
|||
return utils.BadRequest(c, "Refresh token is required")
|
||||
}
|
||||
|
||||
userID, ok := c.Locals("user_id").(string)
|
||||
if !ok || userID == "" {
|
||||
return utils.Unauthorized(c, "Unauthorized or missing user ID")
|
||||
if claims.UserID == "" {
|
||||
return utils.BadRequest(c, "userid is required")
|
||||
}
|
||||
|
||||
tokenData, err := utils.RefreshAccessToken(userID, deviceID, body.RefreshToken)
|
||||
tokenData, err := utils.RefreshAccessToken(claims.UserID, claims.DeviceID, body.RefreshToken)
|
||||
if err != nil {
|
||||
return utils.Unauthorized(c, err.Error())
|
||||
}
|
||||
|
@ -62,6 +65,21 @@ func (h *AuthenticationHandler) GetMe(c *fiber.Ctx) error {
|
|||
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -30,6 +30,7 @@ func AuthenticationRouter(api fiber.Router) {
|
|||
// authHandler.GetMe,
|
||||
// )
|
||||
|
||||
authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus)
|
||||
authRoute.Post("/login/admin", authHandler.Login)
|
||||
authRoute.Post("/register/admin", authHandler.Register)
|
||||
authRoute.Post("/request-otp", authHandler.RequestOtpHandler)
|
||||
|
|
|
@ -3,6 +3,7 @@ package authentication
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -12,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
type AuthenticationService interface {
|
||||
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
|
||||
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error)
|
||||
RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error
|
||||
|
||||
|
@ -48,6 +50,78 @@ func normalizeRoleName(roleName string) string {
|
|||
}
|
||||
}
|
||||
|
||||
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) (*AuthResponse, error) {
|
||||
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
|
@ -341,8 +415,16 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO
|
|||
user.RegistrationStatus,
|
||||
)
|
||||
|
||||
var message string
|
||||
if user.RegistrationStatus == utils.RegStatusComplete {
|
||||
message = "verif pin"
|
||||
nextStep = "verif_pin"
|
||||
} else {
|
||||
message = "otp berhasil diverifikasi"
|
||||
}
|
||||
|
||||
return &AuthResponse{
|
||||
Message: "otp berhasil diverifikasi",
|
||||
Message: message,
|
||||
AccessToken: tokenResponse.AccessToken,
|
||||
RefreshToken: tokenResponse.RefreshToken,
|
||||
TokenType: string(tokenResponse.TokenType),
|
||||
|
|
|
@ -14,15 +14,15 @@ type RequestCartDTO struct {
|
|||
CartItems []RequestCartItemDTO `json:"cart_items"`
|
||||
}
|
||||
|
||||
type CartResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TotalAmount float64 `json:"total_amount"`
|
||||
EstimatedTotalPrice float64 `json:"estimated_total_price"`
|
||||
CartItems []CartItemResponse `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 CartItemResponse struct {
|
||||
type ResponseCartItemDTO struct {
|
||||
ID string `json:"id"`
|
||||
TrashID string `json:"trash_id"`
|
||||
TrashName string `json:"trash_name"`
|
||||
|
|
|
@ -1 +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
|
||||
}
|
|
@ -1 +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)
|
||||
}
|
||||
|
|
|
@ -2,355 +2,266 @@ package cart
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
// "rijig/dto"
|
||||
// "rijig/internal/repositories"
|
||||
"rijig/internal/trash"
|
||||
"rijig/model"
|
||||
"rijig/utils"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CartService struct {
|
||||
cartRepo CartRepository
|
||||
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(cartRepo CartRepository, trashRepo trash.TrashRepositoryInterface) *CartService {
|
||||
return &CartService{
|
||||
cartRepo: cartRepo,
|
||||
trashRepo: trashRepo,
|
||||
}
|
||||
func NewCartService(repo CartRepository, trashRepo trash.TrashRepositoryInterface) CartService {
|
||||
return &cartService{repo, trashRepo}
|
||||
}
|
||||
|
||||
func (s *CartService) AddToCart(ctx context.Context, userID, trashCategoryID string, amount float64) error {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
if err != nil && err.Error() != "ErrCacheMiss" {
|
||||
return fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
func (s *cartService) AddOrUpdateItem(ctx context.Context, userID string, req RequestCartItemDTO) error {
|
||||
if req.Amount <= 0 {
|
||||
return errors.New("amount harus lebih dari 0")
|
||||
}
|
||||
|
||||
if cartItems == nil {
|
||||
cartItems = make(map[string]model.CartItem)
|
||||
}
|
||||
|
||||
trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, trashCategoryID)
|
||||
_, err := s.trashRepo.GetTrashCategoryByID(ctx, req.TrashID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get trash category: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
cartItems[trashCategoryID] = model.CartItem{
|
||||
TrashCategoryID: trashCategoryID,
|
||||
Amount: amount,
|
||||
SubTotalEstimatedPrice: amount * float64(trashCategory.EstimatedPrice),
|
||||
}
|
||||
|
||||
return utils.SetCache(cartKey, cartItems, 24*time.Hour)
|
||||
}
|
||||
|
||||
func (s *CartService) RemoveFromCart(ctx context.Context, userID, trashCategoryID string) error {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
existingCart, err := GetCartFromRedis(ctx, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "ErrCacheMiss" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
delete(cartItems, trashCategoryID)
|
||||
|
||||
if len(cartItems) == 0 {
|
||||
return utils.DeleteCache(cartKey)
|
||||
if existingCart == nil {
|
||||
existingCart = &RequestCartDTO{
|
||||
CartItems: []RequestCartItemDTO{},
|
||||
}
|
||||
}
|
||||
|
||||
return utils.SetCache(cartKey, cartItems, 24*time.Hour)
|
||||
}
|
||||
|
||||
func (s *CartService) ClearCart(userID string) error {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
return utils.DeleteCache(cartKey)
|
||||
}
|
||||
|
||||
func (s *CartService) GetCartFromRedis(ctx context.Context, userID string) (*CartResponse, error) {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
if err != nil {
|
||||
if err.Error() == "ErrCacheMiss" {
|
||||
return &CartResponse{
|
||||
ID: "N/A",
|
||||
UserID: userID,
|
||||
TotalAmount: 0,
|
||||
EstimatedTotalPrice: 0,
|
||||
CartItems: []CartItemResponse{},
|
||||
}, nil
|
||||
updated := false
|
||||
for i, item := range existingCart.CartItems {
|
||||
if item.TrashID == req.TrashID {
|
||||
existingCart.CartItems[i].Amount = req.Amount
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
}
|
||||
|
||||
var totalAmount float64
|
||||
var estimatedTotal float64
|
||||
var cartItemDTOs []CartItemResponse
|
||||
|
||||
for _, item := range cartItems {
|
||||
trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get trash category %s: %v", item.TrashCategoryID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
totalAmount += item.Amount
|
||||
estimatedTotal += item.SubTotalEstimatedPrice
|
||||
|
||||
cartItemDTOs = append(cartItemDTOs, CartItemResponse{
|
||||
ID: uuid.NewString(),
|
||||
TrashID: trashCategory.ID,
|
||||
TrashName: trashCategory.Name,
|
||||
TrashIcon: trashCategory.IconTrash,
|
||||
TrashPrice: float64(trashCategory.EstimatedPrice),
|
||||
Amount: item.Amount,
|
||||
SubTotalEstimatedPrice: item.SubTotalEstimatedPrice,
|
||||
if !updated {
|
||||
existingCart.CartItems = append(existingCart.CartItems, RequestCartItemDTO{
|
||||
TrashID: req.TrashID,
|
||||
Amount: req.Amount,
|
||||
})
|
||||
}
|
||||
|
||||
resp := &CartResponse{
|
||||
ID: "N/A",
|
||||
UserID: userID,
|
||||
TotalAmount: totalAmount,
|
||||
EstimatedTotalPrice: estimatedTotal,
|
||||
CartItems: cartItemDTOs,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
return SetCartToRedis(ctx, userID, *existingCart)
|
||||
}
|
||||
|
||||
func (s *CartService) CommitCartToDatabase(ctx context.Context, userID string) error {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
func (s *cartService) GetCart(ctx context.Context, userID string) (*ResponseCartDTO, error) {
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
cached, err := GetCartFromRedis(ctx, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "ErrCacheMiss" {
|
||||
log.Printf("No cart items found in Redis for user: %s", userID)
|
||||
return fmt.Errorf("no cart items found")
|
||||
}
|
||||
return fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cartItems) == 0 {
|
||||
log.Printf("No items to commit for user: %s", userID)
|
||||
return fmt.Errorf("no items to commit")
|
||||
}
|
||||
if cached != nil {
|
||||
|
||||
hasCart, err := s.cartRepo.HasExistingCart(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing cart: %w", err)
|
||||
}
|
||||
|
||||
var cart *model.Cart
|
||||
if hasCart {
|
||||
|
||||
cart, err = s.cartRepo.GetCartByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get existing cart: %w", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
cart, err = s.cartRepo.FindOrCreateCart(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cart: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range cartItems {
|
||||
trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, item.TrashCategoryID)
|
||||
if err != nil {
|
||||
log.Printf("Trash category not found for trashID: %s", item.TrashCategoryID)
|
||||
continue
|
||||
if err := RefreshCartTTL(ctx, userID); err != nil {
|
||||
log.Printf("Warning: Failed to refresh cart TTL for user %s: %v", userID, err)
|
||||
}
|
||||
|
||||
err = s.cartRepo.AddOrUpdateCartItem(
|
||||
ctx,
|
||||
cart.ID,
|
||||
item.TrashCategoryID,
|
||||
item.Amount,
|
||||
float64(trashCategory.EstimatedPrice),
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("Failed to add/update cart item: %v", err)
|
||||
continue
|
||||
}
|
||||
return s.buildResponseFromCache(ctx, userID, cached)
|
||||
}
|
||||
|
||||
if err := s.cartRepo.UpdateCartTotals(ctx, cart.ID); err != nil {
|
||||
return fmt.Errorf("failed to update cart totals: %w", err)
|
||||
}
|
||||
|
||||
if err := utils.DeleteCache(cartKey); err != nil {
|
||||
log.Printf("Failed to clear Redis cart: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Cart committed successfully for user: %s", userID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CartService) GetCart(ctx context.Context, userID string) (*CartResponse, error) {
|
||||
|
||||
cartRedis, err := s.GetCartFromRedis(ctx, userID)
|
||||
if err == nil && len(cartRedis.CartItems) > 0 {
|
||||
return cartRedis, nil
|
||||
}
|
||||
|
||||
cartDB, err := s.cartRepo.GetCartByUser(ctx, userID)
|
||||
cart, err := s.repo.GetCartByUser(ctx, userID)
|
||||
if err != nil {
|
||||
|
||||
return &CartResponse{
|
||||
ID: "N/A",
|
||||
return &ResponseCartDTO{
|
||||
ID: "",
|
||||
UserID: userID,
|
||||
TotalAmount: 0,
|
||||
EstimatedTotalPrice: 0,
|
||||
CartItems: []CartItemResponse{},
|
||||
CartItems: []ResponseCartItemDTO{},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
var items []CartItemResponse
|
||||
for _, item := range cartDB.CartItems {
|
||||
items = append(items, CartItemResponse{
|
||||
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: float64(item.TrashCategory.EstimatedPrice),
|
||||
TrashPrice: item.TrashCategory.EstimatedPrice,
|
||||
Amount: item.Amount,
|
||||
SubTotalEstimatedPrice: item.SubTotalEstimatedPrice,
|
||||
})
|
||||
}
|
||||
|
||||
resp := &CartResponse{
|
||||
ID: cartDB.ID,
|
||||
UserID: cartDB.UserID,
|
||||
TotalAmount: cartDB.TotalAmount,
|
||||
EstimatedTotalPrice: cartDB.EstimatedTotalPrice,
|
||||
return &ResponseCartDTO{
|
||||
ID: cart.ID,
|
||||
UserID: cart.UserID,
|
||||
TotalAmount: cart.TotalAmount,
|
||||
EstimatedTotalPrice: cart.EstimatedTotalPrice,
|
||||
CartItems: items,
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *CartService) SyncCartFromDatabaseToRedis(ctx context.Context, userID string) error {
|
||||
|
||||
cartDB, err := s.cartRepo.GetCartByUser(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get cart from database: %w", err)
|
||||
func (s *cartService) commitCartFromRedis(ctx context.Context, userID string, cachedCart *RequestCartDTO) error {
|
||||
if len(cachedCart.CartItems) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cartItems := make(map[string]model.CartItem)
|
||||
for _, item := range cartDB.CartItems {
|
||||
cartItems[item.TrashCategoryID] = model.CartItem{
|
||||
TrashCategoryID: item.TrashCategoryID,
|
||||
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: item.SubTotalEstimatedPrice,
|
||||
}
|
||||
}
|
||||
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
return utils.SetCache(cartKey, cartItems, 24*time.Hour)
|
||||
}
|
||||
|
||||
func (s *CartService) GetCartItemCount(userID string) (int, error) {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
if err != nil {
|
||||
if err.Error() == "ErrCacheMiss" {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
}
|
||||
|
||||
return len(cartItems), nil
|
||||
}
|
||||
|
||||
func (s *CartService) DeleteCart(ctx context.Context, userID string) error {
|
||||
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
if err := utils.DeleteCache(cartKey); err != nil {
|
||||
log.Printf("Failed to delete cart from Redis: %v", err)
|
||||
}
|
||||
|
||||
return s.cartRepo.DeleteCart(ctx, userID)
|
||||
}
|
||||
|
||||
func (s *CartService) UpdateCartWithDTO(ctx context.Context, userID string, cartDTO *RequestCartDTO) error {
|
||||
|
||||
if errors, valid := cartDTO.ValidateRequestCartDTO(); !valid {
|
||||
return fmt.Errorf("validation failed: %v", errors)
|
||||
}
|
||||
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
cartItems := make(map[string]model.CartItem)
|
||||
|
||||
for _, itemDTO := range cartDTO.CartItems {
|
||||
|
||||
trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice)
|
||||
|
||||
cartItems[itemDTO.TrashID] = model.CartItem{
|
||||
TrashCategoryID: itemDTO.TrashID,
|
||||
Amount: itemDTO.Amount,
|
||||
SubTotalEstimatedPrice: subtotal,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return utils.SetCache(cartKey, cartItems, 24*time.Hour)
|
||||
}
|
||||
|
||||
func (s *CartService) AddItemsToCart(ctx context.Context, userID string, items []RequestCartItemDTO) error {
|
||||
cartKey := fmt.Sprintf("cart:%s", userID)
|
||||
|
||||
var cartItems map[string]model.CartItem
|
||||
err := utils.GetCache(cartKey, &cartItems)
|
||||
if err != nil && err.Error() != "ErrCacheMiss" {
|
||||
return fmt.Errorf("failed to get cart from cache: %w", err)
|
||||
}
|
||||
|
||||
if cartItems == nil {
|
||||
cartItems = make(map[string]model.CartItem)
|
||||
}
|
||||
|
||||
for _, itemDTO := range items {
|
||||
if itemDTO.TrashID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
trashCategory, err := s.trashRepo.GetTrashCategoryByID(ctx, itemDTO.TrashID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to get trash category %s: %v", itemDTO.TrashID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
subtotal := itemDTO.Amount * float64(trashCategory.EstimatedPrice)
|
||||
|
||||
cartItems[itemDTO.TrashID] = model.CartItem{
|
||||
TrashCategoryID: itemDTO.TrashID,
|
||||
Amount: itemDTO.Amount,
|
||||
SubTotalEstimatedPrice: subtotal,
|
||||
}
|
||||
}
|
||||
|
||||
return utils.SetCache(cartKey, cartItems, 24*time.Hour)
|
||||
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
|
|
@ -7,6 +7,20 @@ import (
|
|||
"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"`
|
||||
|
|
|
@ -30,6 +30,9 @@ type CollectorRepository interface {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -56,6 +59,36 @@ func (r *collectorRepository) Create(ctx context.Context, collector *model.Colle
|
|||
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
|
||||
|
||||
|
|
|
@ -39,19 +39,19 @@ func (r *RequestCompanyProfileDTO) ValidateCompanyProfileInput() (map[string][]s
|
|||
errors := make(map[string][]string)
|
||||
|
||||
if strings.TrimSpace(r.CompanyName) == "" {
|
||||
errors["company_Name"] = append(errors["company_name"], "Company name is required")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
errors["company_description"] = append(errors["company_description"], "Company description is required")
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
|
|
|
@ -2,7 +2,10 @@ package company
|
|||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"rijig/middleware"
|
||||
"rijig/utils"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
@ -18,9 +21,10 @@ func NewCompanyProfileHandler(service CompanyProfileService) *CompanyProfileHand
|
|||
}
|
||||
|
||||
func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error {
|
||||
userID, ok := c.Locals("user_id").(string)
|
||||
if !ok || userID == "" {
|
||||
return utils.Unauthorized(c, "User not authenticated")
|
||||
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
|
||||
|
@ -32,9 +36,19 @@ func (h *CompanyProfileHandler) CreateCompanyProfile(c *fiber.Ctx) error {
|
|||
return utils.ResponseErrorData(c, fiber.StatusBadRequest, "validation failed", errors)
|
||||
}
|
||||
|
||||
res, err := h.service.CreateCompanyProfile(context.Background(), userID, &req)
|
||||
companyLogo, err := c.FormFile("company_logo")
|
||||
if err != nil {
|
||||
return utils.InternalServerError(c, err.Error())
|
||||
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)
|
||||
|
|
|
@ -3,6 +3,7 @@ package company
|
|||
import (
|
||||
"rijig/config"
|
||||
"rijig/internal/authentication"
|
||||
"rijig/internal/userprofile"
|
||||
"rijig/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
@ -11,7 +12,8 @@ import (
|
|||
func CompanyRouter(api fiber.Router) {
|
||||
companyProfileRepo := NewCompanyProfileRepository(config.DB)
|
||||
authRepo := authentication.NewAuthenticationRepository(config.DB)
|
||||
companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo)
|
||||
userRepo := userprofile.NewUserProfileRepository(config.DB)
|
||||
companyProfileService := NewCompanyProfileService(companyProfileRepo, authRepo, userRepo)
|
||||
companyProfileHandler := NewCompanyProfileHandler(companyProfileService)
|
||||
|
||||
companyProfileAPI := api.Group("/companyprofile")
|
||||
|
|
|
@ -2,8 +2,13 @@ package company
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"rijig/internal/authentication"
|
||||
"rijig/internal/role"
|
||||
"rijig/internal/userprofile"
|
||||
|
@ -13,7 +18,7 @@ import (
|
|||
)
|
||||
|
||||
type CompanyProfileService interface {
|
||||
CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error)
|
||||
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)
|
||||
|
@ -25,12 +30,15 @@ type CompanyProfileService interface {
|
|||
|
||||
type companyProfileService struct {
|
||||
companyRepo CompanyProfileRepository
|
||||
authRepo authentication.AuthenticationRepository
|
||||
authRepo authentication.AuthenticationRepository
|
||||
userRepo userprofile.UserProfileRepository
|
||||
}
|
||||
|
||||
func NewCompanyProfileService(companyRepo CompanyProfileRepository, authRepo authentication.AuthenticationRepository) CompanyProfileService {
|
||||
func NewCompanyProfileService(companyRepo CompanyProfileRepository,
|
||||
authRepo authentication.AuthenticationRepository,
|
||||
userRepo userprofile.UserProfileRepository) CompanyProfileService {
|
||||
return &companyProfileService{
|
||||
companyRepo, authRepo,
|
||||
companyRepo, authRepo, userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,10 +64,73 @@ func FormatResponseCompanyProfile(companyProfile *model.CompanyProfile) (*Respon
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID string, request *RequestCompanyProfileDTO) (*ResponseCompanyProfileDTO, error) {
|
||||
// if errors, valid := request.ValidateCompanyProfileInput(); !valid {
|
||||
// return nil, fmt.Errorf("validation failed: %v", errors)
|
||||
// }
|
||||
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,
|
||||
|
@ -67,7 +138,7 @@ func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID
|
|||
CompanyAddress: request.CompanyAddress,
|
||||
CompanyPhone: request.CompanyPhone,
|
||||
CompanyEmail: request.CompanyEmail,
|
||||
CompanyLogo: request.CompanyLogo,
|
||||
CompanyLogo: companyLogoPath,
|
||||
CompanyWebsite: request.CompanyWebsite,
|
||||
TaxID: request.TaxID,
|
||||
FoundedDate: request.FoundedDate,
|
||||
|
@ -75,12 +146,72 @@ func (s *companyProfileService) CreateCompanyProfile(ctx context.Context, userID
|
|||
CompanyDescription: request.CompanyDescription,
|
||||
}
|
||||
|
||||
created, err := s.companyRepo.CreateCompanyProfile(ctx, companyProfile)
|
||||
_, err = s.companyRepo.CreateCompanyProfile(ctx, companyProfile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return FormatResponseCompanyProfile(created)
|
||||
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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -1 +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
|
||||
}
|
||||
|
|
|
@ -1 +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
|
||||
}
|
||||
|
|
|
@ -32,24 +32,24 @@ type TrashRepositoryInterface interface {
|
|||
GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error)
|
||||
}
|
||||
|
||||
type TrashRepository struct {
|
||||
type trashRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTrashRepository(db *gorm.DB) TrashRepositoryInterface {
|
||||
return &TrashRepository{
|
||||
db: db,
|
||||
return &trashRepository{
|
||||
db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *TrashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error {
|
||||
func (r *trashRepository) CreateTrashCategory(ctx context.Context, category *model.TrashCategory) error {
|
||||
if err := r.db.WithContext(ctx).Create(category).Error; err != nil {
|
||||
return fmt.Errorf("failed to create trash category: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error {
|
||||
func (r *trashRepository) CreateTrashCategoryWithDetails(ctx context.Context, category *model.TrashCategory, details []model.TrashDetail) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
|
||||
if err := tx.Create(category).Error; err != nil {
|
||||
|
@ -75,7 +75,7 @@ func (r *TrashRepository) CreateTrashCategoryWithDetails(ctx context.Context, ca
|
|||
})
|
||||
}
|
||||
|
||||
func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||
func (r *trashRepository) UpdateTrashCategory(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||
|
||||
exists, err := r.CheckTrashCategoryExists(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -99,7 +99,7 @@ func (r *TrashRepository) UpdateTrashCategory(ctx context.Context, id string, up
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) {
|
||||
func (r *trashRepository) GetAllTrashCategories(ctx context.Context) ([]model.TrashCategory, error) {
|
||||
var categories []model.TrashCategory
|
||||
|
||||
if err := r.db.WithContext(ctx).Find(&categories).Error; err != nil {
|
||||
|
@ -109,7 +109,7 @@ func (r *TrashRepository) GetAllTrashCategories(ctx context.Context) ([]model.Tr
|
|||
return categories, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) {
|
||||
func (r *trashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context) ([]model.TrashCategory, error) {
|
||||
var categories []model.TrashCategory
|
||||
|
||||
if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB {
|
||||
|
@ -121,7 +121,7 @@ func (r *TrashRepository) GetAllTrashCategoriesWithDetails(ctx context.Context)
|
|||
return categories, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) {
|
||||
func (r *trashRepository) GetTrashCategoryByID(ctx context.Context, id string) (*model.TrashCategory, error) {
|
||||
var category model.TrashCategory
|
||||
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&category).Error; err != nil {
|
||||
|
@ -134,7 +134,7 @@ func (r *TrashRepository) GetTrashCategoryByID(ctx context.Context, id string) (
|
|||
return &category, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) {
|
||||
func (r *trashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, id string) (*model.TrashCategory, error) {
|
||||
var category model.TrashCategory
|
||||
|
||||
if err := r.db.WithContext(ctx).Preload("Details", func(db *gorm.DB) *gorm.DB {
|
||||
|
@ -149,7 +149,7 @@ func (r *TrashRepository) GetTrashCategoryByIDWithDetails(ctx context.Context, i
|
|||
return &category, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) error {
|
||||
func (r *trashRepository) DeleteTrashCategory(ctx context.Context, id string) error {
|
||||
|
||||
exists, err := r.CheckTrashCategoryExists(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -171,7 +171,7 @@ func (r *TrashRepository) DeleteTrashCategory(ctx context.Context, id string) er
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error {
|
||||
func (r *trashRepository) CreateTrashDetail(ctx context.Context, detail *model.TrashDetail) error {
|
||||
|
||||
exists, err := r.CheckTrashCategoryExists(ctx, detail.TrashCategoryID)
|
||||
if err != nil {
|
||||
|
@ -196,7 +196,7 @@ func (r *TrashRepository) CreateTrashDetail(ctx context.Context, detail *model.T
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error {
|
||||
func (r *trashRepository) AddTrashDetailToCategory(ctx context.Context, categoryID string, detail *model.TrashDetail) error {
|
||||
|
||||
exists, err := r.CheckTrashCategoryExists(ctx, categoryID)
|
||||
if err != nil {
|
||||
|
@ -223,7 +223,7 @@ func (r *TrashRepository) AddTrashDetailToCategory(ctx context.Context, category
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||
func (r *trashRepository) UpdateTrashDetail(ctx context.Context, id string, updates map[string]interface{}) error {
|
||||
|
||||
exists, err := r.CheckTrashDetailExists(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -247,7 +247,7 @@ func (r *TrashRepository) UpdateTrashDetail(ctx context.Context, id string, upda
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) {
|
||||
func (r *trashRepository) GetTrashDetailsByCategory(ctx context.Context, categoryID string) ([]model.TrashDetail, error) {
|
||||
var details []model.TrashDetail
|
||||
|
||||
if err := r.db.WithContext(ctx).Where("trash_category_id = ?", categoryID).Order("step_order ASC").Find(&details).Error; err != nil {
|
||||
|
@ -257,7 +257,7 @@ func (r *TrashRepository) GetTrashDetailsByCategory(ctx context.Context, categor
|
|||
return details, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) {
|
||||
func (r *trashRepository) GetTrashDetailByID(ctx context.Context, id string) (*model.TrashDetail, error) {
|
||||
var detail model.TrashDetail
|
||||
|
||||
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&detail).Error; err != nil {
|
||||
|
@ -270,7 +270,7 @@ func (r *TrashRepository) GetTrashDetailByID(ctx context.Context, id string) (*m
|
|||
return &detail, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) error {
|
||||
func (r *trashRepository) DeleteTrashDetail(ctx context.Context, id string) error {
|
||||
|
||||
exists, err := r.CheckTrashDetailExists(ctx, id)
|
||||
if err != nil {
|
||||
|
@ -292,7 +292,7 @@ func (r *TrashRepository) DeleteTrashDetail(ctx context.Context, id string) erro
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) {
|
||||
func (r *trashRepository) CheckTrashCategoryExists(ctx context.Context, id string) (bool, error) {
|
||||
var count int64
|
||||
|
||||
if err := r.db.WithContext(ctx).Model(&model.TrashCategory{}).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||
|
@ -302,7 +302,7 @@ func (r *TrashRepository) CheckTrashCategoryExists(ctx context.Context, id strin
|
|||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) {
|
||||
func (r *trashRepository) CheckTrashDetailExists(ctx context.Context, id string) (bool, error) {
|
||||
var count int64
|
||||
|
||||
if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).Where("id = ?", id).Count(&count).Error; err != nil {
|
||||
|
@ -312,7 +312,7 @@ func (r *TrashRepository) CheckTrashDetailExists(ctx context.Context, id string)
|
|||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *TrashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) {
|
||||
func (r *trashRepository) GetMaxStepOrderByCategory(ctx context.Context, categoryID string) (int, error) {
|
||||
var maxOrder int
|
||||
|
||||
if err := r.db.WithContext(ctx).Model(&model.TrashDetail{}).
|
||||
|
|
|
@ -1 +1,84 @@
|
|||
// ===internal/trash/trash_route.go===
|
||||
package trash
|
||||
|
||||
import (
|
||||
"rijig/config"
|
||||
"rijig/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func TrashRouter(api fiber.Router) {
|
||||
trashRepo := NewTrashRepository(config.DB)
|
||||
trashService := NewTrashService(trashRepo)
|
||||
trashHandler := NewTrashHandler(trashService)
|
||||
|
||||
trashAPI := api.Group("/trash")
|
||||
trashAPI.Use(middleware.AuthMiddleware())
|
||||
|
||||
// ============= TRASH CATEGORY ROUTES =============
|
||||
|
||||
// Create trash category (JSON)
|
||||
trashAPI.Post("/category", trashHandler.CreateTrashCategory)
|
||||
|
||||
// Create trash category with icon (form-data)
|
||||
trashAPI.Post("/category/with-icon", trashHandler.CreateTrashCategoryWithIcon)
|
||||
|
||||
// Create trash category with details (JSON)
|
||||
trashAPI.Post("/category/with-details", trashHandler.CreateTrashCategoryWithDetails)
|
||||
|
||||
// Get all trash categories (with optional query param: ?with_details=true)
|
||||
trashAPI.Get("/category", trashHandler.GetAllTrashCategories)
|
||||
|
||||
// Get trash category by ID (with optional query param: ?with_details=true)
|
||||
trashAPI.Get("/category/:id", trashHandler.GetTrashCategoryByID)
|
||||
|
||||
// Update trash category (JSON)
|
||||
trashAPI.Put("/category/:id", trashHandler.UpdateTrashCategory)
|
||||
|
||||
// Update trash category with icon (form-data)
|
||||
trashAPI.Put("/category/:id/with-icon", trashHandler.UpdateTrashCategoryWithIcon)
|
||||
|
||||
// Delete trash category
|
||||
trashAPI.Delete("/category/:id", trashHandler.DeleteTrashCategory)
|
||||
|
||||
// ============= TRASH DETAIL ROUTES =============
|
||||
|
||||
// Create trash detail (JSON)
|
||||
trashAPI.Post("/detail", trashHandler.CreateTrashDetail)
|
||||
|
||||
// Create trash detail with icon (form-data)
|
||||
trashAPI.Post("/detail/with-icon", trashHandler.CreateTrashDetailWithIcon)
|
||||
|
||||
// Add trash detail to specific category (JSON)
|
||||
trashAPI.Post("/category/:categoryId/detail", trashHandler.AddTrashDetailToCategory)
|
||||
|
||||
// Add trash detail to specific category with icon (form-data)
|
||||
trashAPI.Post("/category/:categoryId/detail/with-icon", trashHandler.AddTrashDetailToCategoryWithIcon)
|
||||
|
||||
// Get trash details by category ID
|
||||
trashAPI.Get("/category/:categoryId/details", trashHandler.GetTrashDetailsByCategory)
|
||||
|
||||
// Get trash detail by ID
|
||||
trashAPI.Get("/detail/:id", trashHandler.GetTrashDetailByID)
|
||||
|
||||
// Update trash detail (JSON)
|
||||
trashAPI.Put("/detail/:id", trashHandler.UpdateTrashDetail)
|
||||
|
||||
// Update trash detail with icon (form-data)
|
||||
trashAPI.Put("/detail/:id/with-icon", trashHandler.UpdateTrashDetailWithIcon)
|
||||
|
||||
// Delete trash detail
|
||||
trashAPI.Delete("/detail/:id", trashHandler.DeleteTrashDetail)
|
||||
|
||||
// ============= BULK OPERATIONS ROUTES =============
|
||||
|
||||
// Bulk create trash details for specific category
|
||||
trashAPI.Post("/category/:categoryId/details/bulk", trashHandler.BulkCreateTrashDetails)
|
||||
|
||||
// Bulk delete trash details
|
||||
trashAPI.Delete("/details/bulk", trashHandler.BulkDeleteTrashDetails)
|
||||
|
||||
// Reorder trash details within a category
|
||||
trashAPI.Put("/category/:categoryId/details/reorder", trashHandler.ReorderTrashDetails)
|
||||
}
|
||||
|
|
|
@ -8,14 +8,13 @@ import (
|
|||
"rijig/internal/userprofile"
|
||||
"rijig/model"
|
||||
"rijig/utils"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserPinService interface {
|
||||
CreateUserPin(ctx context.Context, userID, deviceId string, dto *RequestPinDTO) (*authentication.AuthResponse, error)
|
||||
VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error)
|
||||
VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*authentication.AuthResponse, error)
|
||||
}
|
||||
|
||||
type userPinService struct {
|
||||
|
@ -100,7 +99,7 @@ func (s *userPinService) CreateUserPin(ctx context.Context, userID, deviceId str
|
|||
)
|
||||
|
||||
return &authentication.AuthResponse{
|
||||
Message: "Isi data diri berhasil",
|
||||
Message: "mantap semuanya completed",
|
||||
AccessToken: tokenResponse.AccessToken,
|
||||
RefreshToken: tokenResponse.RefreshToken,
|
||||
TokenType: string(tokenResponse.TokenType),
|
||||
|
@ -111,7 +110,7 @@ func (s *userPinService) CreateUserPin(ctx context.Context, userID, deviceId str
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*utils.TokenResponse, error) {
|
||||
func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID string, pin *RequestPinDTO) (*authentication.AuthResponse, error) {
|
||||
user, err := s.authRepo.FindUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
|
@ -126,6 +125,42 @@ func (s *userPinService) VerifyUserPin(ctx context.Context, userID, deviceID str
|
|||
return nil, fmt.Errorf("PIN does not match, %s , %s", userPin.Pin, pin.Pin)
|
||||
}
|
||||
|
||||
roleName := strings.ToLower(user.Role.RoleName)
|
||||
return utils.GenerateTokenPair(user.ID, roleName, deviceID, user.RegistrationStatus, int(user.RegistrationProgress))
|
||||
// roleName := strings.ToLower(user.Role.RoleName)
|
||||
|
||||
// updated, err := s.userProfileRepo.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)
|
||||
// }
|
||||
|
||||
tokenResponse, err := utils.GenerateTokenPair(
|
||||
user.ID,
|
||||
user.Role.RoleName,
|
||||
deviceID,
|
||||
user.RegistrationStatus,
|
||||
int(user.RegistrationProgress),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal generate token: %v", err)
|
||||
}
|
||||
|
||||
nextStep := utils.GetNextRegistrationStep(
|
||||
user.Role.RoleName,
|
||||
int(user.RegistrationProgress),
|
||||
user.RegistrationStatus,
|
||||
)
|
||||
|
||||
return &authentication.AuthResponse{
|
||||
Message: "mantap semuanya completed",
|
||||
AccessToken: tokenResponse.AccessToken,
|
||||
RefreshToken: tokenResponse.RefreshToken,
|
||||
TokenType: string(tokenResponse.TokenType),
|
||||
ExpiresIn: tokenResponse.ExpiresIn,
|
||||
RegistrationStatus: user.RegistrationStatus,
|
||||
NextStep: nextStep,
|
||||
SessionID: tokenResponse.SessionID,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -1,156 +1,311 @@
|
|||
package whatsapp
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"rijig/config"
|
||||
"rijig/utils"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type APIResponse struct {
|
||||
Meta map[string]interface{} `json:"meta"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
type QRResponse struct {
|
||||
QRCode string `json:"qr_code,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func WhatsAppQRPageHandler(c *fiber.Ctx) error {
|
||||
type StatusResponse struct {
|
||||
IsConnected bool `json:"is_connected"`
|
||||
IsLoggedIn bool `json:"is_logged_in"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type SendMessageRequest struct {
|
||||
PhoneNumber string `json:"phone_number" validate:"required"`
|
||||
Message string `json:"message" validate:"required"`
|
||||
}
|
||||
|
||||
type SendMessageResponse struct {
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
func GenerateQRHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "WhatsApp service not initialized",
|
||||
},
|
||||
})
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
// Jika sudah login, tampilkan halaman success
|
||||
if wa.IsLoggedIn() {
|
||||
templatePath := filepath.Join("internal", "whatsapp", "success_scan.html")
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "Unable to load success template: " + err.Error(),
|
||||
},
|
||||
})
|
||||
data := QRResponse{
|
||||
Status: "logged_in",
|
||||
Message: "WhatsApp is already connected and logged in",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return tmpl.Execute(c.Response().BodyWriter(), nil)
|
||||
return utils.SuccessWithData(c, "Already logged in", data)
|
||||
}
|
||||
|
||||
qrDataURI, err := wa.GenerateQR()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "Failed to generate QR code: " + err.Error(),
|
||||
},
|
||||
})
|
||||
return utils.InternalServerError(c, "Failed to generate QR code: "+err.Error())
|
||||
}
|
||||
|
||||
if qrDataURI == "success" {
|
||||
// Login berhasil, tampilkan halaman success
|
||||
templatePath := filepath.Join("internal", "whatsapp", "success_scan.html")
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "Unable to load success template: " + err.Error(),
|
||||
},
|
||||
})
|
||||
switch qrDataURI {
|
||||
case "success":
|
||||
data := QRResponse{
|
||||
Status: "login_success",
|
||||
Message: "WhatsApp login successful",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
return utils.SuccessWithData(c, "Successfully logged in", data)
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return tmpl.Execute(c.Response().BodyWriter(), nil)
|
||||
}
|
||||
|
||||
if qrDataURI == "already_connected" {
|
||||
// Sudah terhubung, tampilkan halaman success
|
||||
templatePath := filepath.Join("internal", "whatsapp", "success_scan.html")
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "Unable to load success template: " + err.Error(),
|
||||
},
|
||||
})
|
||||
case "already_connected":
|
||||
data := QRResponse{
|
||||
Status: "already_connected",
|
||||
Message: "WhatsApp is already connected",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
return utils.SuccessWithData(c, "Already connected", data)
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return tmpl.Execute(c.Response().BodyWriter(), nil)
|
||||
default:
|
||||
|
||||
data := QRResponse{
|
||||
QRCode: qrDataURI,
|
||||
Status: "qr_generated",
|
||||
Message: "Scan QR code with WhatsApp to login",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
return utils.SuccessWithData(c, "QR code generated successfully", data)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckLoginStatusHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
// Tampilkan QR code scanner
|
||||
templatePath := filepath.Join("internal", "whatsapp", "scanner.html")
|
||||
tmpl, err := template.ParseFiles(templatePath)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "Unable to load scanner template: " + err.Error(),
|
||||
},
|
||||
})
|
||||
// if !wa.IsLoggedIn() {
|
||||
// return utils.Unauthorized(c, "WhatsApp not logged in")
|
||||
// }
|
||||
|
||||
isConnected := wa.IsConnected()
|
||||
isLoggedIn := wa.IsLoggedIn()
|
||||
|
||||
var status string
|
||||
var message string
|
||||
|
||||
if isLoggedIn && isConnected {
|
||||
status = "connected_and_logged_in"
|
||||
message = "WhatsApp is connected and logged in"
|
||||
} else if isLoggedIn {
|
||||
status = "logged_in_but_disconnected"
|
||||
message = "WhatsApp is logged in but disconnected"
|
||||
} else if isConnected {
|
||||
status = "connected_but_not_logged_in"
|
||||
message = "WhatsApp is connected but not logged in"
|
||||
} else {
|
||||
status = "disconnected"
|
||||
message = "WhatsApp is disconnected"
|
||||
}
|
||||
|
||||
c.Set("Content-Type", "text/html")
|
||||
return tmpl.Execute(c.Response().BodyWriter(), template.URL(qrDataURI))
|
||||
data := StatusResponse{
|
||||
IsConnected: isConnected,
|
||||
IsLoggedIn: isLoggedIn,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "Status retrieved successfully", data)
|
||||
}
|
||||
|
||||
func WhatsAppLogoutHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "WhatsApp service not initialized",
|
||||
},
|
||||
})
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
if !wa.IsLoggedIn() {
|
||||
return utils.BadRequest(c, "No active session to logout")
|
||||
}
|
||||
|
||||
err := wa.Logout()
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": err.Error(),
|
||||
},
|
||||
})
|
||||
return utils.InternalServerError(c, "Failed to logout: "+err.Error())
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "Successfully logged out and session deleted",
|
||||
},
|
||||
})
|
||||
data := map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "Successfully logged out and session deleted", data)
|
||||
}
|
||||
|
||||
func WhatsAppStatusHandler(c *fiber.Ctx) error {
|
||||
func SendMessageHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "error",
|
||||
"message": "WhatsApp service not initialized",
|
||||
},
|
||||
})
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
status := map[string]interface{}{
|
||||
"is_connected": wa.IsConnected(),
|
||||
"is_logged_in": wa.IsLoggedIn(),
|
||||
if !wa.IsLoggedIn() {
|
||||
return utils.Unauthorized(c, "WhatsApp not logged in")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(APIResponse{
|
||||
Meta: map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "WhatsApp status retrieved successfully",
|
||||
},
|
||||
Data: status,
|
||||
})
|
||||
req := GetValidatedSendMessageRequest(c)
|
||||
if req == nil {
|
||||
return utils.BadRequest(c, "Invalid request data")
|
||||
}
|
||||
|
||||
err := wa.SendMessage(req.PhoneNumber, req.Message)
|
||||
if err != nil {
|
||||
return utils.InternalServerError(c, "Failed to send message: "+err.Error())
|
||||
}
|
||||
|
||||
data := SendMessageResponse{
|
||||
PhoneNumber: req.PhoneNumber,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "Message sent successfully", data)
|
||||
}
|
||||
|
||||
func GetDeviceInfoHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
if !wa.IsLoggedIn() {
|
||||
return utils.Unauthorized(c, "WhatsApp not logged in")
|
||||
}
|
||||
|
||||
var deviceInfo map[string]interface{}
|
||||
if wa.Client != nil && wa.Client.Store.ID != nil {
|
||||
deviceInfo = map[string]interface{}{
|
||||
"device_id": wa.Client.Store.ID.User,
|
||||
"device_name": wa.Client.Store.ID.Device,
|
||||
"is_logged_in": wa.IsLoggedIn(),
|
||||
"is_connected": wa.IsConnected(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
} else {
|
||||
deviceInfo = map[string]interface{}{
|
||||
"device_id": nil,
|
||||
"device_name": nil,
|
||||
"is_logged_in": false,
|
||||
"is_connected": false,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "Device info retrieved successfully", deviceInfo)
|
||||
}
|
||||
|
||||
func HealthCheckHandler(c *fiber.Ctx) error {
|
||||
wa := config.GetWhatsAppService()
|
||||
if wa == nil {
|
||||
return utils.InternalServerError(c, "WhatsApp service not initialized")
|
||||
}
|
||||
|
||||
healthData := map[string]interface{}{
|
||||
"service_status": "running",
|
||||
"container_status": wa.Container != nil,
|
||||
"client_status": wa.Client != nil,
|
||||
"is_connected": wa.IsConnected(),
|
||||
"is_logged_in": wa.IsLoggedIn(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
message := "WhatsApp service is healthy"
|
||||
if !wa.IsConnected() || !wa.IsLoggedIn() {
|
||||
message = "WhatsApp service is running but not fully operational"
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, message, healthData)
|
||||
}
|
||||
|
||||
func validatePhoneNumber(phoneNumber string) error {
|
||||
|
||||
cleaned := strings.ReplaceAll(phoneNumber, " ", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "-", "")
|
||||
cleaned = strings.ReplaceAll(cleaned, "+", "")
|
||||
|
||||
if !regexp.MustCompile(`^\d+$`).MatchString(cleaned) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Phone number must contain only digits")
|
||||
}
|
||||
|
||||
if len(cleaned) < 10 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Phone number too short. Include country code (e.g., 628123456789)")
|
||||
}
|
||||
|
||||
if len(cleaned) > 15 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Phone number too long")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateMessage(message string) error {
|
||||
|
||||
if strings.TrimSpace(message) == "" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Message cannot be empty")
|
||||
}
|
||||
|
||||
if len(message) > 4096 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Message too long. Maximum 4096 characters allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateSendMessageRequest(c *fiber.Ctx) error {
|
||||
var req SendMessageRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "Invalid JSON format: "+err.Error())
|
||||
}
|
||||
|
||||
if err := validatePhoneNumber(req.PhoneNumber); err != nil {
|
||||
return utils.BadRequest(c, err.Error())
|
||||
}
|
||||
|
||||
if err := validateMessage(req.Message); err != nil {
|
||||
return utils.BadRequest(c, err.Error())
|
||||
}
|
||||
|
||||
req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, " ", "")
|
||||
req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, "-", "")
|
||||
req.PhoneNumber = strings.ReplaceAll(req.PhoneNumber, "+", "")
|
||||
|
||||
c.Locals("validatedRequest", req)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func GetValidatedSendMessageRequest(c *fiber.Ctx) *SendMessageRequest {
|
||||
if req, ok := c.Locals("validatedRequest").(SendMessageRequest); ok {
|
||||
return &req
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateContentType() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
|
||||
if c.Method() == "GET" {
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
contentType := c.Get("Content-Type")
|
||||
if !strings.Contains(contentType, "application/json") {
|
||||
return utils.BadRequest(c, "Content-Type must be application/json")
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,32 @@
|
|||
package whatsapp
|
||||
|
||||
import (
|
||||
"rijig/middleware"
|
||||
"rijig/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func WhatsAppRouter(api fiber.Router) {
|
||||
api.Get("/whatsapp-status", WhatsAppStatusHandler)
|
||||
api.Get("/whatsapp/pw=admin1234", WhatsAppQRPageHandler)
|
||||
api.Post("/logout/whastapp", WhatsAppLogoutHandler)
|
||||
|
||||
whatsapp := api.Group("/whatsapp")
|
||||
|
||||
whatsapp.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole())
|
||||
|
||||
whatsapp.Post("/generate-qr", GenerateQRHandler)
|
||||
whatsapp.Get("/status", CheckLoginStatusHandler)
|
||||
whatsapp.Post("/logout", WhatsAppLogoutHandler)
|
||||
|
||||
messaging := whatsapp.Group("/message")
|
||||
messaging.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole())
|
||||
messaging.Post("/send", ValidateSendMessageRequest, SendMessageHandler)
|
||||
|
||||
management := whatsapp.Group("/management")
|
||||
management.Use(middleware.AuthMiddleware(), middleware.RequireAdminRole())
|
||||
management.Get("/device-info", GetDeviceInfoHandler)
|
||||
management.Get("/health", HealthCheckHandler)
|
||||
|
||||
api.Get("/whatsapp/ping", func(c *fiber.Ctx) error {
|
||||
return utils.Success(c, "WhatsApp service is running")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package worker
|
||||
/*
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
@ -8,19 +8,18 @@ import (
|
|||
"time"
|
||||
|
||||
"rijig/config"
|
||||
"rijig/dto"
|
||||
"rijig/internal/repositories"
|
||||
"rijig/internal/services"
|
||||
"rijig/internal/cart"
|
||||
"rijig/internal/trash"
|
||||
"rijig/model"
|
||||
)
|
||||
|
||||
type CartWorker struct {
|
||||
cartService services.CartService
|
||||
cartRepo repositories.CartRepository
|
||||
trashRepo repositories.TrashRepository
|
||||
cartService cart.CartService
|
||||
cartRepo cart.CartRepository
|
||||
trashRepo trash.TrashRepositoryInterface
|
||||
}
|
||||
|
||||
func NewCartWorker(cartService services.CartService, cartRepo repositories.CartRepository, trashRepo repositories.TrashRepository) *CartWorker {
|
||||
func NewCartWorker(cartService cart.CartService, cartRepo cart.CartRepository, trashRepo trash.TrashRepositoryInterface) *CartWorker {
|
||||
return &CartWorker{
|
||||
cartService: cartService,
|
||||
cartRepo: cartRepo,
|
||||
|
@ -32,7 +31,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error {
|
|||
ctx := context.Background()
|
||||
threshold := 1 * time.Minute
|
||||
|
||||
keys, err := services.GetExpiringCartKeys(ctx, threshold)
|
||||
keys, err := cart.GetExpiringCartKeys(ctx, threshold)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -59,7 +58,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error {
|
|||
|
||||
if hasCart {
|
||||
|
||||
if err := services.DeleteCartFromRedis(ctx, userID); err != nil {
|
||||
if err := cart.DeleteCartFromRedis(ctx, userID); err != nil {
|
||||
log.Printf("[CART-WORKER] Failed to delete Redis cache for user %s: %v", userID, err)
|
||||
} else {
|
||||
log.Printf("[CART-WORKER] Deleted Redis cache for user %s (already has DB cart)", userID)
|
||||
|
@ -78,7 +77,7 @@ func (w *CartWorker) AutoCommitExpiringCarts() error {
|
|||
continue
|
||||
}
|
||||
|
||||
if err := services.DeleteCartFromRedis(ctx, userID); err != nil {
|
||||
if err := cart.DeleteCartFromRedis(ctx, userID); err != nil {
|
||||
log.Printf("[CART-WORKER] Warning: Failed to delete Redis key after commit for user %s: %v", userID, err)
|
||||
}
|
||||
|
||||
|
@ -98,13 +97,13 @@ func (w *CartWorker) extractUserIDFromKey(key string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.RequestCartDTO, error) {
|
||||
func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*cart.RequestCartDTO, error) {
|
||||
val, err := config.RedisClient.Get(ctx, key).Result()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cart dto.RequestCartDTO
|
||||
var cart cart.RequestCartDTO
|
||||
if err := json.Unmarshal([]byte(val), &cart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -112,7 +111,7 @@ func (w *CartWorker) getCartFromRedis(ctx context.Context, key string) (*dto.Req
|
|||
return &cart, nil
|
||||
}
|
||||
|
||||
func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *dto.RequestCartDTO) error {
|
||||
func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData *cart.RequestCartDTO) error {
|
||||
if len(cartData.CartItems) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -156,4 +155,3 @@ func (w *CartWorker) commitCartToDB(ctx context.Context, userID string, cartData
|
|||
|
||||
return w.cartRepo.CreateCartWithItems(ctx, newCart)
|
||||
}
|
||||
*/
|
|
@ -1,199 +0,0 @@
|
|||
package middleware
|
||||
/*
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"rijig/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func RateLimitByUser(maxRequests int, duration time.Duration) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
claims, err := GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("rate_limit:%s:%s", claims.UserID, c.Route().Path)
|
||||
|
||||
count, err := utils.IncrementCounter(key, duration)
|
||||
if err != nil {
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
if count > int64(maxRequests) {
|
||||
|
||||
ttl, _ := utils.GetTTL(key)
|
||||
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
|
||||
"error": "Rate limit exceeded",
|
||||
"message": "Terlalu banyak permintaan, silakan coba lagi nanti",
|
||||
"retry_after": int64(ttl.Seconds()),
|
||||
"limit": maxRequests,
|
||||
"remaining": 0,
|
||||
})
|
||||
}
|
||||
|
||||
c.Set("X-RateLimit-Limit", fmt.Sprintf("%d", maxRequests))
|
||||
c.Set("X-RateLimit-Remaining", fmt.Sprintf("%d", maxRequests-int(count)))
|
||||
c.Set("X-RateLimit-Reset", fmt.Sprintf("%d", time.Now().Add(duration).Unix()))
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SessionValidation() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
claims, err := GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if claims.SessionID == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Invalid session",
|
||||
"message": "Session tidak valid",
|
||||
})
|
||||
}
|
||||
|
||||
sessionKey := fmt.Sprintf("session:%s", claims.SessionID)
|
||||
var sessionData map[string]interface{}
|
||||
err = utils.GetCache(sessionKey, &sessionData)
|
||||
if err != nil {
|
||||
if err.Error() == "ErrCacheMiss" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Session not found",
|
||||
"message": "Session tidak ditemukan, silakan login kembali",
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Session error",
|
||||
"message": "Terjadi kesalahan saat validasi session",
|
||||
})
|
||||
}
|
||||
|
||||
if sessionData["user_id"] != claims.UserID {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Session mismatch",
|
||||
"message": "Session tidak sesuai dengan user",
|
||||
})
|
||||
}
|
||||
|
||||
if expiryInterface, exists := sessionData["expires_at"]; exists {
|
||||
if expiry, ok := expiryInterface.(float64); ok {
|
||||
if time.Now().Unix() > int64(expiry) {
|
||||
|
||||
utils.DeleteCache(sessionKey)
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Session expired",
|
||||
"message": "Session telah berakhir, silakan login kembali",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireApprovedRegistration() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
claims, err := GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if claims.RegistrationStatus == utils.RegStatusRejected {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Registration rejected",
|
||||
"message": "Registrasi Anda ditolak, silakan hubungi admin",
|
||||
})
|
||||
}
|
||||
|
||||
if claims.RegistrationStatus == utils.RegStatusPending {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Registration pending",
|
||||
"message": "Registrasi Anda masih menunggu persetujuan admin",
|
||||
})
|
||||
}
|
||||
|
||||
if claims.RegistrationStatus != utils.RegStatusComplete {
|
||||
progress := utils.GetUserRegistrationProgress(claims.UserID)
|
||||
nextStep := utils.GetNextRegistrationStep(claims.Role, progress)
|
||||
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Registration incomplete",
|
||||
"message": "Silakan lengkapi registrasi terlebih dahulu",
|
||||
"registration_status": claims.RegistrationStatus,
|
||||
"next_step": nextStep,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func ConditionalAuth(condition func(*utils.JWTClaims) bool, errorMessage string) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
claims, err := GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !condition(claims) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "Condition not met",
|
||||
"message": errorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireSpecificRole(role string) fiber.Handler {
|
||||
return ConditionalAuth(
|
||||
func(claims *utils.JWTClaims) bool {
|
||||
return claims.Role == role
|
||||
},
|
||||
fmt.Sprintf("Akses ini hanya untuk role %s", role),
|
||||
)
|
||||
}
|
||||
|
||||
func RequireCompleteRegistrationAndSpecificRole(role string) fiber.Handler {
|
||||
return ConditionalAuth(
|
||||
func(claims *utils.JWTClaims) bool {
|
||||
return claims.Role == role && utils.IsRegistrationComplete(claims.RegistrationStatus)
|
||||
},
|
||||
fmt.Sprintf("Akses ini hanya untuk role %s dengan registrasi lengkap", role),
|
||||
)
|
||||
}
|
||||
|
||||
func DeviceValidation() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
claims, err := GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceID := c.Get("X-Device-ID")
|
||||
if deviceID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "Device ID required",
|
||||
"message": "Device ID diperlukan",
|
||||
})
|
||||
}
|
||||
|
||||
if claims.DeviceID != deviceID {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "Device mismatch",
|
||||
"message": "Token tidak valid untuk device ini",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
*/
|
|
@ -503,7 +503,7 @@ func DeviceValidation() fiber.Handler {
|
|||
return err
|
||||
}
|
||||
|
||||
deviceID := c.Get("X-Device-ID")
|
||||
deviceID := claims.DeviceID
|
||||
if deviceID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(&AuthError{
|
||||
Code: "MISSING_DEVICE_ID",
|
||||
|
|
|
@ -3,11 +3,13 @@ package router
|
|||
import (
|
||||
"os"
|
||||
|
||||
"rijig/internal/about"
|
||||
"rijig/internal/article"
|
||||
"rijig/internal/authentication"
|
||||
"rijig/internal/company"
|
||||
"rijig/internal/identitycart"
|
||||
"rijig/internal/role"
|
||||
"rijig/internal/trash"
|
||||
"rijig/internal/userpin"
|
||||
"rijig/internal/userprofile"
|
||||
"rijig/internal/whatsapp"
|
||||
|
@ -22,8 +24,8 @@ import (
|
|||
func SetupRoutes(app *fiber.App) {
|
||||
apa := app.Group(os.Getenv("BASE_URL"))
|
||||
apa.Static("/uploads", "./public"+os.Getenv("BASE_URL")+"/uploads")
|
||||
a := app.Group(os.Getenv("BASE_URL"))
|
||||
whatsapp.WhatsAppRouter(a)
|
||||
// a := app.Group(os.Getenv("BASE_URL"))
|
||||
// whatsapp.WhatsAppRouter(a)
|
||||
|
||||
api := app.Group(os.Getenv("BASE_URL"))
|
||||
api.Use(middleware.APIKeyMiddleware)
|
||||
|
@ -37,6 +39,9 @@ func SetupRoutes(app *fiber.App) {
|
|||
article.ArticleRouter(api)
|
||||
userprofile.UserProfileRouter(api)
|
||||
wilayahindo.WilayahRouter(api)
|
||||
trash.TrashRouter(api)
|
||||
about.AboutRouter(api)
|
||||
whatsapp.WhatsAppRouter(api)
|
||||
|
||||
// || auth router || //
|
||||
// presentation.AuthRouter(api)
|
||||
|
|
Loading…
Reference in New Issue