Compare commits

...

10 Commits

Author SHA1 Message Date
Aditya Siregar
a759e0f57c init 2025-07-30 23:18:20 +07:00
aditya.siregar
4a921df55d init docker 2025-07-18 20:15:12 +07:00
aditya.siregar
4f5950543e init 2025-07-18 20:10:29 +07:00
aditya.siregar
1bceae010b update 2025-06-29 13:07:12 +07:00
aditya.siregar
46b30277ef Update with Tx 2025-06-28 16:34:19 +07:00
aditya.siregar
20d1ef5f2a add refund 2025-06-28 15:29:51 +07:00
aditya.siregar
f31f83e485 upodate system 2025-06-27 13:01:39 +07:00
aditya.siregar
1201b2e45b Add void print 2025-06-24 02:47:44 +07:00
aditya.siregar
53014d90ab add notes description 2025-06-21 17:19:40 +07:00
aditya.siregar
ebb33186b8 Add refund order 2025-06-14 21:17:13 +07:00
522 changed files with 27582 additions and 31269 deletions

44
.air.toml Normal file
View File

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "go build -o ./tmp/main cmd/server/main.go"
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_root = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@ -1,17 +1,82 @@
# Files # Git
.dockerignore .git
.editorconfig
.gitignore .gitignore
.env.* .gitattributes
Dockerfile
Makefile
LICENSE
**/*.md
**/*_test.go
*.out
bin/ # Documentation
# Folders *.md
.git/ docs/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Logs
*.log
logs/
# Runtime data
pids/
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
*.cover
# Node modules (if any frontend assets)
node_modules/
# Temporary files
tmp/
temp/
# Build artifacts
server
*.exe
*.test
*.prof
# Test scripts
test-build.sh
# Temporary directories
tmp/
# Docker files
Dockerfile
.dockerignore
# CI/CD
.github/ .github/
build/ .gitlab-ci.yml
# Environment files
.env
.env.local
.env.*.local
# Test files
*_test.go
# Migration files (if not needed in container)
migrations/
# Development scripts
scripts/dev/
# Cache directories
.cache/

292
ANALYTICS_API.md Normal file
View File

@ -0,0 +1,292 @@
# Analytics API Documentation
This document describes the analytics APIs implemented for the POS system, providing insights into sales, payment methods, products, and overall business performance.
## Overview
The analytics APIs provide comprehensive business intelligence for POS operations, including:
- **Payment Method Analytics**: Track totals for each payment method by date
- **Sales Analytics**: Monitor sales performance over time
- **Product Analytics**: Analyze product performance and revenue
- **Dashboard Analytics**: Overview of key business metrics
## Authentication
All analytics endpoints require authentication and admin/manager privileges. Include the JWT token in the Authorization header:
```
Authorization: Bearer <your-jwt-token>
```
## Base URL
```
GET /api/v1/analytics/{endpoint}
```
## Endpoints
### 1. Payment Method Analytics
**Endpoint:** `GET /api/v1/analytics/payment-methods`
**Description:** Get payment method totals for a given date range. This is the primary endpoint for tracking payment method performance.
**Query Parameters:**
- `organization_id` (required): UUID of the organization
- `outlet_id` (optional): UUID of specific outlet to filter by
- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD)
- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD)
- `group_by` (optional): Grouping interval - "day", "hour", "week", "month" (default: "day")
**Example Request:**
```bash
curl -X GET "http://localhost:8080/api/v1/analytics/payment-methods?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31" \
-H "Authorization: Bearer <your-jwt-token>"
```
**Response:**
```json
{
"success": true,
"data": {
"organization_id": "123e4567-e89b-12d3-a456-426614174000",
"outlet_id": null,
"date_from": "2024-01-01T00:00:00Z",
"date_to": "2024-01-31T23:59:59Z",
"group_by": "day",
"summary": {
"total_amount": 15000.00,
"total_orders": 150,
"total_payments": 180,
"average_order_value": 100.00
},
"data": [
{
"payment_method_id": "456e7890-e89b-12d3-a456-426614174001",
"payment_method_name": "Cash",
"payment_method_type": "cash",
"total_amount": 8000.00,
"order_count": 80,
"payment_count": 80,
"percentage": 53.33
},
{
"payment_method_id": "789e0123-e89b-12d3-a456-426614174002",
"payment_method_name": "Credit Card",
"payment_method_type": "card",
"total_amount": 7000.00,
"order_count": 70,
"payment_count": 100,
"percentage": 46.67
}
]
}
}
```
### 2. Sales Analytics
**Endpoint:** `GET /api/v1/analytics/sales`
**Description:** Get sales performance data over time.
**Query Parameters:**
- `organization_id` (required): UUID of the organization
- `outlet_id` (optional): UUID of specific outlet to filter by
- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD)
- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD)
- `group_by` (optional): Grouping interval - "day", "hour", "week", "month" (default: "day")
**Example Request:**
```bash
curl -X GET "http://localhost:8080/api/v1/analytics/sales?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31&group_by=day" \
-H "Authorization: Bearer <your-jwt-token>"
```
**Response:**
```json
{
"success": true,
"data": {
"organization_id": "123e4567-e89b-12d3-a456-426614174000",
"outlet_id": null,
"date_from": "2024-01-01T00:00:00Z",
"date_to": "2024-01-31T23:59:59Z",
"group_by": "day",
"summary": {
"total_sales": 15000.00,
"total_orders": 150,
"total_items": 450,
"average_order_value": 100.00,
"total_tax": 1500.00,
"total_discount": 500.00,
"net_sales": 13000.00
},
"data": [
{
"date": "2024-01-01T00:00:00Z",
"sales": 500.00,
"orders": 5,
"items": 15,
"tax": 50.00,
"discount": 20.00,
"net_sales": 430.00
}
]
}
}
```
### 3. Product Analytics
**Endpoint:** `GET /api/v1/analytics/products`
**Description:** Get top-performing products by revenue.
**Query Parameters:**
- `organization_id` (required): UUID of the organization
- `outlet_id` (optional): UUID of specific outlet to filter by
- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD)
- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD)
- `limit` (optional): Number of products to return (1-100, default: 10)
**Example Request:**
```bash
curl -X GET "http://localhost:8080/api/v1/analytics/products?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31&limit=5" \
-H "Authorization: Bearer <your-jwt-token>"
```
**Response:**
```json
{
"success": true,
"data": {
"organization_id": "123e4567-e89b-12d3-a456-426614174000",
"outlet_id": null,
"date_from": "2024-01-01T00:00:00Z",
"date_to": "2024-01-31T23:59:59Z",
"data": [
{
"product_id": "abc123-e89b-12d3-a456-426614174000",
"product_name": "Coffee Latte",
"category_id": "cat123-e89b-12d3-a456-426614174000",
"category_name": "Beverages",
"quantity_sold": 100,
"revenue": 2500.00,
"average_price": 25.00,
"order_count": 80
}
]
}
}
```
### 4. Dashboard Analytics
**Endpoint:** `GET /api/v1/analytics/dashboard`
**Description:** Get comprehensive dashboard overview with key metrics.
**Query Parameters:**
- `organization_id` (required): UUID of the organization
- `outlet_id` (optional): UUID of specific outlet to filter by
- `date_from` (required): Start date (ISO 8601 format: YYYY-MM-DD)
- `date_to` (required): End date (ISO 8601 format: YYYY-MM-DD)
**Example Request:**
```bash
curl -X GET "http://localhost:8080/api/v1/analytics/dashboard?organization_id=123e4567-e89b-12d3-a456-426614174000&date_from=2024-01-01&date_to=2024-01-31" \
-H "Authorization: Bearer <your-jwt-token>"
```
**Response:**
```json
{
"success": true,
"data": {
"organization_id": "123e4567-e89b-12d3-a456-426614174000",
"outlet_id": null,
"date_from": "2024-01-01T00:00:00Z",
"date_to": "2024-01-31T23:59:59Z",
"overview": {
"total_sales": 15000.00,
"total_orders": 150,
"average_order_value": 100.00,
"total_customers": 120,
"voided_orders": 5,
"refunded_orders": 3
},
"top_products": [...],
"payment_methods": [...],
"recent_sales": [...]
}
}
```
## Error Responses
All endpoints return consistent error responses:
```json
{
"success": false,
"error": "error_type",
"message": "Error description"
}
```
Common error types:
- `invalid_request`: Invalid query parameters
- `validation_failed`: Request validation failed
- `internal_error`: Server-side error
- `unauthorized`: Authentication required
## Date Format
All date parameters should be in ISO 8601 format: `YYYY-MM-DD`
Examples:
- `2024-01-01` (January 1, 2024)
- `2024-12-31` (December 31, 2024)
## Filtering
- **Organization-level**: All analytics are scoped to a specific organization
- **Outlet-level**: Optional filtering by specific outlet
- **Date range**: Required date range for all analytics queries
- **Time grouping**: Flexible grouping by hour, day, week, or month
## Performance Considerations
- Analytics queries are optimized for read performance
- Large date ranges may take longer to process
- Consider using appropriate date ranges for optimal performance
- Results are cached where possible for better response times
## Use Cases
### Payment Method Analysis
- Track which payment methods are most popular
- Monitor payment method trends over time
- Identify payment method preferences by outlet
- Calculate payment method percentages for reporting
### Sales Performance
- Monitor daily/weekly/monthly sales trends
- Track order volumes and average order values
- Analyze tax and discount patterns
- Compare sales performance across outlets
### Product Performance
- Identify top-selling products
- Analyze product revenue and profitability
- Track product category performance
- Monitor product order frequency
### Business Intelligence
- Dashboard overview for management
- Key performance indicators (KPIs)
- Trend analysis and forecasting
- Operational insights for decision making

320
DOCKER.md Normal file
View File

@ -0,0 +1,320 @@
# Docker Setup for APSKEL POS Backend
This document describes how to run the APSKEL POS Backend using Docker and Docker Compose.
## Prerequisites
- Docker (version 20.10 or later)
- Docker Compose (version 2.0 or later)
- Git (for cloning the repository)
- Go 1.21+ (for local development)
## Quick Start
### 1. Build and Run Production Environment
```bash
# Build and start all services
./docker-build.sh run
# Or manually:
docker-compose up -d
```
The application will be available at:
- **Backend API**: http://localhost:3300
- **Database**: localhost:5432
- **Redis**: localhost:6379
### 2. Development Environment
```bash
# Start development environment with live reload
./docker-build.sh dev
# Or manually:
docker-compose --profile dev up -d
```
Development environment provides:
- **Backend API (Dev)**: http://localhost:3001 (with live reload)
- **Backend API (Prod)**: http://localhost:3300
- Auto-restart on code changes using Air
### 3. Database Migrations
```bash
# Run database migrations
./docker-build.sh migrate
# Or manually:
docker-compose --profile migrate up migrate
```
## Build Script Usage
The `docker-build.sh` script provides convenient commands:
```bash
# Build Docker image
./docker-build.sh build
# Build and run production environment
./docker-build.sh run
# Start development environment
./docker-build.sh dev
# Run database migrations
./docker-build.sh migrate
# Stop all containers
./docker-build.sh stop
# Clean up containers and images
./docker-build.sh clean
# Show container logs
./docker-build.sh logs
# Show help
./docker-build.sh help
```
## Services
### Backend API
- **Port**: 3300 (production), 3001 (development)
- **Health Check**: http://localhost:3300/health
- **Environment**: Configurable via `infra/` directory
- **User**: Runs as non-root user for security
### PostgreSQL Database
- **Port**: 5432
- **Database**: apskel_pos
- **Username**: apskel
- **Password**: See docker-compose.yaml
- **Volumes**: Persistent data storage
### Redis Cache
- **Port**: 6379
- **Usage**: Caching and session storage
- **Volumes**: Persistent data storage
## Environment Configuration
The application uses configuration files from the `infra/` directory:
- `infra/development.yaml` - Development configuration
- `infra/production.yaml` - Production configuration (create if needed)
### Configuration Structure
```yaml
server:
port: 3300
postgresql:
host: postgres # Use service name in Docker
port: 5432
db: apskel_pos
username: apskel
password: 7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
jwt:
token:
secret: "your-jwt-secret"
expires-ttl: 1440
s3:
access_key_id: "your-s3-key"
access_key_secret: "your-s3-secret"
endpoint: "your-s3-endpoint"
bucket_name: "your-bucket"
log:
log_level: "info"
log_format: "json"
```
## Docker Compose Profiles
### Default Profile (Production)
```bash
docker-compose up -d
```
Starts: postgres, redis, backend
### Development Profile
```bash
docker-compose --profile dev up -d
```
Starts: postgres, redis, backend, backend-dev
### Migration Profile
```bash
docker-compose --profile migrate up migrate
```
Runs: database migrations
## Health Checks
All services include health checks:
- **Backend**: HTTP GET /health
- **PostgreSQL**: pg_isready command
- **Redis**: Redis ping command
## Logging
View logs for specific services:
```bash
# All services
docker-compose logs -f
# Backend only
docker-compose logs -f backend
# Database only
docker-compose logs -f postgres
# Development backend
docker-compose logs -f backend-dev
```
## Volumes
### Persistent Volumes
- `postgres_data`: Database files
- `redis_data`: Redis persistence files
- `go_modules`: Go module cache (development)
### Bind Mounts
- `./infra:/infra:ro`: Configuration files (read-only)
- `./migrations:/app/migrations:ro`: Database migrations (read-only)
- `.:/app`: Source code (development only)
## Security
### Production Security Features
- Non-root user execution
- Read-only configuration mounts
- Minimal base image (Debian slim)
- Health checks for monitoring
- Resource limits (configurable)
### Network Security
- Internal Docker network isolation
- Only necessary ports exposed
- Service-to-service communication via Docker network
## Troubleshooting
### Common Issues
1. **Go Version Compatibility Error**
```bash
# Error: package slices is not in GOROOT
# Solution: Make sure Dockerfile uses Go 1.21+
# Check go.mod file requires Go 1.21 or later
```
2. **Port Already in Use**
```bash
# Check what's using the port
lsof -i :3300
# Change ports in docker-compose.yaml if needed
```
3. **Database Connection Failed**
```bash
# Check if database is running
docker-compose ps postgres
# Check database logs
docker-compose logs postgres
```
4. **Permission Denied**
```bash
# Make sure script is executable
chmod +x docker-build.sh
```
5. **Out of Disk Space**
```bash
# Clean up unused Docker resources
docker system prune -a
# Remove old images
docker image prune -a
```
### Debug Mode
Run containers in debug mode:
```bash
# Start with debug logs
ENV_MODE=development docker-compose up
# Enter running container
docker-compose exec backend sh
# Check application logs
docker-compose logs -f backend
```
### Performance Tuning
For production deployment:
1. **Resource Limits**: Add resource limits to docker-compose.yaml
2. **Environment**: Use production configuration
3. **Logging**: Adjust log levels
4. **Health Checks**: Tune intervals for your needs
```yaml
services:
backend:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
```
## API Testing
Once the application is running, test the API:
```bash
# Health check
curl http://localhost:3300/health
# Analytics endpoint (requires authentication)
curl -H "Authorization: Bearer <token>" \
-H "Organization-ID: <org-id>" \
"http://localhost:3300/api/v1/analytics/profit-loss?date_from=01-12-2023&date_to=31-12-2023"
```
## Deployment
For production deployment:
1. Update configuration in `infra/production.yaml`
2. Set appropriate environment variables
3. Use production Docker Compose file
4. Configure reverse proxy (nginx, traefik)
5. Set up SSL certificates
6. Configure monitoring and logging
```bash
# Production deployment
ENV_MODE=production docker-compose -f docker-compose.prod.yaml up -d
```

View File

@ -1,26 +1,99 @@
# Build Stage # Build Stage
FROM golang:1.20-alpine AS build FROM golang:1.21-alpine AS build
RUN apk --no-cache add tzdata # Install necessary packages including CA certificates
RUN apk --no-cache add ca-certificates tzdata git curl
WORKDIR /src WORKDIR /src
# Copy go mod files first for better caching
COPY go.mod go.sum ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . . COPY . .
# RUN CGO_ENABLED=0 GOOS=linux go build -o /app cmd/klinik-core-service # Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -o /app main.go RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /app cmd/server/main.go
RUN ls -la / # Development Stage
FROM golang:1.21-alpine AS development
# Final Stage # Install air for live reload and other dev tools
FROM gcr.io/distroless/static RUN go install github.com/cosmtrek/air@latest
WORKDIR / # Install necessary packages
RUN apk --no-cache add ca-certificates tzdata git curl
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo WORKDIR /app
COPY --from=build /app /app
# RUN ls -la / # Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Set timezone
ENV TZ=Asia/Jakarta ENV TZ=Asia/Jakarta
# Expose port
EXPOSE 3300
# Use air for live reload in development
CMD ["air", "-c", ".air.toml"]
# Migration Stage
FROM build AS migration
# Install migration tool
RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
WORKDIR /app
# Copy migration files
COPY migrations ./migrations
COPY infra ./infra
# Set the entrypoint for migrations
ENTRYPOINT ["migrate"]
# Production Stage
FROM debian:bullseye-slim AS production
# Install minimal runtime dependencies
RUN apt-get update && apt-get install -y \
ca-certificates \
tzdata \
curl \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user for security
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Copy the binary
COPY --from=build /app /app
# Copy configuration files
COPY --from=build /src/infra /infra
# Change ownership to non-root user
RUN chown -R appuser:appuser /app /infra
# Set timezone
ENV TZ=Asia/Jakarta
# Expose port
EXPOSE 3300
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3300/health || exit 1
# Switch to non-root user
USER appuser
# Set the entrypoint
ENTRYPOINT ["/app"] ENTRYPOINT ["/app"]

View File

@ -1,24 +0,0 @@
FROM golang:1.20-alpine AS build
RUN apk --no-cache add tzdata
WORKDIR /src
COPY . .
# RUN CGO_ENABLED=0 GOOS=linux go build -o /app cmd/klinik-core-service
RUN CGO_ENABLED=0 GOOS=linux go build -o /app main.go
RUN ls -la /
FROM gcr.io/distroless/static
WORKDIR /
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /app /app
# RUN ls -la /
ENV TZ=Asia/Jakarta
ENTRYPOINT ["/app"]

View File

@ -1,9 +1,9 @@
PROJECT_NAME = "enaklo-pos-backend" #PROJECT_NAME = "enaklo-pos-backend"
DB_USERNAME := fortuna_admin DB_USERNAME :=apskel
DB_PASSWORD := Z4G827t9428QFQ%5ESZXW%2343dB%25%214Bmh80 DB_PASSWORD :=7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
DB_HOST := 103.96.146.124 DB_HOST :=62.72.45.250
DB_PORT := 1960 DB_PORT :=5433
DB_NAME := fortuna-staging DB_NAME :=apskel_pos
DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable DB_URL = postgres://$(DB_USERNAME):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable

View File

@ -0,0 +1,120 @@
# Order Void Status Improvement
## Overview
This document describes the improved approach for handling order void status when all items are voided.
## Problem with Previous Approach
The previous implementation only set the `is_void` flag to `true` when voiding orders, but kept the original order status (e.g., "pending", "preparing", etc.). This approach had several issues:
1. **Poor Semantic Meaning**: Orders with status "pending" but `is_void = true` were confusing
2. **Difficult Querying**: Hard to filter voided orders by status alone
3. **Inconsistent State**: Order status didn't reflect the actual business state
4. **Audit Trail Issues**: No clear indication of when and why orders were voided
## Improved Approach
### 1. Status Update Strategy
When an order is voided (either entirely or when all items are voided), the system now:
- **Sets `is_void = true`** (for audit trail and void-specific operations)
- **Updates `status = 'cancelled'`** (for business logic and semantic clarity)
- **Records void metadata** (reason, timestamp, user who voided)
### 2. Benefits
#### **Clear Semantic Meaning**
- Voided orders have status "cancelled" which clearly indicates they are no longer active
- Business logic can rely on status for workflow decisions
- Frontend can easily display voided orders with appropriate styling
#### **Better Querying**
```sql
-- Find all cancelled/voided orders
SELECT * FROM orders WHERE status = 'cancelled';
-- Find all active orders (excluding voided)
SELECT * FROM orders WHERE status != 'cancelled';
-- Find voided orders with audit info
SELECT * FROM orders WHERE is_void = true;
```
#### **Consistent State Management**
- Order status always reflects the current business state
- No conflicting states (e.g., "pending" but voided)
- Easier to implement business rules and validations
#### **Enhanced Audit Trail**
- `is_void` flag for void-specific operations
- `void_reason`, `voided_at`, `voided_by` for detailed audit
- `status = 'cancelled'` for business workflow
### 3. Implementation Details
#### **New Repository Method**
```go
VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error
```
This method updates both status and void flags in a single atomic transaction.
#### **Updated Processor Logic**
```go
// For "ALL" void type
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
return fmt.Errorf("failed to void order: %w", err)
}
// For "ITEM" void type when all items are voided
if allItemsVoided {
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
return fmt.Errorf("failed to void order after all items voided: %w", err)
}
}
```
#### **Database Migration**
Added migration `000021_add_paid_status_to_orders.up.sql` to include "paid" status in the constraint.
### 4. Status Flow
```
Order Created → pending
Items Added/Modified → pending
Order Processing → preparing → ready → completed
Order Voided → cancelled (with is_void = true)
```
### 5. Backward Compatibility
- Existing `is_void` flag is preserved for backward compatibility
- New approach is additive, not breaking
- Existing queries using `is_void` continue to work
- New queries can use `status = 'cancelled'` for better performance
### 6. Best Practices
#### **For Queries**
- Use `status = 'cancelled'` for business logic and filtering
- Use `is_void = true` for void-specific operations and audit trails
- Combine both when you need complete void information
#### **For Business Logic**
- Check `status != 'cancelled'` before allowing modifications
- Use `is_void` flag for void-specific validations
- Always include void reason and user for audit purposes
#### **For Frontend**
- Display cancelled orders with appropriate styling
- Show void reason and timestamp when available
- Disable actions on cancelled orders
## Conclusion
This improved approach provides better semantic meaning, easier querying, and more consistent state management while maintaining backward compatibility. The combination of status updates and void flags creates a robust system for handling order cancellations.

155
OUTLET_TAX_CALCULATION.md Normal file
View File

@ -0,0 +1,155 @@
# Outlet-Based Tax Calculation Implementation
## Overview
This document describes the implementation of outlet-based tax calculation in the order processing system. The system now uses the tax rate configured for each outlet instead of a hardcoded tax rate.
## Feature Description
Previously, the system used a hardcoded 10% tax rate for all orders. Now, the tax calculation is based on the `tax_rate` field configured for each outlet, allowing for different tax rates across different locations.
## Implementation Details
### 1. Order Processor Changes
The `OrderProcessorImpl` has been updated to:
- Accept an `OutletRepository` dependency
- Fetch outlet information to get the tax rate
- Calculate tax using the outlet's specific tax rate
- Recalculate tax when adding items to existing orders
### 2. Tax Calculation Logic
```go
// Get outlet information for tax rate
outlet, err := p.outletRepo.GetByID(ctx, req.OutletID)
if err != nil {
return nil, fmt.Errorf("outlet not found: %w", err)
}
// Calculate tax using outlet's tax rate
taxAmount := subtotal * outlet.TaxRate
totalAmount := subtotal + taxAmount
```
### 3. Database Schema
The `outlets` table includes:
- `tax_rate`: Decimal field (DECIMAL(5,4)) for tax rate as a decimal (e.g., 0.085 for 8.5%)
- Constraint: `CHECK (tax_rate >= 0 AND tax_rate <= 1)` to ensure valid percentage
### 4. Tax Rate Examples
| Tax Rate (Decimal) | Percentage | Example Calculation |
|-------------------|------------|-------------------|
| 0.0000 | 0% | No tax |
| 0.0500 | 5% | $100 × 0.05 = $5.00 tax |
| 0.0850 | 8.5% | $100 × 0.085 = $8.50 tax |
| 0.1000 | 10% | $100 × 0.10 = $10.00 tax |
| 0.1500 | 15% | $100 × 0.15 = $15.00 tax |
### 5. API Usage
The tax calculation is automatic and transparent to the API consumer. When creating orders or adding items, the system:
1. Fetches the outlet's tax rate
2. Calculates tax based on the current subtotal
3. Updates the order with the correct tax amount
```json
{
"outlet_id": "uuid-of-outlet",
"order_items": [
{
"product_id": "uuid-of-product",
"quantity": 2
}
]
}
```
The response will include the calculated tax amount:
```json
{
"id": "order-uuid",
"outlet_id": "outlet-uuid",
"subtotal": 20.00,
"tax_amount": 1.70, // Based on outlet's tax rate
"total_amount": 21.70
}
```
### 6. Business Scenarios
#### Scenario 1: Different Tax Rates by Location
- **Downtown Location**: 8.5% tax rate
- **Suburban Location**: 6.5% tax rate
- **Airport Location**: 10.0% tax rate
#### Scenario 2: Tax-Exempt Locations
- **Wholesale Outlet**: 0% tax rate
- **Export Zone**: 0% tax rate
#### Scenario 3: Seasonal Tax Changes
- **Holiday Period**: Temporary tax rate adjustments
- **Promotional Period**: Reduced tax rates
### 7. Validation
The system includes several validation checks:
1. **Outlet Existence**: Verifies the outlet exists
2. **Tax Rate Range**: Database constraint ensures 0% ≤ tax rate ≤ 100%
3. **Tax Calculation**: Ensures positive tax amounts
### 8. Error Handling
Common error scenarios:
- `outlet not found`: When an invalid outlet ID is provided
- Database constraint violations for invalid tax rates
### 9. Testing
The implementation includes unit tests to verify:
- Correct tax calculation with different outlet tax rates
- Proper error handling for invalid outlets
- Tax recalculation when adding items to existing orders
### 10. Migration
The feature uses existing database schema from migration `000002_create_outlets_table.up.sql` which includes the `tax_rate` column.
### 11. Configuration
Outlet tax rates can be configured through:
1. **Outlet Creation API**: Set initial tax rate
2. **Outlet Update API**: Modify tax rate for existing outlets
3. **Database Direct Update**: For bulk changes
### 12. Future Enhancements
Potential improvements:
1. **Tax Rate History**: Track tax rate changes over time
2. **Conditional Tax Rates**: Different rates based on order type or customer type
3. **Tax Exemptions**: Support for tax-exempt customers or items
4. **Multi-Tax Support**: Support for multiple tax types (state, local, etc.)
5. **Tax Rate Validation**: Integration with tax authority APIs for rate validation
### 13. Performance Considerations
- Outlet information is fetched once per order creation/modification
- Tax calculation is performed in memory for efficiency
- Consider caching outlet information for high-volume scenarios
### 14. Compliance
- Tax rates should comply with local tax regulations
- Consider implementing tax rate validation against official sources
- Maintain audit trails for tax rate changes

157
PRODUCT_STOCK_MANAGEMENT.md Normal file
View File

@ -0,0 +1,157 @@
# Product Stock Management
This document explains the new product stock management functionality that allows automatic inventory record creation when products are created or updated.
## Features
1. **Automatic Inventory Creation**: When creating a product, you can automatically create inventory records for all outlets in the organization
2. **Initial Stock Setting**: Set initial stock quantity for all outlets
3. **Reorder Level Management**: Set reorder levels for all outlets
4. **Bulk Inventory Updates**: Update reorder levels for all existing inventory records when updating a product
## API Usage
### Creating a Product with Stock Management
```json
POST /api/v1/products
{
"category_id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Premium Coffee",
"description": "High-quality coffee beans",
"price": 15.99,
"cost": 8.50,
"business_type": "restaurant",
"is_active": true,
"variants": [
{
"name": "Large",
"price_modifier": 2.00,
"cost": 1.00
}
],
"initial_stock": 100,
"reorder_level": 20,
"create_inventory": true
}
```
**Parameters:**
- `initial_stock` (optional): Initial stock quantity for all outlets (default: 0)
- `reorder_level` (optional): Reorder level for all outlets (default: 0)
- `create_inventory` (optional): Whether to create inventory records for all outlets (default: false)
### Updating a Product with Stock Management
```json
PUT /api/v1/products/{product_id}
{
"name": "Premium Coffee Updated",
"price": 16.99,
"reorder_level": 25
}
```
**Parameters:**
- `reorder_level` (optional): Updates the reorder level for all existing inventory records
## How It Works
### Product Creation Flow
1. **Validation**: Validates product data and checks for duplicates
2. **Product Creation**: Creates the product in the database
3. **Variant Creation**: Creates product variants if provided
4. **Inventory Creation** (if `create_inventory: true`):
- Fetches all outlets for the organization
- Creates inventory records for each outlet with:
- Initial stock quantity (if provided)
- Reorder level (if provided)
- Uses bulk creation for efficiency
### Product Update Flow
1. **Validation**: Validates update data
2. **Product Update**: Updates the product in the database
3. **Inventory Update** (if `reorder_level` provided):
- Fetches all existing inventory records for the product
- Updates reorder level for each inventory record
## Database Schema
### Products Table
- Standard product fields
- No changes to existing schema
### Inventory Table
- `outlet_id`: Reference to outlet
- `product_id`: Reference to product
- `quantity`: Current stock quantity
- `reorder_level`: Reorder threshold
- `updated_at`: Last update timestamp
## Error Handling
- **No Outlets**: If `create_inventory: true` but no outlets exist, returns an error
- **Duplicate Inventory**: Prevents creating duplicate inventory records for the same product-outlet combination
- **Validation**: Validates stock quantities and reorder levels are non-negative
## Performance Considerations
- **Bulk Operations**: Uses `CreateInBatches` for efficient bulk inventory creation
- **Transactions**: Inventory operations are wrapped in transactions for data consistency
- **Batch Size**: Default batch size of 100 for bulk operations
## Example Response
```json
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"category_id": "550e8400-e29b-41d4-a716-446655440002",
"name": "Premium Coffee",
"description": "High-quality coffee beans",
"price": 15.99,
"cost": 8.50,
"business_type": "restaurant",
"is_active": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"category": {
"id": "550e8400-e29b-41d4-a716-446655440002",
"name": "Beverages"
},
"variants": [
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"name": "Large",
"price_modifier": 2.00,
"cost": 1.00
}
],
"inventory": [
{
"id": "550e8400-e29b-41d4-a716-446655440004",
"outlet_id": "550e8400-e29b-41d4-a716-446655440005",
"quantity": 100,
"reorder_level": 20
},
{
"id": "550e8400-e29b-41d4-a716-446655440006",
"outlet_id": "550e8400-e29b-41d4-a716-446655440007",
"quantity": 100,
"reorder_level": 20
}
]
}
```
## Migration Notes
This feature requires the existing database schema with:
- `products` table
- `inventory` table
- `outlets` table
- Proper foreign key relationships
No additional migrations are required as the feature uses existing tables.

View File

@ -0,0 +1,127 @@
# Product Variant Price Modifier Implementation
## Overview
This document describes the implementation of price modifier functionality for product variants in the order processing system.
## Feature Description
When a product variant is specified in an order item, the system now automatically applies the variant's price modifier to the base product price. This allows for flexible pricing based on product variations (e.g., size upgrades, add-ons, etc.).
## Implementation Details
### 1. Order Processor Changes
The `OrderProcessorImpl` has been updated to:
- Accept a `ProductVariantRepository` dependency
- Fetch product variant information when `ProductVariantID` is provided
- Apply the price modifier to the base product price
- Use variant-specific cost if available
### 2. Price Calculation Logic
```go
// Base price from product
unitPrice := product.Price
unitCost := product.Cost
// Apply variant price modifier if specified
if itemReq.ProductVariantID != nil {
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
if err != nil {
return nil, fmt.Errorf("product variant not found: %w", err)
}
// Verify variant belongs to the product
if variant.ProductID != itemReq.ProductID {
return nil, fmt.Errorf("product variant does not belong to the specified product")
}
// Apply price modifier
unitPrice += variant.PriceModifier
// Use variant cost if available, otherwise use product cost
if variant.Cost > 0 {
unitCost = variant.Cost
}
}
```
### 3. Database Schema
The `product_variants` table includes:
- `price_modifier`: Decimal field for price adjustments (+/- values)
- `cost`: Optional variant-specific cost
- `product_id`: Foreign key to products table
### 4. API Usage
When creating orders or adding items to existing orders, you can specify a product variant:
```json
{
"order_items": [
{
"product_id": "uuid-of-product",
"product_variant_id": "uuid-of-variant",
"quantity": 2,
"notes": "Extra large size"
}
]
}
```
### 5. Example Scenarios
#### Scenario 1: Size Upgrade
- Base product: Coffee ($3.00)
- Variant: Large (+$1.00 modifier)
- Final price: $4.00
#### Scenario 2: Add-on
- Base product: Pizza ($12.00)
- Variant: Extra cheese (+$2.50 modifier)
- Final price: $14.50
#### Scenario 3: Discount
- Base product: Sandwich ($8.00)
- Variant: Student discount (-$1.00 modifier)
- Final price: $7.00
## Validation
The system includes several validation checks:
1. **Variant Existence**: Verifies the product variant exists
2. **Product Association**: Ensures the variant belongs to the specified product
3. **Price Integrity**: Maintains positive pricing (base price + modifier must be >= 0)
## Error Handling
Common error scenarios:
- `product variant not found`: When an invalid variant ID is provided
- `product variant does not belong to the specified product`: When variant-product mismatch occurs
## Testing
The implementation includes unit tests to verify:
- Correct price calculation with variants
- Proper error handling for invalid variants
- Cost calculation using variant-specific costs
## Migration
The feature uses existing database schema from migration `000013_add_cost_to_product_variants.up.sql` which adds the `cost` column to the `product_variants` table.
## Future Enhancements
Potential improvements:
1. **Percentage-based modifiers**: Support for percentage-based price adjustments
2. **Conditional modifiers**: Modifiers that apply based on order context
3. **Bulk variant pricing**: Tools for managing variant pricing across products
4. **Pricing history**: Track price modifier changes over time

View File

@ -0,0 +1,241 @@
# Profit/Loss Analytics API
This document describes the Profit/Loss Analytics API that provides comprehensive financial analysis for the POS system, including revenue, costs, and profitability metrics.
## Overview
The Profit/Loss Analytics API allows you to:
- Analyze profit and loss performance over time periods
- Track gross profit and net profit margins
- View product-wise profitability
- Monitor cost vs revenue trends
- Calculate profitability ratios
## Authentication
All analytics endpoints require authentication and admin/manager permissions.
## Endpoints
### Get Profit/Loss Analytics
**Endpoint:** `GET /api/v1/analytics/profit-loss`
**Description:** Retrieves comprehensive profit and loss analytics data including summary metrics, time-series data, and product profitability analysis.
**Query Parameters:**
- `outlet_id` (UUID, optional) - Filter by specific outlet
- `date_from` (string, required) - Start date in DD-MM-YYYY format
- `date_to` (string, required) - End date in DD-MM-YYYY format
- `group_by` (string, optional) - Time grouping: `hour`, `day`, `week`, `month` (default: `day`)
**Example Request:**
```bash
curl -X GET "http://localhost:8080/api/v1/analytics/profit-loss?date_from=01-12-2023&date_to=31-12-2023&group_by=day" \
-H "Authorization: Bearer <token>" \
-H "Organization-ID: <org-id>"
```
**Example Response:**
```json
{
"success": true,
"data": {
"organization_id": "123e4567-e89b-12d3-a456-426614174000",
"outlet_id": "123e4567-e89b-12d3-a456-426614174001",
"date_from": "2023-12-01T00:00:00Z",
"date_to": "2023-12-31T23:59:59Z",
"group_by": "day",
"summary": {
"total_revenue": 125000.00,
"total_cost": 75000.00,
"gross_profit": 50000.00,
"gross_profit_margin": 40.00,
"total_tax": 12500.00,
"total_discount": 2500.00,
"net_profit": 35000.00,
"net_profit_margin": 28.00,
"total_orders": 1250,
"average_profit": 28.00,
"profitability_ratio": 66.67
},
"data": [
{
"date": "2023-12-01T00:00:00Z",
"revenue": 4032.26,
"cost": 2419.35,
"gross_profit": 1612.91,
"gross_profit_margin": 40.00,
"tax": 403.23,
"discount": 80.65,
"net_profit": 1129.03,
"net_profit_margin": 28.00,
"orders": 40
},
{
"date": "2023-12-02T00:00:00Z",
"revenue": 3750.00,
"cost": 2250.00,
"gross_profit": 1500.00,
"gross_profit_margin": 40.00,
"tax": 375.00,
"discount": 75.00,
"net_profit": 1050.00,
"net_profit_margin": 28.00,
"orders": 35
}
],
"product_data": [
{
"product_id": "123e4567-e89b-12d3-a456-426614174002",
"product_name": "Premium Burger",
"category_id": "123e4567-e89b-12d3-a456-426614174003",
"category_name": "Main Course",
"quantity_sold": 150,
"revenue": 2250.00,
"cost": 900.00,
"gross_profit": 1350.00,
"gross_profit_margin": 60.00,
"average_price": 15.00,
"average_cost": 6.00,
"profit_per_unit": 9.00
},
{
"product_id": "123e4567-e89b-12d3-a456-426614174004",
"product_name": "Caesar Salad",
"category_id": "123e4567-e89b-12d3-a456-426614174005",
"category_name": "Salads",
"quantity_sold": 80,
"revenue": 960.00,
"cost": 384.00,
"gross_profit": 576.00,
"gross_profit_margin": 60.00,
"average_price": 12.00,
"average_cost": 4.80,
"profit_per_unit": 7.20
}
]
}
}
```
## Response Structure
### Summary Object
- `total_revenue` - Total revenue for the period
- `total_cost` - Total cost of goods sold
- `gross_profit` - Revenue minus cost (total_revenue - total_cost)
- `gross_profit_margin` - Gross profit as percentage of revenue
- `total_tax` - Total tax collected
- `total_discount` - Total discounts given
- `net_profit` - Profit after taxes and discounts
- `net_profit_margin` - Net profit as percentage of revenue
- `total_orders` - Number of completed orders
- `average_profit` - Average profit per order
- `profitability_ratio` - Gross profit as percentage of total cost
### Time Series Data
The `data` array contains profit/loss metrics grouped by the specified time period:
- `date` - Date/time for the data point
- `revenue` - Revenue for the period
- `cost` - Cost for the period
- `gross_profit` - Gross profit for the period
- `gross_profit_margin` - Gross profit margin percentage
- `tax` - Tax amount for the period
- `discount` - Discount amount for the period
- `net_profit` - Net profit for the period
- `net_profit_margin` - Net profit margin percentage
- `orders` - Number of orders in the period
### Product Profitability Data
The `product_data` array shows the top 20 most profitable products:
- `product_id` - Unique product identifier
- `product_name` - Product name
- `category_id` - Product category identifier
- `category_name` - Category name
- `quantity_sold` - Total units sold
- `revenue` - Total revenue from the product
- `cost` - Total cost for the product
- `gross_profit` - Total gross profit
- `gross_profit_margin` - Profit margin percentage
- `average_price` - Average selling price per unit
- `average_cost` - Average cost per unit
- `profit_per_unit` - Average profit per unit
## Key Metrics Explained
### Gross Profit Margin
Calculated as: `(Revenue - Cost) / Revenue × 100`
Shows the percentage of revenue retained after direct costs.
### Net Profit Margin
Calculated as: `(Revenue - Cost - Discount) / Revenue × 100`
Shows the percentage of revenue retained after all direct costs and discounts.
### Profitability Ratio
Calculated as: `Gross Profit / Total Cost × 100`
Shows the return on investment for costs incurred.
## Use Cases
1. **Financial Performance Analysis** - Track overall profitability trends
2. **Product Performance** - Identify most and least profitable products
3. **Cost Management** - Monitor cost ratios and margins
4. **Pricing Strategy** - Analyze impact of pricing on profitability
5. **Inventory Decisions** - Focus on high-margin products
6. **Business Intelligence** - Make data-driven financial decisions
## Error Responses
The API returns standard error responses with appropriate HTTP status codes:
**400 Bad Request:**
```json
{
"success": false,
"errors": [
{
"code": "invalid_request",
"entity": "AnalyticsHandler::GetProfitLossAnalytics",
"message": "date_from is required"
}
]
}
```
**401 Unauthorized:**
```json
{
"success": false,
"errors": [
{
"code": "unauthorized",
"entity": "AuthMiddleware",
"message": "Invalid or missing authentication token"
}
]
}
```
**403 Forbidden:**
```json
{
"success": false,
"errors": [
{
"code": "forbidden",
"entity": "AuthMiddleware",
"message": "Admin or manager role required"
}
]
}
```
## Notes
- Only completed and paid orders are included in profit/loss calculations
- Voided and refunded orders are excluded from the analysis
- Product profitability is sorted by gross profit in descending order
- Time series data is automatically filled for periods with no data (showing zero values)
- All monetary values are in the organization's base currency
- Margins and ratios are calculated as percentages with 2 decimal precision

250
README.md
View File

@ -57,8 +57,254 @@ $ ./bin/http-server --env-path ./config/env/.env
``` ```
## API Docs ## API Docs
* [enaklo-pos Backend](https://enaklo-pos-be.app-dev.altru.id/docs/index.html#/) * [apskel-pos Backend](https://apskel-pos-be.app-dev.altru.id/docs/index.html#/)
## License ## License
This project is licensed under the [MIT License](https://github.com/pvarentsov/enaklo-pos-be/blob/main/LICENSE). This project is licensed under the [MIT License](https://github.com/pvarentsov/apskel-pos-be/blob/main/LICENSE).
# Apskel POS Backend
A SaaS Point of Sale (POS) Restaurant System backend built with clean architecture principles in Go.
## Architecture Overview
This application follows a clean architecture pattern with clear separation of concerns:
```
Handler → Service → Processor → Repository
```
### Layers
1. **Contract Package** (`internal/contract/`)
- Request/Response DTOs for API communication
- Contains JSON tags for serialization
- Input validation tags
2. **Handler Layer** (`internal/handler/`)
- HTTP request/response handling
- Request validation using go-playground/validator
- Route definitions and middleware
- Transforms contracts to/from services
3. **Service Layer** (`internal/service/`)
- Business logic orchestration
- Calls processors and transformers
- Coordinates between different business operations
4. **Processor Layer** (`internal/processor/`)
- Complex business operations
- Cross-repository transactions
- Business rule enforcement
- Handles operations like order creation with inventory updates
5. **Repository Layer** (`internal/repository/`)
- Data access layer
- Individual repository per entity
- Database-specific operations
- Uses entities for database models
6. **Supporting Packages**:
- **Models** (`internal/models/`) - Pure business logic models (no database dependencies)
- **Entities** (`internal/entities/`) - Database models with GORM tags
- **Constants** (`internal/constants/`) - Type-safe enums and business constants
- **Transformer** (`internal/transformer/`) - Contract ↔ Model conversions
- **Mappers** (`internal/mappers/`) - Model ↔ Entity conversions
## Key Features
- **Clean Architecture**: Strict separation between business logic and infrastructure
- **Type Safety**: Constants package with validation helpers
- **Validation**: Comprehensive request validation using go-playground/validator
- **Error Handling**: Structured error responses with proper HTTP status codes
- **Database Independence**: Business logic never depends on database implementation
- **Testability**: Each layer can be tested independently
## API Endpoints
### Health Check
- `GET /api/v1/health` - Health check endpoint
### Organizations
- `POST /api/v1/organizations` - Create organization
- `GET /api/v1/organizations` - List organizations
- `GET /api/v1/organizations/{id}` - Get organization by ID
- `PUT /api/v1/organizations/{id}` - Update organization
- `DELETE /api/v1/organizations/{id}` - Delete organization
### Users
- `POST /api/v1/users` - Create user
- `GET /api/v1/users` - List users
- `GET /api/v1/users/{id}` - Get user by ID
- `PUT /api/v1/users/{id}` - Update user
- `DELETE /api/v1/users/{id}` - Delete user
- `PUT /api/v1/users/{id}/password` - Change password
- `PUT /api/v1/users/{id}/activate` - Activate user
- `PUT /api/v1/users/{id}/deactivate` - Deactivate user
### Orders
- `POST /api/v1/orders` - Create order with items
- `GET /api/v1/orders` - List orders
- `GET /api/v1/orders/{id}` - Get order by ID
- `GET /api/v1/orders/{id}?include_items=true` - Get order with items
- `PUT /api/v1/orders/{id}` - Update order
- `PUT /api/v1/orders/{id}/cancel` - Cancel order
- `PUT /api/v1/orders/{id}/complete` - Complete order
- `POST /api/v1/orders/{id}/items` - Add item to order
### Order Items
- `PUT /api/v1/order-items/{id}` - Update order item
- `DELETE /api/v1/order-items/{id}` - Remove order item
## Installation
1. **Clone the repository**
```bash
git clone <repository-url>
cd apskel-pos-backend
```
2. **Install dependencies**
```bash
go mod tidy
```
3. **Set up database**
```bash
# Set your PostgreSQL database URL
export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
```
4. **Run migrations**
```bash
make migration-up
```
## Usage
### Development
```bash
# Start the server
go run cmd/server/main.go -port 8080 -db-url "postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
# Or using environment variable
export DATABASE_URL="postgres://username:password@localhost:5432/apskel_pos?sslmode=disable"
go run cmd/server/main.go -port 8080
```
### Using Make Commands
```bash
# Run the application
make start
# Format code
make fmt
# Run tests
make test
# Build for production
make build-http
# Docker operations
make docker-up
make docker-down
# Database migrations
make migration-create name=create_users_table
make migration-up
make migration-down
```
## Example API Usage
### Create Organization
```bash
curl -X POST http://localhost:8080/api/v1/organizations \
-H "Content-Type: application/json" \
-d '{
"name": "My Restaurant",
"plan_type": "premium"
}'
```
### Create User
```bash
curl -X POST http://localhost:8080/api/v1/users \
-H "Content-Type: application/json" \
-d '{
"organization_id": "uuid-here",
"username": "john_doe",
"email": "john@example.com",
"password": "password123",
"full_name": "John Doe",
"role": "manager"
}'
```
### Create Order with Items
```bash
curl -X POST http://localhost:8080/api/v1/orders \
-H "Content-Type: application/json" \
-d '{
"outlet_id": "uuid-here",
"user_id": "uuid-here",
"table_number": "A1",
"order_type": "dine_in",
"notes": "No onions",
"order_items": [
{
"product_id": "uuid-here",
"quantity": 2,
"unit_price": 15.99
}
]
}'
```
## Project Structure
```
apskel-pos-backend/
├── cmd/
│ └── server/ # Application entry point
├── internal/
│ ├── app/ # Application wiring and dependency injection
│ ├── contract/ # API contracts (request/response DTOs)
│ ├── handler/ # HTTP handlers and routes
│ ├── service/ # Business logic orchestration
│ ├── processor/ # Complex business operations
│ ├── repository/ # Data access layer
│ ├── models/ # Pure business models
│ ├── entities/ # Database entities (GORM models)
│ ├── constants/ # Business constants and enums
│ ├── transformer/ # Contract ↔ Model transformations
│ └── mappers/ # Model ↔ Entity transformations
├── migrations/ # Database migrations
├── Makefile # Build and development commands
├── go.mod # Go module definition
└── README.md # This file
```
## Dependencies
- **[Gorilla Mux](https://github.com/gorilla/mux)** - HTTP router and URL matcher
- **[GORM](https://gorm.io/)** - ORM for database operations
- **[PostgreSQL Driver](https://github.com/lib/pq)** - PostgreSQL database driver
- **[Validator](https://github.com/go-playground/validator)** - Struct validation
- **[UUID](https://github.com/google/uuid)** - UUID generation and parsing
## Contributing
1. Fork the repository
2. Create a feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.

330
TABLE_MANAGEMENT_API.md Normal file
View File

@ -0,0 +1,330 @@
# Table Management API
This document describes the Table Management API endpoints for managing restaurant tables in the POS system.
## Overview
The Table Management API allows you to:
- Create, read, update, and delete tables
- Manage table status (available, occupied, reserved, cleaning, maintenance)
- Occupy and release tables with orders
- Track table positions and capacity
- Get available and occupied tables for specific outlets
## Table Entity
A table has the following properties:
```json
{
"id": "uuid",
"organization_id": "uuid",
"outlet_id": "uuid",
"table_name": "string",
"start_time": "datetime (optional)",
"status": "available|occupied|reserved|cleaning|maintenance",
"order_id": "uuid (optional)",
"payment_amount": "decimal",
"position_x": "decimal",
"position_y": "decimal",
"capacity": "integer (1-20)",
"is_active": "boolean",
"metadata": "object",
"created_at": "datetime",
"updated_at": "datetime",
"order": "OrderResponse (optional)"
}
```
## API Endpoints
### 1. Create Table
**POST** `/api/v1/tables`
Create a new table for an outlet.
**Request Body:**
```json
{
"outlet_id": "uuid",
"table_name": "string",
"position_x": "decimal",
"position_y": "decimal",
"capacity": "integer (1-20)",
"metadata": "object (optional)"
}
```
**Response:** `201 Created`
```json
{
"id": "uuid",
"organization_id": "uuid",
"outlet_id": "uuid",
"table_name": "string",
"status": "available",
"position_x": "decimal",
"position_y": "decimal",
"capacity": "integer",
"is_active": true,
"metadata": "object",
"created_at": "datetime",
"updated_at": "datetime"
}
```
### 2. Get Table by ID
**GET** `/api/v1/tables/{id}`
Get table details by ID.
**Response:** `200 OK`
```json
{
"id": "uuid",
"organization_id": "uuid",
"outlet_id": "uuid",
"table_name": "string",
"start_time": "datetime (optional)",
"status": "string",
"order_id": "uuid (optional)",
"payment_amount": "decimal",
"position_x": "decimal",
"position_y": "decimal",
"capacity": "integer",
"is_active": "boolean",
"metadata": "object",
"created_at": "datetime",
"updated_at": "datetime",
"order": "OrderResponse (optional)"
}
```
### 3. Update Table
**PUT** `/api/v1/tables/{id}`
Update table details.
**Request Body:**
```json
{
"table_name": "string (optional)",
"status": "available|occupied|reserved|cleaning|maintenance (optional)",
"position_x": "decimal (optional)",
"position_y": "decimal (optional)",
"capacity": "integer (1-20) (optional)",
"is_active": "boolean (optional)",
"metadata": "object (optional)"
}
```
**Response:** `200 OK` - Updated table object
### 4. Delete Table
**DELETE** `/api/v1/tables/{id}`
Delete a table. Cannot delete occupied tables.
**Response:** `204 No Content`
### 5. List Tables
**GET** `/api/v1/tables`
Get paginated list of tables with optional filters.
**Query Parameters:**
- `organization_id` (optional): Filter by organization
- `outlet_id` (optional): Filter by outlet
- `status` (optional): Filter by status
- `is_active` (optional): Filter by active status
- `search` (optional): Search in table names
- `page` (default: 1): Page number
- `limit` (default: 10, max: 100): Page size
**Response:** `200 OK`
```json
{
"tables": [
{
"id": "uuid",
"table_name": "string",
"status": "string",
"capacity": "integer",
"is_active": "boolean",
"created_at": "datetime",
"updated_at": "datetime"
}
],
"total_count": "integer",
"page": "integer",
"limit": "integer",
"total_pages": "integer"
}
```
### 6. Occupy Table
**POST** `/api/v1/tables/{id}/occupy`
Occupy a table with an order.
**Request Body:**
```json
{
"order_id": "uuid",
"start_time": "datetime"
}
```
**Response:** `200 OK` - Updated table object with order information
### 7. Release Table
**POST** `/api/v1/tables/{id}/release`
Release a table and record payment amount.
**Request Body:**
```json
{
"payment_amount": "decimal"
}
```
**Response:** `200 OK` - Updated table object
### 8. Get Available Tables
**GET** `/api/v1/outlets/{outlet_id}/tables/available`
Get list of available tables for a specific outlet.
**Response:** `200 OK`
```json
[
{
"id": "uuid",
"table_name": "string",
"status": "available",
"capacity": "integer",
"position_x": "decimal",
"position_y": "decimal"
}
]
```
### 9. Get Occupied Tables
**GET** `/api/v1/outlets/{outlet_id}/tables/occupied`
Get list of occupied tables for a specific outlet.
**Response:** `200 OK`
```json
[
{
"id": "uuid",
"table_name": "string",
"status": "occupied",
"start_time": "datetime",
"order_id": "uuid",
"capacity": "integer",
"position_x": "decimal",
"position_y": "decimal",
"order": "OrderResponse"
}
]
```
## Table Statuses
- **available**: Table is free and ready for use
- **occupied**: Table is currently in use with an order
- **reserved**: Table is reserved for future use
- **cleaning**: Table is being cleaned
- **maintenance**: Table is under maintenance
## Business Rules
1. **Table Creation**: Tables must have unique names within an outlet
2. **Table Occupation**: Only available or cleaning tables can be occupied
3. **Table Release**: Only occupied tables can be released
4. **Table Deletion**: Occupied tables cannot be deleted
5. **Capacity**: Table capacity must be between 1 and 20
6. **Position**: Tables have X and Y coordinates for layout positioning
## Error Responses
**400 Bad Request:**
```json
{
"error": "Error description",
"message": "Detailed error message"
}
```
**404 Not Found:**
```json
{
"error": "Table not found",
"message": "Table with specified ID does not exist"
}
```
**500 Internal Server Error:**
```json
{
"error": "Failed to create table",
"message": "Database error or other internal error"
}
```
## Authentication
All endpoints require authentication via JWT token in the Authorization header:
```
Authorization: Bearer <jwt_token>
```
## Authorization
All table management endpoints require admin or manager role permissions.
## Example Usage
### Creating a Table
```bash
curl -X POST http://localhost:8080/api/v1/tables \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"outlet_id": "123e4567-e89b-12d3-a456-426614174000",
"table_name": "Table 1",
"position_x": 100.0,
"position_y": 200.0,
"capacity": 4
}'
```
### Occupying a Table
```bash
curl -X POST http://localhost:8080/api/v1/tables/123e4567-e89b-12d3-a456-426614174000/occupy \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"order_id": "123e4567-e89b-12d3-a456-426614174001",
"start_time": "2024-01-15T10:30:00Z"
}'
```
### Getting Available Tables
```bash
curl -X GET http://localhost:8080/api/v1/outlets/123e4567-e89b-12d3-a456-426614174000/tables/available \
-H "Authorization: Bearer <token>"
```

30
cmd/server/main.go Normal file
View File

@ -0,0 +1,30 @@
package main
import (
"apskel-pos-be/config"
"apskel-pos-be/internal/app"
"apskel-pos-be/internal/db"
"apskel-pos-be/internal/logger"
"log"
)
func main() {
cfg := config.LoadConfig()
logger.Setup(cfg.LogLevel(), cfg.LogFormat())
db, err := db.NewPostgres(cfg.Database)
if err != nil {
log.Fatal(err)
}
logger.NonContext.Info("helloworld")
application := app.NewApp(db)
if err := application.Initialize(cfg); err != nil {
log.Fatalf("Failed to initialize application: %v", err)
}
if err := application.Start(cfg.Port()); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

View File

@ -1,9 +0,0 @@
package config
type Brevo struct {
APIKey string `mapstructure:"api_key"`
}
func (b *Brevo) GetApiKey() string {
return b.APIKey
}

View File

@ -10,7 +10,7 @@ import (
) )
const ( const (
YAML_PATH = "infra/enaklopos.%s" YAML_PATH = "infra/%s"
ENV_MODE = "ENV_MODE" ENV_MODE = "ENV_MODE"
DEFAULT_ENV_MODE = "development" DEFAULT_ENV_MODE = "development"
) )
@ -27,15 +27,8 @@ type Config struct {
Server Server `mapstructure:"server"` Server Server `mapstructure:"server"`
Database Database `mapstructure:"postgresql"` Database Database `mapstructure:"postgresql"`
Jwt Jwt `mapstructure:"jwt"` Jwt Jwt `mapstructure:"jwt"`
OSSConfig OSSConfig `mapstructure:"oss"` Log Log `mapstructure:"log"`
Midtrans Midtrans `mapstructure:"midtrans"` S3Config S3Config `mapstructure:"s3"`
Brevo Brevo `mapstructure:"brevo"`
Email Email `mapstructure:"email"`
Withdraw Withdraw `mapstructure:"withdrawal"`
Discovery Discovery `mapstructure:"discovery"`
Order Order `mapstructure:"order"`
FeatureToggle FeatureToggle `mapstructure:"feature_toggle"`
LinkQu LinkQu `mapstructure:"linkqu"`
} }
var ( var (
@ -46,7 +39,7 @@ var (
func LoadConfig() *Config { func LoadConfig() *Config {
envMode := os.Getenv(ENV_MODE) envMode := os.Getenv(ENV_MODE)
if _, ok := validEnvMode[envMode]; !ok { if _, ok := validEnvMode[envMode]; !ok {
envMode = DEFAULT_ENV_MODE // default env mode envMode = DEFAULT_ENV_MODE
} }
cfgFilePath := fmt.Sprintf(YAML_PATH, envMode) cfgFilePath := fmt.Sprintf(YAML_PATH, envMode)
@ -72,19 +65,17 @@ func (c *Config) Auth() *AuthConfig {
return &AuthConfig{ return &AuthConfig{
jwtTokenSecret: c.Jwt.Token.Secret, jwtTokenSecret: c.Jwt.Token.Secret,
jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL, jwtTokenExpiresTTL: c.Jwt.Token.ExpiresTTL,
jwtOrderSecret: c.Jwt.TokenOrder.Secret,
jwtOrderExpiresTTL: c.Jwt.TokenOrder.ExpiresTTL,
jwtSecretResetPassword: JWT{
secret: c.Jwt.TokenResetPassword.Secret,
expireTTL: c.Jwt.TokenResetPassword.ExpiresTTL,
},
jwtWithdraw: JWT{
secret: c.Jwt.TokenWithdraw.Secret,
expireTTL: c.Jwt.TokenWithdraw.ExpiresTTL,
},
jwtCustomer: JWT{
secret: c.Jwt.TokenCustomer.Secret,
expireTTL: c.Jwt.TokenCustomer.ExpiresTTL,
},
} }
} }
func (c *Config) LogLevel() string {
return c.Log.LogLevel
}
func (c *Config) Port() string {
return c.Server.Port
}
func (c *Config) LogFormat() string {
return c.Log.LogFormat
}

View File

@ -5,11 +5,6 @@ import "time"
type AuthConfig struct { type AuthConfig struct {
jwtTokenExpiresTTL int jwtTokenExpiresTTL int
jwtTokenSecret string jwtTokenSecret string
jwtOrderSecret string
jwtOrderExpiresTTL int
jwtSecretResetPassword JWT
jwtWithdraw JWT
jwtCustomer JWT
} }
type JWT struct { type JWT struct {
@ -21,38 +16,7 @@ func (c *AuthConfig) AccessTokenSecret() string {
return c.jwtTokenSecret return c.jwtTokenSecret
} }
func (c *AuthConfig) AccessTokenOrderSecret() string {
return c.jwtOrderSecret
}
func (c *AuthConfig) AccessTokenCustomerSecret() string {
return c.jwtCustomer.secret
}
func (c *AuthConfig) AccessTokenWithdrawSecret() string {
return c.jwtWithdraw.secret
}
func (c *AuthConfig) AccessTokenWithdrawExpire() time.Time {
duration := time.Duration(c.jwtWithdraw.expireTTL)
return time.Now().UTC().Add(time.Minute * duration)
}
func (c *AuthConfig) AccessTokenOrderExpiresDate() time.Time {
duration := time.Duration(c.jwtOrderExpiresTTL)
return time.Now().UTC().Add(time.Minute * duration)
}
func (c *AuthConfig) AccessTokenExpiresDate() time.Time { func (c *AuthConfig) AccessTokenExpiresDate() time.Time {
duration := time.Duration(c.jwtTokenExpiresTTL) duration := time.Duration(c.jwtTokenExpiresTTL)
return time.Now().UTC().Add(time.Minute * duration) return time.Now().UTC().Add(time.Minute * duration)
} }
func (c *AuthConfig) AccessTokenResetPasswordSecret() string {
return c.jwtSecretResetPassword.secret
}
func (c *AuthConfig) AccessTokenResetPasswordExpire() time.Time {
duration := time.Duration(c.jwtSecretResetPassword.expireTTL)
return time.Now().UTC().Add(time.Minute * duration)
}

View File

@ -1,17 +0,0 @@
package config
type Discovery struct {
ExploreDestinations []ExploreDestination `mapstructure:"explore_destinations"`
ExploreRegions []ExploreRegion `mapstructure:"explore_regions"`
}
type ExploreDestinations []ExploreDestination
type ExploreDestination struct {
Name string `mapstructure:"name"`
ImageURL string `mapstructure:"image_url"`
}
type ExploreRegion struct {
Name string `mapstructure:"name"`
}

View File

@ -1,34 +0,0 @@
package config
type Email struct {
Sender string `mapstructure:"sender"`
SenderCustomer string `mapstructure:"sender_customer"`
CustomReceiver string `mapstructure:"custom_receiver"`
ResetPassword EmailConfig `mapstructure:"reset_password"`
}
type EmailConfig struct {
Subject string `mapstructure:"subject"`
OpeningWord string `mapstructure:"opening_word"`
Link string `mapstructure:"link"`
Notes string `mapstructure:"note"`
ClosingWord string `mapstructure:"closing_word"`
TemplateName string `mapstructure:"template_name"`
TemplatePath string `mapstructure:"template_path"`
TemplatePathCustomer string `mapstructure:"template_path_customer"`
}
type EmailMemberRequestActionConfig struct {
TemplateName string `mapstructure:"template_name"`
TemplatePath string `mapstructure:"template_path"`
Subject string `mapstructure:"subject"`
Content string `mapstructure:"content"`
}
func (e *Email) GetSender() string {
return e.Sender
}
func (e *Email) GetCustomReceiver() string {
return e.CustomReceiver
}

View File

@ -2,10 +2,6 @@ package config
type Jwt struct { type Jwt struct {
Token Token `mapstructure:"token"` Token Token `mapstructure:"token"`
TokenOrder Token `mapstructure:"token-order"`
TokenResetPassword Token `mapstructure:"token-reset-password"`
TokenWithdraw Token `mapstructure:"token-withdraw"`
TokenCustomer Token `mapstructure:"token-customer"`
} }
type Token struct { type Token struct {

View File

@ -1,49 +0,0 @@
package config
type LinkQu struct {
BaseURL string `mapstructure:"base_url"`
ClientID string `mapstructure:"client_id"`
ClientSecret string `mapstructure:"client_secret"`
SignatureKey string `mapstructure:"signature_key"`
Username string `mapstructure:"username"`
PIN string `mapstructure:"pin"`
CallbackURL string `mapstructure:"callback_url"`
}
type LinkQuConfig interface {
LinkQuBaseURL() string
LinkQuClientID() string
LinkQuClientSecret() string
LinkQuSignatureKey() string
LinkQuUsername() string
LinkQuPIN() string
LinkQuCallbackURL() string
}
func (c *LinkQu) LinkQuBaseURL() string {
return c.BaseURL
}
func (c *LinkQu) LinkQuClientID() string {
return c.ClientID
}
func (c *LinkQu) LinkQuClientSecret() string {
return c.ClientSecret
}
func (c *LinkQu) LinkQuSignatureKey() string {
return c.SignatureKey
}
func (c *LinkQu) LinkQuUsername() string {
return c.Username
}
func (c *LinkQu) LinkQuPIN() string {
return c.PIN
}
func (c *LinkQu) LinkQuCallbackURL() string {
return c.CallbackURL
}

6
config/log.go Normal file
View File

@ -0,0 +1,6 @@
package config
type Log struct {
LogFormat string `mapstructure:"log_format"`
LogLevel string `mapstructure:"log_level"`
}

View File

@ -1,9 +0,0 @@
package config
type FeatureToggle struct {
LoggerEnabled bool `mapstructure:"logger_enabled"`
}
func (f *FeatureToggle) IsLoggerEnabled() bool {
return f.LoggerEnabled
}

View File

@ -1,25 +0,0 @@
package config
type Midtrans struct {
Serverkey string `mapstructure:"server_key"`
Clientkey string `mapstructure:"client_key"`
Env int `mapstructure:"env"`
}
type MidtransConfig interface {
MidtransServerKey() string
MidtransClientKey() string
MidtranEnvType() int
}
func (c *Midtrans) MidtransServerKey() string {
return c.Serverkey
}
func (c *Midtrans) MidtransClientKey() string {
return c.Clientkey
}
func (c *Midtrans) MidtranEnvType() int {
return c.Env
}

View File

@ -1,12 +0,0 @@
package config
type Order struct {
Fee float64 `mapstructure:"fee"`
}
func (w *Order) GetOrderFee(source string) float64 {
if source == "POS" {
return 0
}
return w.Fee
}

View File

@ -1,6 +1,6 @@
package config package config
type OSSConfig struct { type S3Config struct {
AccessKeyID string `mapstructure:"access_key_id"` AccessKeyID string `mapstructure:"access_key_id"`
AccessKeySecret string `mapstructure:"access_key_secret"` AccessKeySecret string `mapstructure:"access_key_secret"`
Endpoint string `mapstructure:"endpoint"` Endpoint string `mapstructure:"endpoint"`
@ -10,30 +10,30 @@ type OSSConfig struct {
HostURL string `mapstructure:"host_url"` HostURL string `mapstructure:"host_url"`
} }
func (c OSSConfig) GetAccessKeyID() string { func (c S3Config) GetAccessKeyID() string {
return c.AccessKeyID return c.AccessKeyID
} }
func (c OSSConfig) GetAccessKeySecret() string { func (c S3Config) GetAccessKeySecret() string {
return c.AccessKeySecret return c.AccessKeySecret
} }
func (c OSSConfig) GetEndpoint() string { func (c S3Config) GetEndpoint() string {
return c.Endpoint return c.Endpoint
} }
func (c OSSConfig) GetBucketName() string { func (c S3Config) GetBucketName() string {
return c.BucketName return c.BucketName
} }
func (c OSSConfig) GetLogLevel() string { func (c S3Config) GetLogLevel() string {
return c.LogLevel return c.LogLevel
} }
func (c OSSConfig) GetHostURL() string { func (c S3Config) GetHostURL() string {
return c.HostURL return c.HostURL
} }
func (c OSSConfig) GetPhotoFolder() string { func (c S3Config) GetPhotoFolder() string {
return c.PhotoFolder return c.PhotoFolder
} }

View File

@ -1 +0,0 @@
package config

View File

@ -1,9 +0,0 @@
package config
type Withdraw struct {
PlatformFee int64 `mapstructure:"platform_fee"`
}
func (w *Withdraw) GetPlatformFee() int64 {
return w.PlatformFee
}

211
docker-build.sh Executable file
View File

@ -0,0 +1,211 @@
#!/bin/bash
# Docker build script for apskel-pos-backend
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Help function
show_help() {
echo "Usage: $0 [OPTION]"
echo "Build and manage Docker containers for apskel-pos-backend"
echo ""
echo "Options:"
echo " build Build the Docker image"
echo " run Run the production container"
echo " dev Run development environment with live reload"
echo " migrate Run database migrations"
echo " stop Stop all containers"
echo " clean Remove containers and images"
echo " logs Show container logs"
echo " help Show this help message"
echo ""
}
# Build Docker image
build_image() {
log_info "Building apskel-pos-backend Docker image..."
# Check if Go build works locally first (optional quick test)
if command -v go &> /dev/null; then
log_info "Testing Go build locally first..."
if go build -o /tmp/test-build cmd/server/main.go 2>/dev/null; then
log_success "Local Go build test passed"
rm -f /tmp/test-build
else
log_warning "Local Go build test failed, but continuing with Docker build..."
fi
fi
# Build the image with production target
docker build \
--target production \
-t apskel-pos-backend:latest \
-t apskel-pos-backend:$(date +%Y%m%d-%H%M%S) \
.
if [ $? -eq 0 ]; then
log_success "Docker image built successfully!"
else
log_error "Failed to build Docker image"
log_info "Make sure you're using Go 1.21+ and all dependencies are available"
exit 1
fi
}
# Run production container
run_container() {
log_info "Starting production containers..."
# Start the containers
docker-compose up -d
if [ $? -eq 0 ]; then
log_success "Containers started successfully!"
log_info "Backend API available at: http://localhost:3300"
log_info "Database available at: localhost:5432"
log_info "Redis available at: localhost:6379"
log_info ""
log_info "Use 'docker-compose logs -f backend' to view logs"
else
log_error "Failed to start containers"
exit 1
fi
}
# Run development environment
run_dev() {
log_info "Starting development environment..."
# Start development containers
docker-compose --profile dev up -d
if [ $? -eq 0 ]; then
log_success "Development environment started!"
log_info "Backend API (dev) available at: http://localhost:3001"
log_info "Backend API (prod) available at: http://localhost:3300"
log_info ""
log_info "Use 'docker-compose logs -f backend-dev' to view development logs"
else
log_error "Failed to start development environment"
exit 1
fi
}
# Run migrations
run_migrations() {
log_info "Running database migrations..."
# Ensure database is running
docker-compose up -d postgres
sleep 5
# Run migrations
docker-compose --profile migrate up migrate
if [ $? -eq 0 ]; then
log_success "Migrations completed successfully!"
else
log_warning "Migrations may have failed or are already up to date"
fi
}
# Stop containers
stop_containers() {
log_info "Stopping all containers..."
docker-compose down
if [ $? -eq 0 ]; then
log_success "All containers stopped"
else
log_error "Failed to stop containers"
exit 1
fi
}
# Clean up containers and images
clean_up() {
log_warning "This will remove all containers, networks, and images related to this project"
read -p "Are you sure? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
log_info "Cleaning up containers and images..."
# Stop and remove containers
docker-compose down -v --remove-orphans
# Remove images
docker rmi apskel-pos-backend:latest || true
docker rmi $(docker images apskel-pos-backend -q) || true
# Remove unused networks and volumes
docker network prune -f || true
docker volume prune -f || true
log_success "Cleanup completed"
else
log_info "Cleanup cancelled"
fi
}
# Show logs
show_logs() {
log_info "Showing container logs..."
# Show logs for all services
docker-compose logs -f
}
# Main script logic
case "${1:-help}" in
"build")
build_image
;;
"run")
build_image
run_container
;;
"dev")
run_dev
;;
"migrate")
run_migrations
;;
"stop")
stop_containers
;;
"clean")
clean_up
;;
"logs")
show_logs
;;
"help"|*)
show_help
;;
esac

View File

@ -1,8 +1,126 @@
version: "3.3" version: '3.8'
services: services:
app: # PostgreSQL Database
build: . postgres:
image: postgres:15-alpine
container_name: apskel-pos-postgres
restart: unless-stopped
environment:
POSTGRES_DB: apskel_pos
POSTGRES_USER: apskel
POSTGRES_PASSWORD: 7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
networks:
- apskel-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U apskel -d apskel_pos"]
interval: 30s
timeout: 10s
retries: 3
# Redis (Optional - for caching and sessions)
redis:
image: redis:7-alpine
container_name: apskel-pos-redis
restart: unless-stopped
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- apskel-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 3
# Backend API
backend:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: apskel-pos-backend
restart: unless-stopped
environment:
ENV_MODE: production
GIN_MODE: release
ports: ports:
- "3300:3300" - "3300:3300"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- apskel-network
volumes: volumes:
- ./:/app/ - ./infra:/app/infra:ro
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3300/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
# Development backend (for local development)
backend-dev:
build:
context: .
dockerfile: Dockerfile
target: development
container_name: apskel-pos-backend-dev
restart: unless-stopped
environment:
ENV_MODE: development
GIN_MODE: debug
ports:
- "3001:3300"
depends_on:
postgres:
condition: service_healthy
networks:
- apskel-network
volumes:
- .:/app
- go_modules:/go/pkg/mod
profiles:
- dev
# Migration service (run once)
migrate:
build:
context: .
dockerfile: Dockerfile
target: migration
container_name: apskel-pos-migrate
environment:
ENV_MODE: production
depends_on:
postgres:
condition: service_healthy
networks:
- apskel-network
volumes:
- ./infra:/app/infra:ro
- ./migrations:/app/migrations:ro
profiles:
- migrate
volumes:
postgres_data:
driver: local
redis_data:
driver: local
go_modules:
driver: local
networks:
apskel-network:
driver: bridge

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

46
go.mod
View File

@ -1,40 +1,28 @@
module enaklo-pos-be module apskel-pos-be
go 1.20 go 1.21
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.17.0 github.com/go-playground/validator/v10 v10.17.0
github.com/gofrs/uuid v4.2.0+incompatible
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/uuid v1.1.2 github.com/google/uuid v1.1.2
github.com/lib/pq v1.2.0 github.com/lib/pq v1.2.0
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.16.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/datatypes v1.2.0
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/antihax/optional v1.0.0 // indirect
github.com/bytedance/sonic v1.10.2 // indirect github.com/bytedance/sonic v1.10.2 // indirect
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/spec v0.20.14 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -42,51 +30,43 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.4.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect go.uber.org/multierr v1.8.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect golang.org/x/text v0.19.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gorm.io/driver/mysql v1.4.7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
) )
require ( require (
github.com/aws/aws-sdk-go v1.50.0 github.com/aws/aws-sdk-go v1.55.7
github.com/getbrevo/brevo-go v1.0.0 github.com/golang-jwt/jwt/v5 v5.2.3
github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00 github.com/stretchr/testify v1.8.4
github.com/xuri/excelize/v2 v2.9.0
go.uber.org/zap v1.21.0 go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
golang.org/x/net v0.30.0
gorm.io/driver/postgres v1.5.0 gorm.io/driver/postgres v1.5.0
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
) )

85
go.sum
View File

@ -38,12 +38,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aws/aws-sdk-go v1.50.0 h1:HBtrLeO+QyDKnc3t1+5DR1RxodOHCGr8ZcrHudpv7jI=
github.com/aws/aws-sdk-go v1.50.0/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
@ -51,8 +47,6 @@ github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@ -78,13 +72,11 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/getbrevo/brevo-go v1.0.0 h1:E/pRCsQeExvZeTCJU5vy+xHWcLaL5axWQ9QkxjlFke4=
github.com/getbrevo/brevo-go v1.0.0/go.mod h1:2TBMEnaDqq/oiAXUYtn6eykiEdHcEoS7tc63+YoFibw=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
@ -92,31 +84,18 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/spec v0.20.14 h1:7CBlRnw+mtjFGlPDRZmAMnq35cRzI91xj03HVyUi/Do=
github.com/go-openapi/spec v0.20.14/go.mod h1:8EOhTpBoFiask8rrgwbLC3zmJfz4zsCUueRuPM6GNkw=
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -154,6 +133,7 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -189,15 +169,12 @@ github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHo
github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -212,6 +189,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -222,12 +200,8 @@ github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -235,8 +209,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -246,14 +218,12 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
@ -266,6 +236,7 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@ -280,24 +251,10 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=
github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=
github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04=
github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00 h1:iCcVFY2mUdalvtpNN0M/vcf7+OYHGKXwzG5JLZgjwQU=
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00/go.mod h1:21mwYsDK+z+5kR2fvUB8n2yijZZm504Vjzk1s0rNQJg=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -343,11 +300,9 @@ golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN5RBoH5xkJk3CqlMI/Y=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@ -371,7 +326,6 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -407,7 +361,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -419,8 +372,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -434,7 +385,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -473,6 +423,7 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -545,8 +496,6 @@ golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -651,19 +600,13 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

34
infra/development.yaml Normal file
View File

@ -0,0 +1,34 @@
server:
base-url:
local-url:
port: 4000
jwt:
token:
expires-ttl: 1440
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
postgresql:
host: 62.72.45.250
port: 5433
driver: postgres
db: apskel_pos
username: apskel
password: '7a8UJbM2GgBWaseh0lnP3O5i1i5nINXk'
ssl-mode: disable
max-idle-connections-in-second: 600
max-open-connections-in-second: 600
connection-max-life-time-in-second: 600
debug: false
s3:
access_key_id: cf9a475e18bc7626cbdbf09709d82a64
access_key_secret: 91f3321294d3e23035427a0ecb893ada
endpoint: sin1.contabostorage.com
bucket_name: enaklo
log_level: Error
host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:'
log:
log_format: 'json'
log_level: 'debug'

View File

@ -1,96 +0,0 @@
server:
base-url: https://api.enaklo-pos.id/core
local-url: http://localhost:3300
port: 3300
jwt:
token:
expires-ttl: 1440
secret: "5Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
token-order:
expires-ttl: 2
secret: "123Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
token-withdraw:
expires-ttl: 2
secret: "909Lm25V3Qd7aut8dr4QUxm5PZUrSFs"
token-customer:
expires-ttl: 1400
secret: "WakLm25V3Qd7aut8dr4QUxm5PZUrWa#"
postgresql:
host: 62.72.45.250
port: 22010
driver: postgres
db: enaklo-pos-staging
username: admin
password: '4W^he3^BBBmPisWa$J#2'
ssl-mode: disable
max-idle-connections-in-second: 600
max-open-connections-in-second: 600
connection-max-life-time-in-second: 600
debug: false
oss:
access_key_id: cf9a475e18bc7626cbdbf09709d82a64
access_key_secret: 91f3321294d3e23035427a0ecb893ada
endpoint: sin1.contabostorage.com
bucket_name: enaklo
log_level: Error
host_url: 'https://sin1.contabostorage.com/fda98c2228f246f29a7e466b86b3b9e7:'
midtrans:
server_key: "SB-Mid-server-YOIvuaIlRw3In9SymCuFz-hB"
client_key: "SB-Mid-client-ulkZGFiS8PqBNOZz"
env: 1
linkqu:
base_url: "https://gateway-dev.linkqu.id"
client_id: "testing"
client_secret: "123"
signature_key: "LinkQu@2020"
username: "LI307GXIN"
pin: "2K2NPCBBNNTovgB"
callback_url: "https://enaklo-pos-be.app-dev.altru.id/api/v1/linkqu/callback"
brevo:
api_key: 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
email:
sender: "noreply@enaklo.co.id"
sender_customer: "enaklo-pos.official@gmail.com"
reset_password:
template_name: "reset_password"
template_path: "templates/reset_password.html"
template_path_customer: "templates/reset_password_customer.html"
subject: "Reset Password"
opening_word: "Terima kasih sudah menjadi bagian dari enaklo-pos. Anda telah berhasil melakukan reset password, silakan masukan unik password yang dibuat oleh sistem dibawah ini:"
closing_word: "Silakan login kembali menggunakan email dan password anda diatas, sistem akan secara otomatis meminta anda untuk membuat password baru setelah berhasil login. Mohon maaf atas kendala yang dialami."
order:
fee: 5000
withdrawal:
platform_fee: 5000
discovery:
explore_destinations:
- name: "Jakarta"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/03c0b046-43ab-4d35-a743-6a173bc66b90-1722680749.png"
- name: "Banten"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/c8e7dd8a-17be-449f-afdc-0c07eda438ce-1722680809.png"
- name: "Yogyakarta"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/83b78c19-4c97-48c9-bc97-a7403e1c4eed-1722680828.png"
- name: "Jawa Barat"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/07c35ab1-3e20-4858-8d7d-b29517239dc3-1722680848.png"
- name: "Jawa Tengah"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/a1915a98-c2aa-4997-8e75-bd4e43789b0c-1722680874.png"
- name: "Jawa Timur"
image_url: "https://obs.eranyacloud.com/enaklo-pos-dev/file/7b5d2b86-e8a8-4703-a153-c186021cf088-1722680894.png"
explore_regions:
- name: "Jawa"
- name: "Sumatera"
- name: "Kalimantan"
- name: "Sulawesi"
feature_toggle:
logger_enabled: false

BIN
internal/.DS_Store vendored

Binary file not shown.

242
internal/README.md Normal file
View File

@ -0,0 +1,242 @@
# Internal Architecture
This document describes the clean architecture implementation for the POS backend with complete separation of concerns between database entities, business models, and constants.
## 📁 Package Structure
### `/constants` - Business Constants
- **Purpose**: All business logic constants, enums, and validation helpers
- **Usage**: Used by models, services, and validation layers
- **Features**:
- Type-safe enums (UserRole, OrderStatus, PaymentStatus, etc.)
- Business validation functions (IsValidUserRole, etc.)
- Default values and limits
- No dependencies on database or frameworks
### `/entities` - Database Models
- **Purpose**: Database-specific models with GORM tags and hooks
- **Usage**: **ONLY** used by repository layer for database operations
- **Features**:
- GORM annotations (`gorm:` tags)
- Database relationships and constraints
- BeforeCreate/AfterCreate hooks
- Table name specifications
- SQL-specific data types
- **Never used in business logic**
### `/models` - Business Models
- **Purpose**: **Pure** business domain models without any framework dependencies
- **Usage**: Used by services, handlers, and business logic
- **Features**:
- Clean JSON serialization (`json:` tags)
- Validation rules (`validate:` tags)
- Request/Response DTOs
- **Zero GORM dependencies**
- **Zero database annotations**
- Uses constants package for type safety
- Pure business logic methods
### `/mappers` - Data Transformation
- **Purpose**: Convert between entities and business models
- **Usage**: Bridge between repository and service layers
- **Features**:
- Entity ↔ Model conversion functions
- Request DTO → Entity conversion
- Entity → Response DTO conversion
- Null-safe conversions
- Slice/collection conversions
- Type conversions between constants and entities
### `/repository` - Data Access Layer
- **Purpose**: Database operations using entities exclusively
- **Usage**: Only works with database entities
- **Features**:
- CRUD operations with entities
- Query methods with entities
- **Private repository implementations**
- Interface-based contracts
- **Never references business models**
## 🔄 Data Flow
```
API Request (JSON)
Request DTO (models)
Business Logic (services with models + constants)
Entity (via mapper)
Repository Layer (entities only)
Database
Entity (from database)
Business Model (via mapper)
Response DTO (models)
API Response (JSON)
```
## 🎯 Key Design Principles
### ✅ **Clean Business Models**
```go
type User struct {
ID uuid.UUID `json:"id"`
Role constants.UserRole `json:"role"`
}
```
```go
type User struct {
ID uuid.UUID `gorm:"primaryKey" json:"id"`
Role string `gorm:"size:50" json:"role"`
}
```
### ✅ **Type-Safe Constants**
```go
type UserRole string
const (
RoleAdmin UserRole = "admin"
)
func IsValidUserRole(role UserRole) bool { /* ... */ }
```
```go
const AdminRole = "admin" ```
### ✅ **Repository Isolation**
```go
func (r *userRepository) Create(ctx context.Context, user *entities.User) error {
return r.db.Create(user).Error
}
```
```go
func (r *userRepository) Create(ctx context.Context, user *models.User) error {
}
```
## 📊 Example Usage
### Service Layer (Business Logic)
```go
func (s *userService) CreateUser(req *models.UserCreateRequest) (*models.UserResponse, error) {
if !constants.IsValidUserRole(req.Role) {
return nil, errors.New("invalid role")
}
entity := mappers.UserCreateRequestToEntity(req, hashedPassword)
err := s.userRepo.Create(ctx, entity)
if err != nil {
return nil, err
}
return mappers.UserEntityToResponse(entity), nil
}
```
### Repository Layer (Data Access)
```go
func (r *userRepository) Create(ctx context.Context, user *entities.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
```
### Handler Layer (API)
```go
func (h *userHandler) CreateUser(c *gin.Context) {
var req models.UserCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
resp, err := h.userService.CreateUser(&req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(201, resp)
}
```
## 🏗️ Architecture Benefits
1. **🎯 Single Responsibility**: Each package has one clear purpose
2. **🔒 Zero Database Leakage**: Business logic never sees database concerns
3. **🧪 Testability**: Easy to mock interfaces and test business logic
4. **🔧 Maintainability**: Changes to database don't affect business models
5. **🚀 Flexibility**: Can change ORM without touching business logic
6. **📜 API Stability**: Business models provide stable contracts
7. **🛡️ Type Safety**: Constants package prevents invalid states
8. **🧹 Clean Code**: No mixed concerns anywhere in the codebase
## 📋 Development Guidelines
### Constants Package (`/constants`)
- ✅ Define all business enums and constants
- ✅ Provide validation helper functions
- ✅ Include default values and limits
- ❌ Never import database or framework packages
- ❌ No business logic, only constants and validation
### Models Package (`/models`)
- ✅ Pure business structs with JSON tags only
- ✅ Use constants package for type safety
- ✅ Include validation tags for input validation
- ✅ Separate Request/Response DTOs
- ✅ Add business logic methods (validation, calculations)
- ❌ **NEVER** include GORM tags or database annotations
- ❌ **NEVER** import database packages
- ❌ No database relationships or foreign keys
### Entities Package (`/entities`)
- ✅ Include GORM tags and database constraints
- ✅ Define relationships and foreign keys
- ✅ Add database hooks (BeforeCreate, etc.)
- ✅ Use database-specific types
- ❌ **NEVER** use in business logic or handlers
- ❌ **NEVER** add business validation rules
### Mappers Package (`/mappers`)
- ✅ Always check for nil inputs
- ✅ Handle type conversions between constants and strings
- ✅ Provide slice conversion helpers
- ✅ Keep conversions simple and direct
- ❌ No business logic in mappers
- ❌ No database operations
### Repository Package (`/repository`)
- ✅ Work exclusively with entities
- ✅ Use private repository implementations
- ✅ Provide clean interface contracts
- ❌ **NEVER** reference business models
- ❌ **NEVER** import models package
## 🚀 Migration Complete
**All packages have been successfully reorganized:**
- ✅ **4 Constants files** - All business constants moved to type-safe enums
- ✅ **10 Clean Model files** - Zero GORM dependencies, pure business logic
- ✅ **11 Entity files** - Database-only models with GORM tags
- ✅ **11 Repository files** - Updated to use entities exclusively
- ✅ **2 Mapper files** - Handle conversions between layers
- ✅ **Complete separation** - No cross-layer dependencies
**The codebase now follows strict clean architecture principles with complete separation of database concerns from business logic!** 🎉

296
internal/app/app.go Normal file
View File

@ -0,0 +1,296 @@
package app
import (
"apskel-pos-be/internal/client"
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"apskel-pos-be/config"
"apskel-pos-be/internal/handler"
"apskel-pos-be/internal/middleware"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/repository"
"apskel-pos-be/internal/router"
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/validator"
"gorm.io/gorm"
)
type App struct {
server *http.Server
db *gorm.DB
router *router.Router
shutdown chan os.Signal
}
func NewApp(db *gorm.DB) *App {
return &App{
db: db,
shutdown: make(chan os.Signal, 1),
}
}
func (a *App) Initialize(cfg *config.Config) error {
repos := a.initRepositories()
processors := a.initProcessors(cfg, repos)
services := a.initServices(processors, cfg)
validators := a.initValidators()
middleware := a.initMiddleware(services)
healthHandler := handler.NewHealthHandler()
a.router = router.NewRouter(
cfg,
healthHandler,
services.authService,
middleware.authMiddleware,
services.userService,
validators.userValidator,
services.organizationService,
validators.organizationValidator,
services.outletService,
validators.outletValidator,
services.outletSettingService,
services.categoryService,
validators.categoryValidator,
services.productService,
validators.productValidator,
services.productVariantService,
validators.productVariantValidator,
services.inventoryService,
validators.inventoryValidator,
services.orderService,
validators.orderValidator,
services.fileService,
validators.fileValidator,
services.customerService,
validators.customerValidator,
services.paymentMethodService,
validators.paymentMethodValidator,
services.analyticsService,
services.tableService,
)
return nil
}
func (a *App) Start(port string) error {
engine := a.router.Init()
a.server = &http.Server{
Addr: ":" + port,
Handler: engine,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
signal.Notify(a.shutdown, os.Interrupt, syscall.SIGTERM)
go func() {
log.Printf("Server starting on port %s", port)
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
<-a.shutdown
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := a.server.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
return err
}
log.Println("Server exited gracefully")
return nil
}
func (a *App) Shutdown() {
close(a.shutdown)
}
type repositories struct {
userRepo *repository.UserRepositoryImpl
organizationRepo *repository.OrganizationRepositoryImpl
outletRepo *repository.OutletRepositoryImpl
outletSettingRepo *repository.OutletSettingRepositoryImpl
categoryRepo *repository.CategoryRepositoryImpl
productRepo *repository.ProductRepositoryImpl
productVariantRepo *repository.ProductVariantRepositoryImpl
inventoryRepo *repository.InventoryRepositoryImpl
inventoryMovementRepo *repository.InventoryMovementRepositoryImpl
orderRepo *repository.OrderRepositoryImpl
orderItemRepo *repository.OrderItemRepositoryImpl
paymentRepo *repository.PaymentRepositoryImpl
paymentMethodRepo *repository.PaymentMethodRepositoryImpl
fileRepo *repository.FileRepositoryImpl
customerRepo *repository.CustomerRepository
analyticsRepo *repository.AnalyticsRepositoryImpl
tableRepo *repository.TableRepository
}
func (a *App) initRepositories() *repositories {
return &repositories{
userRepo: repository.NewUserRepository(a.db),
organizationRepo: repository.NewOrganizationRepositoryImpl(a.db),
outletRepo: repository.NewOutletRepositoryImpl(a.db),
outletSettingRepo: repository.NewOutletSettingRepositoryImpl(a.db),
categoryRepo: repository.NewCategoryRepositoryImpl(a.db),
productRepo: repository.NewProductRepositoryImpl(a.db),
productVariantRepo: repository.NewProductVariantRepositoryImpl(a.db),
inventoryRepo: repository.NewInventoryRepositoryImpl(a.db),
inventoryMovementRepo: repository.NewInventoryMovementRepositoryImpl(a.db),
orderRepo: repository.NewOrderRepositoryImpl(a.db),
orderItemRepo: repository.NewOrderItemRepositoryImpl(a.db),
paymentRepo: repository.NewPaymentRepositoryImpl(a.db),
paymentMethodRepo: repository.NewPaymentMethodRepositoryImpl(a.db),
fileRepo: repository.NewFileRepositoryImpl(a.db),
customerRepo: repository.NewCustomerRepository(a.db),
analyticsRepo: repository.NewAnalyticsRepositoryImpl(a.db),
tableRepo: repository.NewTableRepository(a.db),
}
}
type processors struct {
userProcessor *processor.UserProcessorImpl
organizationProcessor processor.OrganizationProcessor
outletProcessor processor.OutletProcessor
outletSettingProcessor *processor.OutletSettingProcessorImpl
categoryProcessor processor.CategoryProcessor
productProcessor processor.ProductProcessor
productVariantProcessor processor.ProductVariantProcessor
inventoryProcessor processor.InventoryProcessor
orderProcessor processor.OrderProcessor
paymentMethodProcessor processor.PaymentMethodProcessor
fileProcessor processor.FileProcessor
customerProcessor *processor.CustomerProcessor
analyticsProcessor *processor.AnalyticsProcessorImpl
tableProcessor *processor.TableProcessor
}
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
fileClient := client.NewFileClient(cfg.S3Config)
return &processors{
userProcessor: processor.NewUserProcessor(repos.userRepo, repos.organizationRepo, repos.outletRepo),
organizationProcessor: processor.NewOrganizationProcessorImpl(repos.organizationRepo, repos.outletRepo, repos.userRepo),
outletProcessor: processor.NewOutletProcessorImpl(repos.outletRepo),
outletSettingProcessor: processor.NewOutletSettingProcessorImpl(repos.outletSettingRepo, repos.outletRepo),
categoryProcessor: processor.NewCategoryProcessorImpl(repos.categoryRepo),
productProcessor: processor.NewProductProcessorImpl(repos.productRepo, repos.categoryRepo, repos.productVariantRepo, repos.inventoryRepo, repos.outletRepo),
productVariantProcessor: processor.NewProductVariantProcessorImpl(repos.productVariantRepo, repos.productRepo),
inventoryProcessor: processor.NewInventoryProcessorImpl(repos.inventoryRepo, repos.productRepo, repos.outletRepo),
orderProcessor: processor.NewOrderProcessorImpl(repos.orderRepo, repos.orderItemRepo, repos.paymentRepo, repos.productRepo, repos.paymentMethodRepo, repos.inventoryRepo, repos.inventoryMovementRepo, repos.productVariantRepo, repos.outletRepo, repos.customerRepo),
paymentMethodProcessor: processor.NewPaymentMethodProcessorImpl(repos.paymentMethodRepo),
fileProcessor: processor.NewFileProcessorImpl(repos.fileRepo, fileClient),
customerProcessor: processor.NewCustomerProcessor(repos.customerRepo),
analyticsProcessor: processor.NewAnalyticsProcessorImpl(repos.analyticsRepo),
tableProcessor: processor.NewTableProcessor(repos.tableRepo, repos.orderRepo),
}
}
type services struct {
userService *service.UserServiceImpl
authService service.AuthService
organizationService service.OrganizationService
outletService service.OutletService
outletSettingService service.OutletSettingService
categoryService service.CategoryService
productService service.ProductService
productVariantService service.ProductVariantService
inventoryService service.InventoryService
orderService service.OrderService
paymentMethodService service.PaymentMethodService
fileService service.FileService
customerService service.CustomerService
analyticsService *service.AnalyticsServiceImpl
tableService *service.TableService
}
func (a *App) initServices(processors *processors, cfg *config.Config) *services {
authConfig := cfg.Auth()
jwtSecret := authConfig.AccessTokenSecret()
authService := service.NewAuthService(processors.userProcessor, jwtSecret)
organizationService := service.NewOrganizationService(processors.organizationProcessor)
outletService := service.NewOutletService(processors.outletProcessor)
outletSettingService := service.NewOutletSettingService(processors.outletSettingProcessor)
categoryService := service.NewCategoryService(processors.categoryProcessor)
productService := service.NewProductService(processors.productProcessor)
productVariantService := service.NewProductVariantService(processors.productVariantProcessor)
inventoryService := service.NewInventoryService(processors.inventoryProcessor)
orderService := service.NewOrderServiceImpl(processors.orderProcessor)
paymentMethodService := service.NewPaymentMethodService(processors.paymentMethodProcessor)
fileService := service.NewFileServiceImpl(processors.fileProcessor)
var customerService service.CustomerService = service.NewCustomerService(processors.customerProcessor)
analyticsService := service.NewAnalyticsServiceImpl(processors.analyticsProcessor)
tableService := service.NewTableService(processors.tableProcessor, transformer.NewTableTransformer())
return &services{
userService: service.NewUserService(processors.userProcessor),
authService: authService,
organizationService: organizationService,
outletService: outletService,
outletSettingService: outletSettingService,
categoryService: categoryService,
productService: productService,
productVariantService: productVariantService,
inventoryService: inventoryService,
orderService: orderService,
paymentMethodService: paymentMethodService,
fileService: fileService,
customerService: customerService,
analyticsService: analyticsService,
tableService: tableService,
}
}
type middlewares struct {
authMiddleware *middleware.AuthMiddleware
}
func (a *App) initMiddleware(services *services) *middlewares {
return &middlewares{
authMiddleware: middleware.NewAuthMiddleware(services.authService),
}
}
type validators struct {
userValidator *validator.UserValidatorImpl
organizationValidator validator.OrganizationValidator
outletValidator validator.OutletValidator
categoryValidator validator.CategoryValidator
productValidator validator.ProductValidator
productVariantValidator validator.ProductVariantValidator
inventoryValidator validator.InventoryValidator
orderValidator validator.OrderValidator
paymentMethodValidator validator.PaymentMethodValidator
fileValidator validator.FileValidator
customerValidator validator.CustomerValidator
tableValidator *validator.TableValidator
}
func (a *App) initValidators() *validators {
return &validators{
userValidator: validator.NewUserValidator(),
organizationValidator: validator.NewOrganizationValidator(),
outletValidator: validator.NewOutletValidator(),
categoryValidator: validator.NewCategoryValidator(),
productValidator: validator.NewProductValidator(),
productVariantValidator: validator.NewProductVariantValidator(),
inventoryValidator: validator.NewInventoryValidator(),
orderValidator: validator.NewOrderValidator(),
paymentMethodValidator: validator.NewPaymentMethodValidator(),
fileValidator: validator.NewFileValidatorImpl(),
customerValidator: validator.NewCustomerValidator(),
tableValidator: validator.NewTableValidator(),
}
}

View File

@ -1,46 +1,18 @@
package app package app
import ( import (
"enaklo-pos-be/config"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gofrs/uuid" "github.com/google/uuid"
"enaklo-pos-be/internal/middlewares"
) )
func NewServer(cfg *config.Config) *Server {
gin.SetMode(gin.ReleaseMode)
engine := gin.New()
engine.RedirectTrailingSlash = true
engine.RedirectFixedPath = true
server := &Server{
engine,
}
server.Use(middlewares.Cors())
server.Use(middlewares.LogCorsError())
server.Use(middlewares.Trace())
server.Use(middlewares.Logger(&cfg.FeatureToggle))
server.Use(middlewares.RequestMiddleware(&cfg.FeatureToggle))
return server
}
type Server struct { type Server struct {
*gin.Engine *gin.Engine
} }
func (*Server) GenerateUUID() (string, error) { func generateServerID() string {
id, err := uuid.NewV4() return uuid.New().String()
if err != nil {
return "", err
}
return id.String(), nil
} }
func (s Server) Listen(address string) error { func (s Server) Listen(address string) error {

View File

@ -0,0 +1,80 @@
package appcontext
import (
"context"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type key string
const (
CorrelationIDKey = key("CorrelationID")
OrganizationIDKey = key("OrganizationIDKey")
UserIDKey = key("UserID")
OutletIDKey = key("OutletID")
RoleIDKey = key("RoleID")
AppVersionKey = key("AppVersion")
AppIDKey = key("AppID")
AppTypeKey = key("AppType")
PlatformKey = key("platform")
DeviceOSKey = key("deviceOS")
UserLocaleKey = key("userLocale")
UserRoleKey = key("userRole")
)
func LogFields(ctx interface{}) map[string]interface{} {
fields := make(map[string]interface{})
fields[string(CorrelationIDKey)] = value(ctx, CorrelationIDKey)
fields[string(OrganizationIDKey)] = value(ctx, OrganizationIDKey)
fields[string(OutletIDKey)] = value(ctx, OutletIDKey)
fields[string(AppVersionKey)] = value(ctx, AppVersionKey)
fields[string(AppIDKey)] = value(ctx, AppIDKey)
fields[string(AppTypeKey)] = value(ctx, AppTypeKey)
fields[string(UserIDKey)] = value(ctx, UserIDKey)
fields[string(PlatformKey)] = value(ctx, PlatformKey)
fields[string(DeviceOSKey)] = value(ctx, DeviceOSKey)
fields[string(UserLocaleKey)] = value(ctx, UserLocaleKey)
return fields
}
func value(ctx interface{}, key key) string {
switch c := ctx.(type) {
case *gin.Context:
return getFromGinContext(c, key)
case context.Context:
return getFromGoContext(c, key)
default:
return ""
}
}
func uuidValue(ctx interface{}, key key) uuid.UUID {
switch c := ctx.(type) {
case *gin.Context:
val, _ := uuid.Parse(getFromGinContext(c, key))
return val
case context.Context:
val, _ := uuid.Parse(getFromGoContext(c, key))
return val
default:
return uuid.New()
}
}
func getFromGinContext(c *gin.Context, key key) string {
keyStr := string(key)
if val, exists := c.Get(keyStr); exists {
if str, ok := val.(string); ok {
return str
}
}
return getFromGoContext(c.Request.Context(), key)
}
func getFromGoContext(ctx context.Context, key key) string {
if val, ok := ctx.Value(key).(string); ok {
return val
}
return ""
}

View File

@ -0,0 +1,81 @@
package appcontext
import (
"context"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
type logCtxKeyType struct{}
var logCtxKey = logCtxKeyType(struct{}{})
type Logger struct {
*logrus.Logger
}
var log *Logger
type ContextInfo struct {
CorrelationID string
UserID uuid.UUID
OrganizationID uuid.UUID
OutletID string
AppVersion string
AppID string
AppType string
Platform string
DeviceOS string
UserLocale string
UserRole string
}
type ctxKeyType struct{}
var ctxKey = ctxKeyType(struct{}{})
func NewAppContext(ctx context.Context, info *ContextInfo) context.Context {
ctx = NewContext(ctx, map[string]interface{}{
"correlation_id": info.CorrelationID,
"user_id": info.UserID,
"app_version": info.AppVersion,
"app_id": info.AppID,
"app_type": info.AppType,
"platform": info.Platform,
"device_os": info.DeviceOS,
"user_locale": info.UserLocale,
})
return context.WithValue(ctx, ctxKey, info)
}
func NewContext(ctx context.Context, baseFields map[string]interface{}) context.Context {
entry, ok := ctx.Value(logCtxKey).(*logrus.Entry)
if !ok {
entry = log.WithFields(map[string]interface{}{})
}
return context.WithValue(ctx, logCtxKey, entry.WithFields(baseFields))
}
func FromGinContext(ctx context.Context) *ContextInfo {
return &ContextInfo{
CorrelationID: value(ctx, CorrelationIDKey),
UserID: uuidValue(ctx, UserIDKey),
OutletID: value(ctx, OutletIDKey),
OrganizationID: uuidValue(ctx, OrganizationIDKey),
AppVersion: value(ctx, AppVersionKey),
AppID: value(ctx, AppIDKey),
AppType: value(ctx, AppTypeKey),
Platform: value(ctx, PlatformKey),
DeviceOS: value(ctx, DeviceOSKey),
UserLocale: value(ctx, UserLocaleKey),
UserRole: value(ctx, UserRoleKey),
}
}
func FromContext(ctx context.Context) *ContextInfo {
if info, ok := ctx.Value(ctxKey).(*ContextInfo); ok {
return info
}
return nil
}

View File

@ -1,4 +1,4 @@
package oss package client
import ( import (
"bytes" "bytes"
@ -11,7 +11,7 @@ import (
"github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3"
) )
type OSSConfig interface { type FileConfig interface {
GetAccessKeyID() string GetAccessKeyID() string
GetAccessKeySecret() string GetAccessKeySecret() string
GetEndpoint() string GetEndpoint() string
@ -22,17 +22,17 @@ type OSSConfig interface {
const _awsRegion = "us-east-1" const _awsRegion = "us-east-1"
const _s3ACL = "public-read" const _s3ACL = "public-read"
type OssRepositoryImpl struct { type S3FileClientImpl struct {
s3 *s3.S3 s3 *s3.S3
cfg OSSConfig cfg FileConfig
} }
func NewOssRepositoryImpl(ossCfg OSSConfig) *OssRepositoryImpl { func NewFileClient(fileCfg FileConfig) *S3FileClientImpl {
sess, err := session.NewSession(&aws.Config{ sess, err := session.NewSession(&aws.Config{
S3ForcePathStyle: aws.Bool(true), S3ForcePathStyle: aws.Bool(true),
Endpoint: aws.String(ossCfg.GetEndpoint()), Endpoint: aws.String(fileCfg.GetEndpoint()),
Region: aws.String(_awsRegion), Region: aws.String(_awsRegion),
Credentials: credentials.NewStaticCredentials(ossCfg.GetAccessKeyID(), ossCfg.GetAccessKeySecret(), ""), Credentials: credentials.NewStaticCredentials(fileCfg.GetAccessKeyID(), fileCfg.GetAccessKeySecret(), ""),
}) })
if err != nil { if err != nil {
@ -40,13 +40,13 @@ func NewOssRepositoryImpl(ossCfg OSSConfig) *OssRepositoryImpl {
return nil return nil
} }
return &OssRepositoryImpl{ return &S3FileClientImpl{
s3: s3.New(sess), s3: s3.New(sess),
cfg: ossCfg, cfg: fileCfg,
} }
} }
func (r *OssRepositoryImpl) UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) { func (r *S3FileClientImpl) UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error) {
reader := bytes.NewReader(fileContent) reader := bytes.NewReader(fileContent)
_, err = r.s3.PutObject(&s3.PutObjectInput{ _, err = r.s3.PutObject(&s3.PutObjectInput{
@ -59,7 +59,7 @@ func (r *OssRepositoryImpl) UploadFile(ctx context.Context, fileName string, fil
return r.GetPublicURL(fileName), err return r.GetPublicURL(fileName), err
} }
func (r *OssRepositoryImpl) GetPublicURL(fileName string) string { func (r *S3FileClientImpl) GetPublicURL(fileName string) string {
if fileName == "" { if fileName == "" {
return "" return ""
} }

Binary file not shown.

View File

@ -1,5 +0,0 @@
package database
type Config interface {
ConnString() string
}

View File

@ -1,52 +0,0 @@
package errors
import "net/http"
const (
Success Code = "20000"
ServerError Code = "50000"
BadRequest Code = "40000"
InvalidRequest Code = "40001"
Unauthorized Code = "40100"
CheckinInvalid Code = "40002"
Forbidden Code = "40300"
Timeout Code = "50400"
)
type Code string
var (
codeMap = map[Code]string{
Success: "Success",
BadRequest: "Bad or invalid request",
Unauthorized: "Unauthorized Token",
Timeout: "Gateway Timeout",
ServerError: "Internal Server Error",
Forbidden: "Forbidden",
InvalidRequest: "Invalid Request",
CheckinInvalid: "Ticket Already Used or Expired",
}
codeHTTPMap = map[Code]int{
Success: http.StatusOK,
BadRequest: http.StatusBadRequest,
Unauthorized: http.StatusUnauthorized,
Timeout: http.StatusGatewayTimeout,
ServerError: http.StatusInternalServerError,
Forbidden: http.StatusForbidden,
InvalidRequest: http.StatusUnprocessableEntity,
CheckinInvalid: http.StatusBadRequest,
}
)
func (c Code) GetMessage() string {
return codeMap[c]
}
func (c Code) GetHTTPCode() int {
return codeHTTPMap[c]
}
func (c Code) GetCode() string {
return string(c)
}

View File

@ -1,142 +0,0 @@
package errors
import "net/http"
type ErrType string
const (
errRequestTimeOut ErrType = "Request Timeout to 3rd Party"
errConnectTimeOut ErrType = "Connect Timeout to 3rd Party"
errFailedExternalCall ErrType = "Failed response from 3rd Party call"
errExternalCall ErrType = "error on 3rd Party call"
errInvalidRequest ErrType = "Invalid Request"
errBadRequest ErrType = "Bad Request"
errOrderNotFound ErrType = "Astria order is not found"
errCheckoutIDNotDefined ErrType = "Checkout client id not found"
errInternalServer ErrType = "Internal Server error"
errExternalServer ErrType = "External Server error"
errUserIsNotFound ErrType = "User is not found"
errInvalidLogin ErrType = "User email or password is invalid"
errUnauthorized ErrType = "Unauthorized"
errInsufficientBalance ErrType = "Insufficient Balance"
errInactivePartner ErrType = "Partner's license is invalid or has expired. Please contact Admin Support."
errTicketAlreadyUsed ErrType = "Ticket Already Used."
errProductIsRequired ErrType = "Product"
errEmailAndPhoneNumberRequired ErrType = "Email or Phone is required"
errEmailAlreadyRegistered ErrType = "Email is already registered"
errPhoneNumberAlreadyRegistered ErrType = "Phone is already registered"
)
var (
ErrorBadRequest = NewServiceException(errBadRequest)
ErrorInvalidRequest = NewServiceException(errInvalidRequest)
ErrorExternalRequest = NewServiceException(errExternalServer)
ErrorUnauthorized = NewServiceException(errUnauthorized)
ErrorOrderNotFound = NewServiceException(errOrderNotFound)
ErrorClientIDNotDefined = NewServiceException(errCheckoutIDNotDefined)
ErrorRequestTimeout = NewServiceException(errRequestTimeOut)
ErrorExternalCall = NewServiceException(errExternalCall)
ErrorFailedExternalCall = NewServiceException(errFailedExternalCall)
ErrorConnectionTimeOut = NewServiceException(errConnectTimeOut)
ErrorInternalServer = NewServiceException(errInternalServer)
ErrorUserIsNotFound = NewServiceException(errUserIsNotFound)
ErrorUserInvalidLogin = NewServiceException(errInvalidLogin)
ErrorInsufficientBalance = NewServiceException(errInsufficientBalance)
ErrorInvalidLicense = NewServiceException(errInactivePartner)
ErrorTicketInvalidOrAlreadyUsed = NewServiceException(errTicketAlreadyUsed)
ErrorPhoneNumberEmailIsRequired = NewServiceException(errEmailAndPhoneNumberRequired)
ErrorPhoneNumberIsAlreadyRegistered = NewServiceException(errPhoneNumberAlreadyRegistered)
ErrorEmailIsAlreadyRegistered = NewServiceException(errEmailAlreadyRegistered)
)
type Error interface {
ErrorType() ErrType
MapErrorsToHTTPCode() int
MapErrorsToCode() Code
error
}
type ServiceException struct {
errorType ErrType
message string
}
func NewServiceException(errType ErrType) *ServiceException {
return &ServiceException{
errorType: errType,
message: string(errType),
}
}
func NewError(errType ErrType, message string) *ServiceException {
return &ServiceException{
errorType: errType,
message: message,
}
}
func NewErrorMessage(errType *ServiceException, message string) *ServiceException {
return &ServiceException{
errorType: errType.ErrorType(),
message: message,
}
}
func (s *ServiceException) ErrorType() ErrType {
return s.errorType
}
func (s *ServiceException) Error() string {
return s.message
}
func (s *ServiceException) MapErrorsToHTTPCode() int {
switch s.ErrorType() {
case errBadRequest:
return http.StatusBadRequest
case errInvalidRequest:
return http.StatusBadRequest
case errInvalidLogin:
return http.StatusBadRequest
case errUserIsNotFound:
return http.StatusBadRequest
default:
return http.StatusInternalServerError
}
}
func (s *ServiceException) MapErrorsToCode() Code {
switch s.ErrorType() {
case errUnauthorized:
return Unauthorized
case errConnectTimeOut:
return Timeout
case errBadRequest:
return BadRequest
case errUserIsNotFound:
return BadRequest
case errInvalidLogin:
return BadRequest
case errInsufficientBalance:
return BadRequest
case errInactivePartner:
return BadRequest
case errTicketAlreadyUsed:
return CheckinInvalid
default:
return BadRequest
}
}

View File

@ -1,46 +0,0 @@
package http
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"time"
"go.uber.org/zap"
"enaklo-pos-be/internal/common/logger"
)
type HttpClient struct {
Client *http.Client
}
func NewHttpClient() *HttpClient {
return &HttpClient{
Client: &http.Client{},
}
}
func (c *HttpClient) Do(ctx context.Context, req *http.Request) (int, []byte, error) {
start := time.Now()
logger.ContextLogger(ctx).Info(fmt.Sprintf("Sending request: %v %v", req.Method, req.URL))
resp, err := c.Client.Do(req)
if err != nil {
logger.ContextLogger(ctx).Error(" Failed to send request:", zap.Error(err))
return 0, nil, err
}
logger.ContextLogger(ctx).Info(fmt.Sprintf("Received Response: : %v", resp.StatusCode))
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.ContextLogger(ctx).Error(" Failed to read response:", zap.Error(err))
return 0, nil, err
}
logger.ContextLogger(ctx).Info(fmt.Sprintf("Latency : %v", time.Since(start)))
return resp.StatusCode, body, nil
}

View File

@ -1,56 +0,0 @@
package logger
import (
"context"
"enaklo-pos-be/internal/constants"
"fmt"
"sync"
"go.uber.org/zap"
)
var mainLogger *zap.Logger = nil
var mainLoggerInit sync.Once
func NewMainLoggerSingleton() *zap.Logger {
mainLoggerInit.Do(func() {
logger, err := zap.NewProduction()
if err != nil {
logger.Error("logger initialization failed", zap.Any("error", err))
panic(fmt.Sprintf("logger initialization failed %v", err))
}
logger.Info("logger started")
mainLogger = logger
})
return mainLogger
}
func NewMainNoOpLoggerSingleton() *zap.Logger {
mainLoggerInit.Do(func() {
logger := zap.NewNop()
logger.Info("logger started")
mainLogger = logger
})
return mainLogger
}
func NewNoOp() *zap.Logger {
return zap.NewNop()
}
func GetLogger() *zap.Logger {
return mainLogger
}
func ContextLogger(ctx context.Context) *zap.Logger {
logger := GetLogger()
if ctxRqID, ok := ctx.Value(constants.ContextRequestID).(string); ok {
return logger.With(zap.String(constants.ContextRequestID, ctxRqID))
}
return logger
}

View File

@ -1,96 +0,0 @@
package mycontext
import (
"context"
"enaklo-pos-be/internal/constants/role"
"enaklo-pos-be/internal/entity"
)
type ContextKey string
type Context interface {
context.Context
RequestedBy() int64
IsSuperAdmin() bool
IsAdmin() bool
IsPartnerAdmin() bool
IsCasheer() bool
GetPartnerID() *int64
GetSiteID() *int64
GetName() string
}
type MyContextImpl struct {
context.Context
requestedBy int64
requestID string
partnerID int64
roleID int
siteID int64
name string
}
func (m *MyContextImpl) RequestedBy() int64 {
return m.requestedBy
}
func (m *MyContextImpl) IsSuperAdmin() bool {
return m.roleID == int(role.SuperAdmin)
}
func (m *MyContextImpl) IsAdmin() bool {
return m.roleID == int(role.SuperAdmin) || m.roleID == int(role.Admin)
}
func (m *MyContextImpl) IsPartnerAdmin() bool {
return m.roleID == int(role.PartnerAdmin)
}
func (m *MyContextImpl) IsCasheer() bool {
return m.roleID == int(role.Casheer)
}
func (m *MyContextImpl) GetPartnerID() *int64 {
if m.partnerID != 0 {
return &m.partnerID
}
return nil
}
func (m *MyContextImpl) GetSiteID() *int64 {
if m.siteID != 0 {
return &m.siteID
}
return nil
}
func (m *MyContextImpl) GetName() string {
return m.name
}
func NewMyContext(parent context.Context, claims *entity.JWTAuthClaims) (*MyContextImpl, error) {
return &MyContextImpl{
Context: parent,
requestedBy: claims.UserID,
partnerID: claims.PartnerID,
roleID: claims.Role,
siteID: claims.SiteID,
name: claims.Name,
}, nil
}
func NewMyContextCustomer(parent context.Context, claims *entity.JWTAuthClaimsCustomer) (*MyContextImpl, error) {
return &MyContextImpl{
Context: parent,
requestedBy: claims.UserID,
name: claims.Name,
}, nil
}
func NewContext(parent context.Context) *MyContextImpl {
return &MyContextImpl{
Context: parent,
}
}

View File

@ -1,53 +0,0 @@
package request
import (
"context"
"github.com/gin-gonic/gin"
)
const (
ReqInfoKey reqInfoKeyType = "request-info"
)
func SetTraceId(c *gin.Context, traceId string) {
info, exists := c.Get(ReqInfoKey)
if exists {
parsedInfo := info.(RequestInfo)
parsedInfo.TraceId = traceId
c.Set(ReqInfoKey, parsedInfo)
return
}
c.Set(ReqInfoKey, RequestInfo{TraceId: traceId})
}
func SetUserId(c *gin.Context, userId int64) {
info, exists := c.Get(ReqInfoKey)
if exists {
parsedInfo := info.(RequestInfo)
parsedInfo.UserId = userId
c.Set(ReqInfoKey, parsedInfo)
return
}
c.Set(ReqInfoKey, RequestInfo{UserId: userId})
}
func SetUserContext(c *gin.Context, payload map[string]interface{}) {
c.Set(ReqInfoKey, RequestInfo{
UserId: int64(payload["userId"].(float64)),
Role: payload["role"].(string),
})
}
func ContextWithReqInfo(c *gin.Context) context.Context {
info, ok := c.Get(ReqInfoKey)
if ok {
return WithRequestInfo(c, info.(RequestInfo))
}
return WithRequestInfo(c, RequestInfo{})
}

View File

@ -1,40 +0,0 @@
package request
import "context"
type requestInfoKey int
const (
key requestInfoKey = iota
)
type RequestInfo struct {
UserId int64
TraceId string
Permissions map[string]bool
Role string
}
func WithRequestInfo(ctx context.Context, info RequestInfo) context.Context {
return context.WithValue(ctx, key, info)
}
func GetRequestInfo(ctx context.Context) (requestInfo RequestInfo, ok bool) {
requestInfo, ok = ctx.Value(key).(RequestInfo)
return
}
type reqInfoKeyType = string
const (
reqInfoKey reqInfoKeyType = "request-info"
)
func GetReqInfo(c context.Context) RequestInfo {
info := c.Value(reqInfoKey)
if info != nil {
return info.(RequestInfo)
}
return RequestInfo{}
}

Binary file not shown.

View File

@ -1,12 +0,0 @@
package branch
type BranchStatus string
const (
Active BranchStatus = "Active"
Inactive BranchStatus = "Inactive"
)
func (b BranchStatus) toString() string {
return string(b)
}

View File

@ -0,0 +1,63 @@
package constants
type BusinessType string
const (
BusinessTypeRestaurant BusinessType = "restaurant"
BusinessTypeRetail BusinessType = "retail"
BusinessTypeCafe BusinessType = "cafe"
BusinessTypeBar BusinessType = "bar"
)
type Currency string
const (
CurrencyUSD Currency = "USD"
CurrencyEUR Currency = "EUR"
CurrencyGBP Currency = "GBP"
CurrencyIDR Currency = "IDR"
)
const (
DefaultBusinessType = BusinessTypeRestaurant
DefaultCurrency = CurrencyUSD
DefaultTaxRate = 0.0
MaxNameLength = 255
MaxDescriptionLength = 1000
)
func GetAllBusinessTypes() []BusinessType {
return []BusinessType{
BusinessTypeRestaurant,
BusinessTypeRetail,
BusinessTypeCafe,
BusinessTypeBar,
}
}
func GetAllCurrencies() []Currency {
return []Currency{
CurrencyUSD,
CurrencyEUR,
CurrencyGBP,
CurrencyIDR,
}
}
func IsValidBusinessType(businessType BusinessType) bool {
for _, validType := range GetAllBusinessTypes() {
if businessType == validType {
return true
}
}
return false
}
func IsValidCurrency(currency Currency) bool {
for _, validCurrency := range GetAllCurrencies() {
if currency == validCurrency {
return true
}
}
return false
}

View File

@ -0,0 +1,17 @@
package constants
const (
RequestMethod = "RequestMethod"
RequestPath = "RequestPath"
RequestURLQueryParam = "RequestURLQueryParam"
ResponseStatusCode = "ResponseStatusCode"
ResponseStatusText = "ResponseStatusText"
ResponseTimeTaken = "ResponseTimeTaken"
)
var ValidCountryCodeMap = map[string]bool{
"ID": true,
"VI": true,
"SG": true,
"TH": true,
}

View File

@ -1,66 +0,0 @@
package constants
import (
"github.com/google/uuid"
"time"
)
const (
ContextRequestID string = "requestId"
)
type UserType string
func (u UserType) toString() string {
return string(u)
}
const (
StatusPending = "PENDING"
StatusPaid = "PAID"
StatusCanceled = "CANCELED"
StatusExpired = "EXPIRED"
StatusExecuted = "EXECUTED"
)
const (
PaymentCash = "CASH"
PaymentCreditCard = "CREDIT_CARD"
PaymentDebitCard = "DEBIT_CARD"
PaymentEWallet = "E_WALLET"
)
const (
SourcePOS = "POS"
SourceMobile = "MOBILE"
SourceWeb = "WEB"
)
const (
DefaultInquiryExpiryDuration = 30 * time.Minute
)
func GenerateUUID() string {
return uuid.New().String()
}
func GenerateRefID() string {
now := time.Now()
return now.Format("20060102") + "-" + uuid.New().String()[:8]
}
var TimeNow = func() time.Time {
return time.Now()
}
type RegistrationStatus string
const (
RegistrationSuccess RegistrationStatus = "SUCCESS"
RegistrationPending RegistrationStatus = "PENDING"
RegistrationFailed RegistrationStatus = "FAILED"
)
func (u RegistrationStatus) String() string {
return string(u)
}

View File

@ -1,12 +0,0 @@
package device
type DeviceStatus string
const (
On DeviceStatus = "On"
Off DeviceStatus = "Off"
)
func (b DeviceStatus) toString() string {
return string(b)
}

View File

@ -1,12 +0,0 @@
package device
type DeviceConnectionStatus string
const (
Connected DeviceConnectionStatus = "Connected"
Disconnected DeviceConnectionStatus = "Disconnected"
)
func (b DeviceConnectionStatus) toString() string {
return string(b)
}

View File

@ -0,0 +1,57 @@
package constants
import (
"fmt"
"net/http"
)
const (
InternalServerErrorCode = "900"
MissingFieldErrorCode = "303"
MalformedFieldErrorCode = "310"
ValidationErrorCode = "304"
InvalidFieldErrorCode = "305"
)
const (
RequestEntity = "request"
UserServiceEntity = "user_service"
OrganizationServiceEntity = "organization_service"
CategoryServiceEntity = "category_service"
ProductServiceEntity = "product_service"
ProductVariantServiceEntity = "product_variant_service"
InventoryServiceEntity = "inventory_service"
OrderServiceEntity = "order_service"
CustomerServiceEntity = "customer_service"
UserValidatorEntity = "user_validator"
AuthHandlerEntity = "auth_handler"
UserHandlerEntity = "user_handler"
CategoryHandlerEntity = "category_handler"
ProductHandlerEntity = "product_handler"
ProductVariantHandlerEntity = "product_variant_handler"
InventoryHandlerEntity = "inventory_handler"
OrderValidatorEntity = "order_validator"
OrderHandlerEntity = "order_handler"
OrganizationValidatorEntity = "organization_validator"
OrgHandlerEntity = "organization_handler"
PaymentMethodValidatorEntity = "payment_method_validator"
PaymentMethodHandlerEntity = "payment_method_handler"
OutletServiceEntity = "outlet_service"
)
var HttpErrorMap = map[string]int{
InternalServerErrorCode: http.StatusInternalServerError,
MissingFieldErrorCode: http.StatusBadRequest,
MalformedFieldErrorCode: http.StatusBadRequest,
ValidationErrorCode: http.StatusBadRequest,
InvalidFieldErrorCode: http.StatusBadRequest,
}
// Error messages
var (
ErrPaymentMethodNameRequired = fmt.Errorf("payment method name is required")
ErrPaymentMethodTypeRequired = fmt.Errorf("payment method type is required")
ErrInvalidPaymentMethodType = fmt.Errorf("invalid payment method type")
ErrInvalidPageNumber = fmt.Errorf("page number must be greater than 0")
ErrInvalidLimit = fmt.Errorf("limit must be between 1 and 100")
)

View File

@ -0,0 +1,80 @@
package constants
type FileType string
const (
FileTypeImage FileType = "image"
FileTypeDocument FileType = "document"
FileTypeVideo FileType = "video"
FileTypeAudio FileType = "audio"
FileTypeArchive FileType = "archive"
FileTypeOther FileType = "other"
)
func GetAllFileTypes() []FileType {
return []FileType{
FileTypeImage,
FileTypeDocument,
FileTypeVideo,
FileTypeAudio,
FileTypeArchive,
FileTypeOther,
}
}
func IsValidFileType(fileType FileType) bool {
for _, validType := range GetAllFileTypes() {
if fileType == validType {
return true
}
}
return false
}
// MIME type mappings
var MimeTypeToFileType = map[string]FileType{
// Images
"image/jpeg": FileTypeImage,
"image/jpg": FileTypeImage,
"image/png": FileTypeImage,
"image/gif": FileTypeImage,
"image/webp": FileTypeImage,
"image/svg+xml": FileTypeImage,
// Documents
"application/pdf": FileTypeDocument,
"application/msword": FileTypeDocument,
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": FileTypeDocument,
"application/vnd.ms-excel": FileTypeDocument,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": FileTypeDocument,
"text/plain": FileTypeDocument,
"text/csv": FileTypeDocument,
// Videos
"video/mp4": FileTypeVideo,
"video/avi": FileTypeVideo,
"video/mov": FileTypeVideo,
"video/wmv": FileTypeVideo,
"video/flv": FileTypeVideo,
"video/webm": FileTypeVideo,
// Audio
"audio/mpeg": FileTypeAudio,
"audio/mp3": FileTypeAudio,
"audio/wav": FileTypeAudio,
"audio/ogg": FileTypeAudio,
"audio/aac": FileTypeAudio,
// Archives
"application/zip": FileTypeArchive,
"application/x-rar-compressed": FileTypeArchive,
"application/x-7z-compressed": FileTypeArchive,
"application/gzip": FileTypeArchive,
}
func GetFileTypeFromMimeType(mimeType string) FileType {
if fileType, exists := MimeTypeToFileType[mimeType]; exists {
return fileType
}
return FileTypeOther
}

View File

@ -0,0 +1,27 @@
package constants
const (
CorrelationIDHeader = "debug-id"
XAppVersionHeader = "x-appversion"
XDeviceOSHeader = "X-DeviceOS"
XPlatformHeader = "X-Platform"
XAppTypeHeader = "X-AppType"
XAppIDHeader = "x-appid"
XPhoneModelHeader = "X-PhoneModel"
OrganizationID = "x_organization_id"
OutletID = "x_owner_id"
CountryCodeHeader = "country-code"
AcceptedLanguageHeader = "accept-language"
XUserLocaleHeader = "x-user-locale"
LocaleHeader = "locale"
GojekTimezoneHeader = "Gojek-Timezone"
UserTypeHeader = "User-Type"
AccountIDHeader = "Account-Id"
GopayUserType = "gopay"
XCorrelationIDHeader = "X-Correlation-Id"
XRequestIDHeader = "X-Request-Id"
XCountryCodeHeader = "X-Country-Code"
XAppVersionHeaderPOP = "X-App-Version"
XOwnerIDHeader = "X-Owner-Id"
XAppIDHeaderPOP = "X-App-Id"
)

View File

@ -0,0 +1,87 @@
package constants
type OrderType string
const (
OrderTypeDineIn OrderType = "dine_in"
OrderTypeTakeout OrderType = "takeout"
OrderTypeDelivery OrderType = "delivery"
)
type OrderStatus string
const (
OrderStatusPending OrderStatus = "pending"
OrderStatusPreparing OrderStatus = "preparing"
OrderStatusReady OrderStatus = "ready"
OrderStatusCompleted OrderStatus = "completed"
OrderStatusCancelled OrderStatus = "cancelled"
OrderStatusPaid OrderStatus = "paid"
)
type OrderItemStatus string
const (
OrderItemStatusPending OrderItemStatus = "pending"
OrderItemStatusPreparing OrderItemStatus = "preparing"
OrderItemStatusReady OrderItemStatus = "ready"
OrderItemStatusServed OrderItemStatus = "served"
OrderItemStatusCancelled OrderItemStatus = "cancelled"
OrderItemStatusCompleted OrderItemStatus = "completed"
)
func GetAllOrderTypes() []OrderType {
return []OrderType{
OrderTypeDineIn,
OrderTypeTakeout,
OrderTypeDelivery,
}
}
func GetAllOrderStatuses() []OrderStatus {
return []OrderStatus{
OrderStatusPending,
OrderStatusPreparing,
OrderStatusReady,
OrderStatusCompleted,
OrderStatusCancelled,
OrderStatusPaid,
}
}
func GetAllOrderItemStatuses() []OrderItemStatus {
return []OrderItemStatus{
OrderItemStatusPending,
OrderItemStatusPreparing,
OrderItemStatusReady,
OrderItemStatusServed,
OrderItemStatusCancelled,
}
}
func (o OrderType) IsValidOrderType() bool {
for _, validType := range GetAllOrderTypes() {
if o == validType {
return true
}
}
return false
}
func IsValidOrderStatus(status OrderStatus) bool {
for _, validStatus := range GetAllOrderStatuses() {
if status == validStatus {
return true
}
}
return false
}
func IsValidOrderItemStatus(status OrderItemStatus) bool {
for _, validStatus := range GetAllOrderItemStatuses() {
if status == validStatus {
return true
}
}
return false
}

View File

@ -1,84 +0,0 @@
package order
type OrderStatus string
const (
New OrderStatus = "NEW"
Paid OrderStatus = "PAID"
Cancel OrderStatus = "CANCEL"
Pending OrderStatus = "PENDING"
)
func (b OrderStatus) toString() string {
return string(b)
}
func (i *OrderStatus) IsNew() bool {
if i == nil {
return false
}
if *i == New {
return true
}
return false
}
func (i OrderStatus) String() string {
return string(i)
}
type ItemType string
const (
Product ItemType = "PRODUCT"
Studio ItemType = "STUDIO"
)
func (b ItemType) toString() string {
return string(b)
}
func (i *ItemType) IsProduct() bool {
if i == nil {
return false
}
if *i == Product {
return true
}
return false
}
func (i *ItemType) IsStudio() bool {
if i == nil {
return false
}
if *i == Studio {
return true
}
return false
}
type OrderSearchStatus string
const (
Active OrderSearchStatus = "ACTIVE"
Inactive OrderSearchStatus = "INACTIVE"
)
func (i *OrderSearchStatus) IsActive() bool {
if i == nil {
return false
}
if *i == Active {
return true
}
return false
}

View File

@ -0,0 +1,26 @@
package constants
type PlanType string
const (
PlanBasic PlanType = "basic"
PlanPremium PlanType = "premium"
PlanEnterprise PlanType = "enterprise"
)
func GetAllPlanTypes() []PlanType {
return []PlanType{
PlanBasic,
PlanPremium,
PlanEnterprise,
}
}
func IsValidPlanType(planType PlanType) bool {
for _, validType := range GetAllPlanTypes() {
if planType == validType {
return true
}
}
return false
}

View File

@ -1,9 +0,0 @@
package constants
const (
OssLogLevelLogOff = "LogOff"
OssLogLevelDebug = "Debug"
OssLogLevelError = "Error"
OssLogLevelWarn = "Warn"
OssLogLevelInfo = "Info"
)

View File

@ -0,0 +1,27 @@
package constants
// Outlet printer setting keys
const (
PRINTER_OUTLET_NAME = "printer_outlet_name"
PRINTER_ADDRESS = "printer_address"
PRINTER_PHONE_NUMBER = "printer_phone_number"
PRINTER_PAPER_SIZE = "printer_paper_size"
PRINTER_FOOTER = "printer_footer"
PRINTER_FOOTER_HASHTAG = "printer_footer_hashtag"
)
// Default printer settings
const (
DEFAULT_PAPER_SIZE = "80mm"
DEFAULT_FOOTER = "Thank you for your purchase!"
DEFAULT_FOOTER_HASHTAG = "#ThankYou"
)
// Valid paper sizes
var ValidPaperSizes = []string{
"58mm",
"80mm",
"A4",
"A5",
"Letter",
}

View File

@ -0,0 +1,82 @@
package constants
type PaymentMethodType string
const (
PaymentMethodTypeCash PaymentMethodType = "cash"
PaymentMethodTypeCard PaymentMethodType = "card"
PaymentMethodTypeDigitalWallet PaymentMethodType = "digital_wallet"
PaymentMethodTypeQR PaymentMethodType = "qr"
PaymentMethodTypeEDC PaymentMethodType = "edc"
)
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "pending"
PaymentStatusCompleted PaymentStatus = "completed"
PaymentStatusFailed PaymentStatus = "failed"
PaymentStatusRefunded PaymentStatus = "refunded"
)
type PaymentTransactionStatus string
const (
PaymentTransactionStatusPending PaymentTransactionStatus = "pending"
PaymentTransactionStatusCompleted PaymentTransactionStatus = "completed"
PaymentTransactionStatusFailed PaymentTransactionStatus = "failed"
PaymentTransactionStatusRefunded PaymentTransactionStatus = "refunded"
)
func GetAllPaymentMethodTypes() []PaymentMethodType {
return []PaymentMethodType{
PaymentMethodTypeCash,
PaymentMethodTypeCard,
PaymentMethodTypeDigitalWallet,
}
}
func GetAllPaymentStatuses() []PaymentStatus {
return []PaymentStatus{
PaymentStatusPending,
PaymentStatusCompleted,
PaymentStatusFailed,
PaymentStatusRefunded,
}
}
func GetAllPaymentTransactionStatuses() []PaymentTransactionStatus {
return []PaymentTransactionStatus{
PaymentTransactionStatusPending,
PaymentTransactionStatusCompleted,
PaymentTransactionStatusFailed,
PaymentTransactionStatusRefunded,
}
}
func IsValidPaymentMethodType(methodType PaymentMethodType) bool {
for _, validType := range GetAllPaymentMethodTypes() {
if methodType == validType {
return true
}
}
return false
}
func IsValidPaymentStatus(status PaymentStatus) bool {
for _, validStatus := range GetAllPaymentStatuses() {
if status == validStatus {
return true
}
}
return false
}
func IsValidPaymentTransactionStatus(status PaymentTransactionStatus) bool {
for _, validStatus := range GetAllPaymentTransactionStatuses() {
if status == validStatus {
return true
}
}
return false
}

View File

@ -1,58 +0,0 @@
package product
type ProductStatus string
const (
Active ProductStatus = "Active"
Inactive ProductStatus = "Inactive"
)
func (b ProductStatus) toString() string {
return string(b)
}
type ProductType string
const (
Food ProductType = "FOOD"
Beverage ProductType = "BEVERAGE"
)
func (b ProductType) toString() string {
return string(b)
}
type ProductStock string
const (
Available ProductStock = "AVAILABLE"
Unavailable ProductStock = "UNAVAILABLE"
)
func (b ProductStock) toString() string {
return string(b)
}
func (i *ProductStock) IsAvailable() bool {
if i == nil {
return false
}
if *i == Available {
return true
}
return false
}
func (i *ProductStock) IsUnavailable() bool {
if i == nil {
return false
}
if *i == Unavailable {
return true
}
return false
}

View File

@ -1,12 +0,0 @@
package role
type Role int64
const (
SuperAdmin Role = 1
Admin Role = 2
PartnerAdmin Role = 3
SiteAdmin Role = 4
Casheer Role = 5
Customer Role = 6
)

View File

@ -1,12 +0,0 @@
package studio
type StudioStatus string
const (
Active StudioStatus = "active"
Inactive StudioStatus = "inactive"
)
func (b StudioStatus) toString() string {
return string(b)
}

View File

@ -0,0 +1,30 @@
package constants
type TableStatus string
const (
TableStatusAvailable TableStatus = "available"
TableStatusOccupied TableStatus = "occupied"
TableStatusReserved TableStatus = "reserved"
TableStatusCleaning TableStatus = "cleaning"
TableStatusMaintenance TableStatus = "maintenance"
)
func GetAllTableStatuses() []TableStatus {
return []TableStatus{
TableStatusAvailable,
TableStatusOccupied,
TableStatusReserved,
TableStatusCleaning,
TableStatusMaintenance,
}
}
func IsValidTableStatus(status TableStatus) bool {
for _, validStatus := range GetAllTableStatuses() {
if status == validStatus {
return true
}
}
return false
}

View File

@ -1,28 +0,0 @@
package transaction
type PaymentStatus string
const (
New PaymentStatus = "NEW"
Paid PaymentStatus = "PAID"
Cancel PaymentStatus = "CANCEL"
)
func (b PaymentStatus) toString() string {
return string(b)
}
type PaymentMethod string
const (
Cash PaymentMethod = "CASH"
Debit PaymentMethod = "DEBIT"
Transfer PaymentMethod = "TRANSFER"
QRIS PaymentMethod = "QRIS"
Online PaymentMethod = "ONLINE"
VA PaymentMethod = "VA"
)
func (b PaymentMethod) toString() string {
return string(b)
}

View File

@ -0,0 +1,28 @@
package constants
type UserRole string
const (
RoleAdmin UserRole = "admin"
RoleManager UserRole = "manager"
RoleCashier UserRole = "cashier"
RoleWaiter UserRole = "waiter"
)
func GetAllUserRoles() []UserRole {
return []UserRole{
RoleAdmin,
RoleManager,
RoleCashier,
RoleWaiter,
}
}
func IsValidUserRole(role UserRole) bool {
for _, validRole := range GetAllUserRoles() {
if role == validRole {
return true
}
}
return false
}

View File

@ -1,12 +0,0 @@
package userstatus
type UserStatus string
const (
Active UserStatus = "Active"
Inactive UserStatus = "Inactive"
)
func (u UserStatus) toString() string {
return string(u)
}

View File

@ -0,0 +1,210 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type PaymentMethodAnalyticsRequest struct {
OrganizationID uuid.UUID `form:"organization_id"`
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
}
// PaymentMethodAnalyticsResponse represents the response for payment method analytics
type PaymentMethodAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"`
Summary PaymentMethodSummary `json:"summary"`
Data []PaymentMethodAnalyticsData `json:"data"`
}
// PaymentMethodSummary represents the summary of payment method analytics
type PaymentMethodSummary struct {
TotalAmount float64 `json:"total_amount"`
TotalOrders int64 `json:"total_orders"`
TotalPayments int64 `json:"total_payments"`
AverageOrderValue float64 `json:"average_order_value"`
}
type PaymentMethodAnalyticsData struct {
PaymentMethodID uuid.UUID `json:"payment_method_id"`
PaymentMethodName string `json:"payment_method_name"`
PaymentMethodType string `json:"payment_method_type"`
TotalAmount float64 `json:"total_amount"`
OrderCount int64 `json:"order_count"`
PaymentCount int64 `json:"payment_count"`
Percentage float64 `json:"percentage"`
}
type SalesAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
}
type SalesAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"`
Summary SalesSummary `json:"summary"`
Data []SalesAnalyticsData `json:"data"`
}
// SalesSummary represents the summary of sales analytics
type SalesSummary struct {
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
TotalItems int64 `json:"total_items"`
AverageOrderValue float64 `json:"average_order_value"`
TotalTax float64 `json:"total_tax"`
TotalDiscount float64 `json:"total_discount"`
NetSales float64 `json:"net_sales"`
}
// SalesAnalyticsData represents individual sales analytics data point
type SalesAnalyticsData struct {
Date time.Time `json:"date"`
Sales float64 `json:"sales"`
Orders int64 `json:"orders"`
Items int64 `json:"items"`
Tax float64 `json:"tax"`
Discount float64 `json:"discount"`
NetSales float64 `json:"net_sales"`
}
// ProductAnalyticsRequest represents the request for product analytics
type ProductAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
Limit int `form:"limit,default=10" validate:"min=1,max=100"`
}
// ProductAnalyticsResponse represents the response for product analytics
type ProductAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"`
Data []ProductAnalyticsData `json:"data"`
}
// ProductAnalyticsData represents individual product analytics data
type ProductAnalyticsData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
QuantitySold int64 `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
AveragePrice float64 `json:"average_price"`
OrderCount int64 `json:"order_count"`
}
// DashboardAnalyticsRequest represents the request for dashboard analytics
type DashboardAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
}
// DashboardAnalyticsResponse represents the response for dashboard analytics
type DashboardAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"`
Overview DashboardOverview `json:"overview"`
TopProducts []ProductAnalyticsData `json:"top_products"`
PaymentMethods []PaymentMethodAnalyticsData `json:"payment_methods"`
RecentSales []SalesAnalyticsData `json:"recent_sales"`
}
// DashboardOverview represents the overview data for dashboard
type DashboardOverview struct {
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_orders"`
RefundedOrders int64 `json:"refunded_orders"`
}
// ProfitLossAnalyticsRequest represents the request for profit and loss analytics
type ProfitLossAnalyticsRequest struct {
OrganizationID uuid.UUID
OutletID *uuid.UUID `form:"outlet_id,omitempty"`
DateFrom string `form:"date_from" validate:"required"`
DateTo string `form:"date_to" validate:"required"`
GroupBy string `form:"group_by,default=day" validate:"omitempty,oneof=day hour week month"`
}
// ProfitLossAnalyticsResponse represents the response for profit and loss analytics
type ProfitLossAnalyticsResponse struct {
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
DateFrom time.Time `json:"date_from"`
DateTo time.Time `json:"date_to"`
GroupBy string `json:"group_by"`
Summary ProfitLossSummary `json:"summary"`
Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"`
}
// ProfitLossSummary represents the summary of profit and loss analytics
type ProfitLossSummary struct {
TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
TotalTax float64 `json:"total_tax"`
TotalDiscount float64 `json:"total_discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
TotalOrders int64 `json:"total_orders"`
AverageProfit float64 `json:"average_profit"`
ProfitabilityRatio float64 `json:"profitability_ratio"`
}
// ProfitLossData represents individual profit and loss data point by time period
type ProfitLossData struct {
Date time.Time `json:"date"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
Tax float64 `json:"tax"`
Discount float64 `json:"discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
Orders int64 `json:"orders"`
}
// ProductProfitData represents profit data for individual products
type ProductProfitData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
QuantitySold int64 `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
AveragePrice float64 `json:"average_price"`
AverageCost float64 `json:"average_cost"`
ProfitPerUnit float64 `json:"profit_per_unit"`
}

View File

@ -0,0 +1,49 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateCategoryRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description *string `json:"description,omitempty"`
BusinessType *string `json:"business_type,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateCategoryRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty"`
BusinessType *string `json:"business_type,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type ListCategoriesRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
BusinessType string `json:"business_type,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"`
Limit int `json:"limit" validate:"required,min=1,max=100"`
}
// Category Response DTOs
type CategoryResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Description *string `json:"description"`
BusinessType string `json:"business_type"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListCategoriesResponse struct {
Categories []CategoryResponse `json:"categories"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,49 @@
package contract
import "time"
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
Code int `json:"code"`
}
type ValidationErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Details map[string]string `json:"details"`
Code int `json:"code"`
}
type SuccessResponse struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
type PaginationRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
}
type PaginationResponse struct {
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
type SearchRequest struct {
Query string `json:"query,omitempty"`
}
type DateRangeRequest struct {
From *time.Time `json:"from,omitempty"`
To *time.Time `json:"to,omitempty"`
}
type HealthResponse struct {
Status string `json:"status"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
}

View File

@ -0,0 +1,58 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateCustomerRequest struct {
Name string `json:"name" validate:"required"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Phone *string `json:"phone,omitempty"`
Address *string `json:"address,omitempty"`
}
type UpdateCustomerRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,required"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Phone *string `json:"phone,omitempty"`
Address *string `json:"address,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
type CustomerResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Email *string `json:"email,omitempty"`
Phone *string `json:"phone,omitempty"`
Address *string `json:"address,omitempty"`
IsDefault bool `json:"is_default"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListCustomersRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search"`
IsActive *bool `json:"is_active"`
IsDefault *bool `json:"is_default"`
SortBy string `json:"sort_by" validate:"omitempty,oneof=name email created_at updated_at"`
SortOrder string `json:"sort_order" validate:"omitempty,oneof=asc desc"`
}
type SetDefaultCustomerRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
}
type PaginatedCustomerResponse struct {
Data []CustomerResponse `json:"data"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,73 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type ListFilesQuery struct {
OrganizationID string `form:"organization_id"`
UserID string `form:"user_id"`
FileType string `form:"file_type"`
IsPublic string `form:"is_public"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
Search string `form:"search"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=10"`
}
// Request DTOs
type UploadFileRequest struct {
FileType string `json:"file_type" validate:"required"`
IsPublic *bool `json:"is_public,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateFileRequest struct {
IsPublic *bool `json:"is_public,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type ListFilesRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
UserID *uuid.UUID `json:"user_id,omitempty"`
FileType *string `json:"file_type,omitempty"`
IsPublic *bool `json:"is_public,omitempty"`
DateFrom *time.Time `json:"date_from,omitempty"`
DateTo *time.Time `json:"date_to,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"`
Limit int `json:"limit" validate:"required,min=1,max=100"`
}
// Response DTOs
type FileResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
UserID uuid.UUID `json:"user_id"`
FileName string `json:"file_name"`
OriginalName string `json:"original_name"`
FileURL string `json:"file_url"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type"`
FileType string `json:"file_type"`
UploadPath string `json:"upload_path"`
IsPublic bool `json:"is_public"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListFilesResponse struct {
Files []*FileResponse `json:"files"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
type UploadFileResponse struct {
File FileResponse `json:"file"`
}

View File

@ -0,0 +1,72 @@
package contract
import (
"time"
"github.com/google/uuid"
)
// Inventory Request DTOs
type CreateInventoryRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
ProductID uuid.UUID `json:"product_id" validate:"required"`
Quantity int `json:"quantity" validate:"min=0"`
ReorderLevel int `json:"reorder_level" validate:"min=0"`
}
type UpdateInventoryRequest struct {
Quantity *int `json:"quantity,omitempty" validate:"omitempty,min=0"`
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
}
type AdjustInventoryRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
Delta int `json:"delta" validate:"required"` // Can be positive or negative
Reason string `json:"reason" validate:"required,min=1,max=255"`
}
type ListInventoryRequest struct {
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
ProductID *uuid.UUID `json:"product_id,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
LowStockOnly *bool `json:"low_stock_only,omitempty"`
ZeroStockOnly *bool `json:"zero_stock_only,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"required,min=1"`
Limit int `json:"limit" validate:"required,min=1,max=100"`
}
// Inventory Response DTOs
type InventoryResponse struct {
ID uuid.UUID `json:"id"`
OutletID uuid.UUID `json:"outlet_id"`
ProductID uuid.UUID `json:"product_id"`
Quantity int `json:"quantity"`
ReorderLevel int `json:"reorder_level"`
IsLowStock bool `json:"is_low_stock"`
UpdatedAt time.Time `json:"updated_at"`
// Related data (optional)
Product *ProductResponse `json:"product,omitempty"`
Outlet *OutletResponse `json:"outlet,omitempty"`
}
type ListInventoryResponse struct {
Inventory []InventoryResponse `json:"inventory"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
type InventoryAdjustmentResponse struct {
InventoryID uuid.UUID `json:"inventory_id"`
ProductID uuid.UUID `json:"product_id"`
OutletID uuid.UUID `json:"outlet_id"`
PreviousQty int `json:"previous_quantity"`
NewQty int `json:"new_quantity"`
Delta int `json:"delta"`
Reason string `json:"reason"`
AdjustedAt time.Time `json:"adjusted_at"`
}

View File

@ -0,0 +1,215 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateOrderRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
UserID uuid.UUID `json:"user_id" validate:"required"`
TableNumber *string `json:"table_number,omitempty" validate:"omitempty,max=50"`
OrderType string `json:"order_type" validate:"required,oneof=dine_in takeaway delivery"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`
OrderItems []CreateOrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
CustomerName *string `json:"customer_name,omitempty" validate:"omitempty,max=255"`
}
type AddToOrderRequest struct {
OrderItems []CreateOrderItemRequest `json:"order_items" validate:"required,min=1,dive"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type AddToOrderResponse struct {
OrderID uuid.UUID `json:"order_id"`
OrderNumber string `json:"order_number"`
AddedItems []OrderItemResponse `json:"added_items"`
UpdatedOrder OrderResponse `json:"updated_order"`
}
type UpdateOrderRequest struct {
TableNumber *string `json:"table_number,omitempty" validate:"omitempty,max=50"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending preparing ready completed cancelled"`
DiscountAmount *float64 `json:"discount_amount,omitempty" validate:"omitempty,min=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=1000"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type CreateOrderItemRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
ProductVariantID *uuid.UUID `json:"product_variant_id,omitempty"`
Quantity int `json:"quantity" validate:"required,min=1"`
UnitPrice *float64 `json:"unit_price,omitempty" validate:"omitempty,min=0"` // Optional, will use database price if not provided
Modifiers []map[string]interface{} `json:"modifiers,omitempty"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateOrderItemRequest struct {
Quantity *int `json:"quantity,omitempty" validate:"omitempty,min=1"`
UnitPrice *float64 `json:"unit_price,omitempty" validate:"omitempty,min=0"`
Modifiers []map[string]interface{} `json:"modifiers,omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending preparing completed cancelled"`
}
type OrderResponse struct {
ID uuid.UUID `json:"id"`
OrderNumber string `json:"order_number"`
OutletID uuid.UUID `json:"outlet_id"`
UserID uuid.UUID `json:"user_id"`
TableNumber *string `json:"table_number"`
OrderType string `json:"order_type"`
Status string `json:"status"`
Subtotal float64 `json:"subtotal"`
TaxAmount float64 `json:"tax_amount"`
DiscountAmount float64 `json:"discount_amount"`
TotalAmount float64 `json:"total_amount"`
Notes *string `json:"notes"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
OrderItems []OrderItemResponse `json:"order_items,omitempty"`
}
type OrderItemResponse struct {
ID uuid.UUID `json:"id"`
OrderID uuid.UUID `json:"order_id"`
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
ProductVariantID *uuid.UUID `json:"product_variant_id"`
ProductVariantName *string `json:"product_variant_name,omitempty"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
Modifiers []map[string]interface{} `json:"modifiers"`
Notes *string `json:"notes,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListOrdersQuery struct {
OrganizationID string `form:"organization_id"`
OutletID string `form:"outlet_id"`
UserID string `form:"user_id"`
CustomerID string `form:"customer_id"`
OrderType string `form:"order_type"`
Status string `form:"status"`
PaymentStatus string `form:"payment_status"`
IsVoid string `form:"is_void"`
IsRefund string `form:"is_refund"`
DateFrom string `form:"date_from"`
DateTo string `form:"date_to"`
Search string `form:"search"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=10"`
}
type ListOrdersRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
UserID *uuid.UUID `json:"user_id,omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending preparing ready completed cancelled"`
OrderType *string `json:"order_type,omitempty" validate:"omitempty,oneof=dine_in takeaway delivery"`
DateFrom *time.Time `json:"date_from,omitempty"`
DateTo *time.Time `json:"date_to,omitempty"`
}
type ListOrdersResponse struct {
Orders []OrderResponse `json:"orders"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
type VoidOrderRequest struct {
OrderID uuid.UUID `json:"order_id" validate:"required"`
Reason string `json:"reason" validate:"required"`
Type string `json:"type" validate:"required,oneof=ALL ITEM"`
Items []VoidItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
}
type VoidItemRequest struct {
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
Quantity int `json:"quantity" validate:"required,min=1"`
}
type SetOrderCustomerRequest struct {
CustomerID uuid.UUID `json:"customer_id" validate:"required"`
}
type SetOrderCustomerResponse struct {
OrderID uuid.UUID `json:"order_id"`
CustomerID uuid.UUID `json:"customer_id"`
Message string `json:"message"`
}
// Payment-related contracts
type CreatePaymentRequest struct {
OrderID uuid.UUID `json:"order_id" validate:"required"`
PaymentMethodID uuid.UUID `json:"payment_method_id" validate:"required"`
Amount float64 `json:"amount" validate:"required,min=0"`
TransactionID *string `json:"transaction_id,omitempty" validate:"omitempty"`
SplitNumber int `json:"split_number,omitempty" validate:"omitempty,min=1"`
SplitTotal int `json:"split_total,omitempty" validate:"omitempty,min=1"`
SplitDescription *string `json:"split_description,omitempty" validate:"omitempty,max=255"`
PaymentOrderItems []CreatePaymentOrderItemRequest `json:"payment_order_items,omitempty" validate:"omitempty,dive"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type CreatePaymentOrderItemRequest struct {
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
Amount float64 `json:"amount" validate:"required,min=0"`
}
type PaymentResponse struct {
ID uuid.UUID `json:"id"`
OrderID uuid.UUID `json:"order_id"`
PaymentMethodID uuid.UUID `json:"payment_method_id"`
Amount float64 `json:"amount"`
Status string `json:"status"`
TransactionID *string `json:"transaction_id,omitempty"`
SplitNumber int `json:"split_number"`
SplitTotal int `json:"split_total"`
SplitDescription *string `json:"split_description,omitempty"`
RefundAmount float64 `json:"refund_amount"`
RefundReason *string `json:"refund_reason,omitempty"`
RefundedAt *time.Time `json:"refunded_at,omitempty"`
RefundedBy *uuid.UUID `json:"refunded_by,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PaymentOrderItems []PaymentOrderItemResponse `json:"payment_order_items,omitempty"`
}
type PaymentOrderItemResponse struct {
ID uuid.UUID `json:"id"`
PaymentID uuid.UUID `json:"payment_id"`
OrderItemID uuid.UUID `json:"order_item_id"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RefundOrderRequest struct {
Reason *string `json:"reason,omitempty" validate:"omitempty,max=255"`
RefundAmount *float64 `json:"refund_amount,omitempty" validate:"omitempty,min=0"`
OrderItems []RefundOrderItemRequest `json:"order_items,omitempty" validate:"omitempty,dive"`
}
type RefundOrderItemRequest struct {
OrderItemID uuid.UUID `json:"order_item_id" validate:"required"`
RefundQuantity int `json:"refund_quantity,omitempty" validate:"omitempty,min=1"`
RefundAmount *float64 `json:"refund_amount,omitempty" validate:"omitempty,min=0"`
Reason *string `json:"reason,omitempty" validate:"omitempty,max=255"`
}
type RefundPaymentRequest struct {
RefundAmount float64 `json:"refund_amount" validate:"required,min=0"`
Reason string `json:"reason" validate:"omitempty,max=255"`
}

View File

@ -0,0 +1,61 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateOrganizationRequest struct {
OrganizationName string `json:"organization_name" validate:"required,min=1,max=255"`
OrganizationEmail *string `json:"organization_email,omitempty" validate:"omitempty,email"`
OrganizationPhoneNumber *string `json:"organization_phone_number,omitempty"`
PlanType string `json:"plan_type" validate:"required,oneof=basic premium enterprise"`
AdminName string `json:"admin_name" validate:"required,min=1,max=255"`
AdminEmail string `json:"admin_email" validate:"required,email"`
AdminPassword string `json:"admin_password" validate:"required,min=6"`
OutletName string `json:"outlet_name" validate:"required,min=1,max=255"`
OutletAddress *string `json:"outlet_address,omitempty"`
OutletTimezone *string `json:"outlet_timezone,omitempty"`
OutletCurrency string `json:"outlet_currency" validate:"required,len=3"`
}
type UpdateOrganizationRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
PhoneNumber *string `json:"phone_number,omitempty"`
PlanType *string `json:"plan_type,omitempty" validate:"omitempty,oneof=basic premium enterprise"`
}
type OrganizationResponse struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Email *string `json:"email"`
PhoneNumber *string `json:"phone_number"`
PlanType string `json:"plan_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateOrganizationResponse struct {
Organization OrganizationResponse `json:"organization"`
AdminUser UserResponse `json:"admin_user"`
DefaultOutlet OutletResponse `json:"default_outlet"`
}
type ListOrganizationsRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search,omitempty"`
PlanType string `json:"plan_type,omitempty" validate:"omitempty,oneof=basic premium enterprise"`
}
type ListOrganizationsResponse struct {
Organizations []OrganizationResponse `json:"organizations"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,57 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateOutletRequest struct {
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
Address string `json:"address" validate:"required,min=1,max=500"`
PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,e164"`
BusinessType string `json:"business_type" validate:"required,oneof=restaurant cafe bar fastfood retail"`
Currency string `json:"currency" validate:"required,len=3"`
TaxRate float64 `json:"tax_rate" validate:"min=0,max=1"`
}
type UpdateOutletRequest struct {
OrganizationID uuid.UUID
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Address *string `json:"address,omitempty" validate:"omitempty,min=1,max=500"`
PhoneNumber *string `json:"phone_number,omitempty" validate:"omitempty,e164"`
TaxRate *float64 `json:"tax_rate,omitempty" validate:"omitempty,min=0,max=1"`
IsActive *bool `json:"is_active,omitempty"`
}
type OutletResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Address string `json:"address"`
PhoneNumber *string `json:"phone_number"`
BusinessType string `json:"business_type"`
Currency string `json:"currency"`
TaxRate float64 `json:"tax_rate"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListOutletsRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search,omitempty"`
OrganizationID uuid.UUID `json:"organization_id,omitempty"`
BusinessType *string `json:"business_type,omitempty" validate:"omitempty,oneof=restaurant cafe bar fastfood retail"`
IsActive *bool `json:"is_active,omitempty"`
}
type ListOutletsResponse struct {
Outlets []OutletResponse `json:"outlets"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,53 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreatePaymentMethodRequest struct {
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=100"`
Type string `json:"type" validate:"required,oneof=cash card digital_wallet qr edc"`
Processor *string `json:"processor,omitempty" validate:"omitempty,max=100"`
Configuration map[string]interface{} `json:"configuration,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
type UpdatePaymentMethodRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=100"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc"`
Processor *string `json:"processor,omitempty" validate:"omitempty,max=100"`
Configuration map[string]interface{} `json:"configuration,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
}
type PaymentMethodResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Name string `json:"name"`
Type string `json:"type"`
Processor *string `json:"processor,omitempty"`
Configuration map[string]interface{} `json:"configuration,omitempty"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListPaymentMethodsRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,oneof=cash card digital_wallet qr edc"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
}
type ListPaymentMethodsResponse struct {
PaymentMethods []PaymentMethodResponse `json:"payment_methods"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,107 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateProductRequest struct {
CategoryID uuid.UUID `json:"category_id" validate:"required"`
SKU *string `json:"sku,omitempty"`
Name string `json:"name" validate:"required,min=1,max=255"`
Description *string `json:"description,omitempty"`
Price float64 `json:"price" validate:"required,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
InitialStock *int `json:"initial_stock,omitempty" validate:"omitempty,min=0"` // Initial stock quantity for all outlets
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Reorder level for all outlets
CreateInventory bool `json:"create_inventory,omitempty"` // Whether to create inventory records for all outlets
}
type UpdateProductRequest struct {
CategoryID *uuid.UUID `json:"category_id,omitempty"`
SKU *string `json:"sku,omitempty"`
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Description *string `json:"description,omitempty"`
Price *float64 `json:"price,omitempty" validate:"omitempty,min=0"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
// Stock management fields
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"` // Update reorder level for all existing inventory records
}
type CreateProductVariantRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
Name string `json:"name" validate:"required,min=1,max=255"`
PriceModifier float64 `json:"price_modifier" validate:"required"`
Cost float64 `json:"cost" validate:"min=0"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateProductVariantRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
PriceModifier *float64 `json:"price_modifier,omitempty"`
Cost *float64 `json:"cost,omitempty" validate:"omitempty,min=0"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type ProductResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
CategoryID uuid.UUID `json:"category_id"`
SKU *string `json:"sku"`
Name string `json:"name"`
Description *string `json:"description"`
Price float64 `json:"price"`
Cost float64 `json:"cost"`
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Category *CategoryResponse `json:"category,omitempty"`
Variants []ProductVariantResponse `json:"variants,omitempty"`
}
type ProductVariantResponse struct {
ID uuid.UUID `json:"id"`
ProductID uuid.UUID `json:"product_id"`
Name string `json:"name"`
PriceModifier float64 `json:"price_modifier"`
Cost float64 `json:"cost"`
Metadata map[string]interface{} `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListProductsRequest struct {
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
BusinessType string `json:"business_type,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
MinPrice *float64 `json:"min_price,omitempty" validate:"omitempty,min=0"`
MaxPrice *float64 `json:"max_price,omitempty" validate:"omitempty,min=0"`
Page int `json:"page" validate:"required,min=1"`
Limit int `json:"limit" validate:"required,min=1,max=100"`
}
type ListProductsResponse struct {
Products []ProductResponse `json:"products"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,39 @@
package contract
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data"`
Errors []*ResponseError `json:"errors"`
}
func (r *Response) GetSuccess() bool {
return r.Success
}
func (r *Response) GetData() interface{} {
return r.Data
}
func (r *Response) GetErrors() []*ResponseError {
return r.Errors
}
func BuildSuccessResponse(data interface{}) *Response {
return &Response{
Success: true,
Data: data,
Errors: []*ResponseError(nil),
}
}
func BuildErrorResponse(errorList []*ResponseError) *Response {
return &Response{
Success: false,
Data: nil,
Errors: errorList,
}
}
func (r *Response) HasErrors() bool {
return r.GetErrors() != nil && len(r.GetErrors()) > 0
}

View File

@ -0,0 +1,33 @@
package contract
import "fmt"
type ResponseError struct {
Code string `json:"code"`
Entity string `json:"entity"`
Cause string `json:"cause"`
}
func NewResponseError(code, entity, cause string) *ResponseError {
return &ResponseError{
Code: code,
Cause: cause,
Entity: entity,
}
}
func (e *ResponseError) GetCode() string {
return e.Code
}
func (e *ResponseError) GetEntity() string {
return e.Entity
}
func (e *ResponseError) GetCause() string {
return e.Cause
}
func (e *ResponseError) Error() string {
return fmt.Sprintf("%s: %s: %s", e.GetCode(), e.GetEntity(), e.GetCause())
}

View File

@ -0,0 +1,82 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateTableRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
TableName string `json:"table_name" validate:"required,max=100"`
PositionX float64 `json:"position_x" validate:"required"`
PositionY float64 `json:"position_y" validate:"required"`
Capacity int `json:"capacity" validate:"required,min=1,max=20"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type UpdateTableRequest struct {
TableName *string `json:"table_name,omitempty" validate:"omitempty,max=100"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=available occupied reserved cleaning maintenance"`
PositionX *float64 `json:"position_x,omitempty"`
PositionY *float64 `json:"position_y,omitempty"`
Capacity *int `json:"capacity,omitempty" validate:"omitempty,min=1,max=20"`
IsActive *bool `json:"is_active,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
type OccupyTableRequest struct {
OrderID uuid.UUID `json:"order_id" validate:"required"`
StartTime time.Time `json:"start_time" validate:"required"`
}
type ReleaseTableRequest struct {
PaymentAmount float64 `json:"payment_amount" validate:"required,min=0"`
}
type TableResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id"`
TableName string `json:"table_name"`
StartTime *time.Time `json:"start_time,omitempty"`
Status string `json:"status"`
OrderID *uuid.UUID `json:"order_id,omitempty"`
PaymentAmount float64 `json:"payment_amount"`
PositionX float64 `json:"position_x"`
PositionY float64 `json:"position_y"`
Capacity int `json:"capacity"`
IsActive bool `json:"is_active"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Order *OrderResponse `json:"order,omitempty"`
}
type ListTablesQuery struct {
OrganizationID string `form:"organization_id"`
OutletID string `form:"outlet_id"`
Status string `form:"status"`
IsActive string `form:"is_active"`
Search string `form:"search"`
Page int `form:"page,default=1"`
Limit int `form:"limit,default=10"`
}
type ListTablesRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,oneof=available occupied reserved cleaning maintenance"`
IsActive *bool `json:"is_active,omitempty"`
Search string `json:"search,omitempty"`
}
type ListTablesResponse struct {
Tables []TableResponse `json:"tables"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}

View File

@ -0,0 +1,73 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateUserRequest struct {
OrganizationID uuid.UUID `json:"organization_id" validate:"required"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
Name string `json:"name" validate:"required,min=1,max=255"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
Role string `json:"role" validate:"required,oneof=admin manager cashier waiter"`
Permissions map[string]interface{} `json:"permissions,omitempty"`
}
type UpdateUserRequest struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
Email *string `json:"email,omitempty" validate:"omitempty,email"`
Role *string `json:"role,omitempty" validate:"omitempty,oneof=admin manager cashier waiter"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Permissions *map[string]interface{} `json:"permissions,omitempty"`
}
type ChangePasswordRequest struct {
CurrentPassword string `json:"current_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=6"`
}
type UpdateUserOutletRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
}
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
type LoginResponse struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
User UserResponse `json:"user"`
}
type UserResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID *uuid.UUID `json:"outlet_id"`
Name string `json:"name"`
Email string `json:"email"`
Role string `json:"role"`
Permissions map[string]interface{} `json:"permissions"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListUsersRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Role *string `json:"role,omitempty"`
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
OrganizationID *uuid.UUID `json:"organization_id,omitempty"`
}
type ListUsersResponse struct {
Users []UserResponse `json:"users"`
Pagination PaginationResponse `json:"pagination"`
}

View File

@ -1,15 +1,13 @@
package db package db
import ( import (
"apskel-pos-be/config"
"fmt" "fmt"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"go.uber.org/zap" "go.uber.org/zap"
_ "gopkg.in/yaml.v3" _ "gopkg.in/yaml.v3"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"enaklo-pos-be/config"
) )
func NewPostgres(c config.Database) (*gorm.DB, error) { func NewPostgres(c config.Database) (*gorm.DB, error) {
@ -19,21 +17,14 @@ func NewPostgres(c config.Database) (*gorm.DB, error) {
db, err := gorm.Open(dialector, &gorm.Config{}) db, err := gorm.Open(dialector, &gorm.Config{})
//db, err := gorm.Open(dialector, &gorm.Config{
// Logger: logger.Default.LogMode(logger.Info), // Enable GORM logging
//})
if err != nil { if err != nil {
return nil, err return nil, err
} }
zapCfg := zap.NewProductionConfig() zapCfg := zap.NewProductionConfig()
zapCfg.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) // whatever minimum level zapCfg.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
zapCfg.DisableCaller = false zapCfg.DisableCaller = false
// logger, _ := zapCfg.Build()
// db = gorm.Open(sqldblogger.New(logger), db)
// ping the database to test the connection
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -0,0 +1,102 @@
package entities
import (
"time"
"github.com/google/uuid"
)
// PaymentMethodAnalytics represents payment method analytics data
type PaymentMethodAnalytics struct {
PaymentMethodID uuid.UUID `json:"payment_method_id"`
PaymentMethodName string `json:"payment_method_name"`
PaymentMethodType string `json:"payment_method_type"`
TotalAmount float64 `json:"total_amount"`
OrderCount int64 `json:"order_count"`
PaymentCount int64 `json:"payment_count"`
}
// SalesAnalytics represents sales analytics data
type SalesAnalytics struct {
Date time.Time `json:"date"`
Sales float64 `json:"sales"`
Orders int64 `json:"orders"`
Items int64 `json:"items"`
Tax float64 `json:"tax"`
Discount float64 `json:"discount"`
NetSales float64 `json:"net_sales"`
}
// ProductAnalytics represents product analytics data
type ProductAnalytics struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
QuantitySold int64 `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
AveragePrice float64 `json:"average_price"`
OrderCount int64 `json:"order_count"`
}
// DashboardOverview represents dashboard overview data
type DashboardOverview struct {
TotalSales float64 `json:"total_sales"`
TotalOrders int64 `json:"total_orders"`
AverageOrderValue float64 `json:"average_order_value"`
TotalCustomers int64 `json:"total_customers"`
VoidedOrders int64 `json:"voided_orders"`
RefundedOrders int64 `json:"refunded_orders"`
}
// ProfitLossAnalytics represents profit and loss analytics data
type ProfitLossAnalytics struct {
Summary ProfitLossSummary `json:"summary"`
Data []ProfitLossData `json:"data"`
ProductData []ProductProfitData `json:"product_data"`
}
// ProfitLossSummary represents profit and loss summary data
type ProfitLossSummary struct {
TotalRevenue float64 `json:"total_revenue"`
TotalCost float64 `json:"total_cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
TotalTax float64 `json:"total_tax"`
TotalDiscount float64 `json:"total_discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
TotalOrders int64 `json:"total_orders"`
AverageProfit float64 `json:"average_profit"`
ProfitabilityRatio float64 `json:"profitability_ratio"`
}
// ProfitLossData represents profit and loss data by time period
type ProfitLossData struct {
Date time.Time `json:"date"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
Tax float64 `json:"tax"`
Discount float64 `json:"discount"`
NetProfit float64 `json:"net_profit"`
NetProfitMargin float64 `json:"net_profit_margin"`
Orders int64 `json:"orders"`
}
// ProductProfitData represents profit data for individual products
type ProductProfitData struct {
ProductID uuid.UUID `json:"product_id"`
ProductName string `json:"product_name"`
CategoryID uuid.UUID `json:"category_id"`
CategoryName string `json:"category_name"`
QuantitySold int64 `json:"quantity_sold"`
Revenue float64 `json:"revenue"`
Cost float64 `json:"cost"`
GrossProfit float64 `json:"gross_profit"`
GrossProfitMargin float64 `json:"gross_profit_margin"`
AveragePrice float64 `json:"average_price"`
AverageCost float64 `json:"average_cost"`
ProfitPerUnit float64 `json:"profit_per_unit"`
}

View File

@ -0,0 +1,56 @@
package entities
import (
"database/sql/driver"
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Metadata map[string]interface{}
func (m Metadata) Value() (driver.Value, error) {
return json.Marshal(m)
}
func (m *Metadata) Scan(value interface{}) error {
if value == nil {
*m = make(Metadata)
return nil
}
bytes, ok := value.([]byte)
if !ok {
return errors.New("type assertion to []byte failed")
}
return json.Unmarshal(bytes, m)
}
type Category struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
Name string `gorm:"not null;size:255" json:"name" validate:"required,min=1,max=255"`
Description *string `gorm:"type:text" json:"description"`
BusinessType string `gorm:"size:50;default:'restaurant'" json:"business_type"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Products []Product `gorm:"foreignKey:CategoryID" json:"products,omitempty"`
}
func (c *Category) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
return nil
}
func (Category) TableName() string {
return "categories"
}

View File

@ -0,0 +1,36 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Customer struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
Name string `gorm:"not null;size:255" json:"name" validate:"required"`
Email *string `gorm:"size:255;uniqueIndex" json:"email,omitempty"`
Phone *string `gorm:"size:20" json:"phone,omitempty"`
Address *string `gorm:"size:500" json:"address,omitempty"`
IsDefault bool `gorm:"default:false" json:"is_default"`
IsActive bool `gorm:"default:true" json:"is_active"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Orders []Order `gorm:"foreignKey:CustomerID" json:"orders,omitempty"`
}
func (c *Customer) BeforeCreate(tx *gorm.DB) error {
if c.ID == uuid.Nil {
c.ID = uuid.New()
}
return nil
}
func (Customer) TableName() string {
return "customers"
}

View File

@ -0,0 +1,27 @@
package entities
import "gorm.io/gorm"
func GetAllEntities() []interface{} {
return []interface{}{
&Organization{},
&Outlet{},
&OutletSetting{},
&User{},
&Category{},
&Product{},
&ProductVariant{},
&Inventory{},
&Order{},
&OrderItem{},
&PaymentMethod{},
&Payment{},
&Customer{},
&Table{},
// Analytics entities are not database tables, they are query results
}
}
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(GetAllEntities()...)
}

28
internal/entities/file.go Normal file
View File

@ -0,0 +1,28 @@
package entities
import (
"time"
"github.com/google/uuid"
)
type File struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"`
FileName string `gorm:"size:255;not null" json:"file_name"`
OriginalName string `gorm:"size:255;not null" json:"original_name"`
FileURL string `gorm:"size:500;not null" json:"file_url"`
FileSize int64 `gorm:"not null" json:"file_size"`
MimeType string `gorm:"size:100;not null" json:"mime_type"`
FileType string `gorm:"size:50;not null" json:"file_type"` // image, document, video, etc.
UploadPath string `gorm:"size:500;not null" json:"upload_path"`
IsPublic bool `gorm:"default:true" json:"is_public"`
Metadata Metadata `gorm:"type:jsonb" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
func (File) TableName() string {
return "files"
}

View File

@ -0,0 +1,42 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Inventory struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
Quantity int `gorm:"not null;default:0" json:"quantity" validate:"min=0"`
ReorderLevel int `gorm:"default:0" json:"reorder_level" validate:"min=0"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
}
func (i *Inventory) BeforeCreate(tx *gorm.DB) error {
if i.ID == uuid.Nil {
i.ID = uuid.New()
}
return nil
}
func (Inventory) TableName() string {
return "inventory"
}
func (i *Inventory) IsLowStock() bool {
return i.Quantity <= i.ReorderLevel
}
func (i *Inventory) UpdateQuantity(delta int) {
i.Quantity += delta
if i.Quantity < 0 {
i.Quantity = 0
}
}

View File

@ -0,0 +1,110 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type InventoryMovementType string
const (
InventoryMovementTypeSale InventoryMovementType = "sale"
InventoryMovementTypePurchase InventoryMovementType = "purchase"
InventoryMovementTypeAdjustment InventoryMovementType = "adjustment"
InventoryMovementTypeReturn InventoryMovementType = "return"
InventoryMovementTypeRefund InventoryMovementType = "refund"
InventoryMovementTypeVoid InventoryMovementType = "void"
InventoryMovementTypeTransferIn InventoryMovementType = "transfer_in"
InventoryMovementTypeTransferOut InventoryMovementType = "transfer_out"
InventoryMovementTypeDamage InventoryMovementType = "damage"
InventoryMovementTypeExpiry InventoryMovementType = "expiry"
)
type InventoryMovementReferenceType string
const (
InventoryMovementReferenceTypeOrder InventoryMovementReferenceType = "order"
InventoryMovementReferenceTypePayment InventoryMovementReferenceType = "payment"
InventoryMovementReferenceTypeRefund InventoryMovementReferenceType = "refund"
InventoryMovementReferenceTypeVoid InventoryMovementReferenceType = "void"
InventoryMovementReferenceTypeManual InventoryMovementReferenceType = "manual"
InventoryMovementReferenceTypeTransfer InventoryMovementReferenceType = "transfer"
InventoryMovementReferenceTypePurchaseOrder InventoryMovementReferenceType = "purchase_order"
)
type InventoryMovement struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id" validate:"required"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id" validate:"required"`
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
MovementType InventoryMovementType `gorm:"not null;size:50" json:"movement_type" validate:"required"`
Quantity int `gorm:"not null" json:"quantity" validate:"required"`
PreviousQuantity int `gorm:"not null" json:"previous_quantity" validate:"required"`
NewQuantity int `gorm:"not null" json:"new_quantity" validate:"required"`
UnitCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"unit_cost"`
TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"`
ReferenceType *InventoryMovementReferenceType `gorm:"size:50" json:"reference_type"`
ReferenceID *uuid.UUID `gorm:"type:uuid;index" json:"reference_id"`
OrderID *uuid.UUID `gorm:"type:uuid;index" json:"order_id"`
PaymentID *uuid.UUID `gorm:"type:uuid;index" json:"payment_id"`
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
Reason *string `gorm:"size:255" json:"reason"`
Notes *string `gorm:"type:text" json:"notes"`
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
Organization Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
Order *Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
Payment *Payment `gorm:"foreignKey:PaymentID" json:"payment,omitempty"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
func (im *InventoryMovement) BeforeCreate(tx *gorm.DB) error {
if im.ID == uuid.Nil {
im.ID = uuid.New()
}
return nil
}
func (InventoryMovement) TableName() string {
return "inventory_movements"
}
func (im *InventoryMovement) IsPositiveMovement() bool {
return im.Quantity > 0
}
func (im *InventoryMovement) IsNegativeMovement() bool {
return im.Quantity < 0
}
func (im *InventoryMovement) GetMovementDescription() string {
switch im.MovementType {
case InventoryMovementTypeSale:
return "Sale"
case InventoryMovementTypePurchase:
return "Purchase"
case InventoryMovementTypeAdjustment:
return "Manual Adjustment"
case InventoryMovementTypeReturn:
return "Return"
case InventoryMovementTypeRefund:
return "Refund"
case InventoryMovementTypeVoid:
return "Void"
case InventoryMovementTypeTransferIn:
return "Transfer In"
case InventoryMovementTypeTransferOut:
return "Transfer Out"
case InventoryMovementTypeDamage:
return "Damage"
case InventoryMovementTypeExpiry:
return "Expiry"
default:
return "Unknown"
}
}

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