init
This commit is contained in:
parent
1bceae010b
commit
4f5950543e
44
.air.toml
Normal file
44
.air.toml
Normal 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
|
||||||
@ -1,17 +1,76 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
# 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
292
ANALYTICS_API.md
Normal 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
|
||||||
312
DOCKER.md
Normal file
312
DOCKER.md
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
## 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. **Port Already in Use**
|
||||||
|
```bash
|
||||||
|
# Check what's using the port
|
||||||
|
lsof -i :3300
|
||||||
|
|
||||||
|
# Change ports in docker-compose.yaml if needed
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Database Connection Failed**
|
||||||
|
```bash
|
||||||
|
# Check if database is running
|
||||||
|
docker-compose ps postgres
|
||||||
|
|
||||||
|
# Check database logs
|
||||||
|
docker-compose logs postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Permission Denied**
|
||||||
|
```bash
|
||||||
|
# Make sure script is executable
|
||||||
|
chmod +x docker-build.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **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
|
||||||
|
```
|
||||||
93
Dockerfile
93
Dockerfile
@ -1,26 +1,99 @@
|
|||||||
# Build Stage
|
# Build Stage
|
||||||
FROM golang:1.20-alpine AS build
|
FROM golang:1.20-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.20-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"]
|
||||||
|
|||||||
@ -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"]
|
|
||||||
12
Makefile
12
Makefile
@ -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
|
||||||
|
|
||||||
|
|||||||
120
ORDER_VOID_STATUS_IMPROVEMENT.md
Normal file
120
ORDER_VOID_STATUS_IMPROVEMENT.md
Normal 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
155
OUTLET_TAX_CALCULATION.md
Normal 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
157
PRODUCT_STOCK_MANAGEMENT.md
Normal 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.
|
||||||
127
PRODUCT_VARIANT_PRICE_MODIFIER.md
Normal file
127
PRODUCT_VARIANT_PRICE_MODIFIER.md
Normal 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
|
||||||
241
PROFIT_LOSS_ANALYTICS_API.md
Normal file
241
PROFIT_LOSS_ANALYTICS_API.md
Normal 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
250
README.md
@ -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.
|
||||||
|
|||||||
30
cmd/server/main.go
Normal file
30
cmd/server/main.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Brevo struct {
|
|
||||||
APIKey string `mapstructure:"api_key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Brevo) GetApiKey() string {
|
|
||||||
return b.APIKey
|
|
||||||
}
|
|
||||||
@ -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"
|
||||||
)
|
)
|
||||||
@ -24,18 +24,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -3,13 +3,8 @@ package config
|
|||||||
import "time"
|
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)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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"`
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -1,11 +1,7 @@
|
|||||||
package config
|
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 {
|
||||||
|
|||||||
@ -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
6
config/log.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
LogFormat string `mapstructure:"log_format"`
|
||||||
|
LogLevel string `mapstructure:"log_level"`
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type FeatureToggle struct {
|
|
||||||
LoggerEnabled bool `mapstructure:"logger_enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FeatureToggle) IsLoggerEnabled() bool {
|
|
||||||
return f.LoggerEnabled
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
@ -1 +0,0 @@
|
|||||||
package config
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
type Withdraw struct {
|
|
||||||
PlatformFee int64 `mapstructure:"platform_fee"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Withdraw) GetPlatformFee() int64 {
|
|
||||||
return w.PlatformFee
|
|
||||||
}
|
|
||||||
199
docker-build.sh
Executable file
199
docker-build.sh
Executable file
@ -0,0 +1,199 @@
|
|||||||
|
#!/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..."
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
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
|
||||||
@ -1,8 +1,126 @@
|
|||||||
version: "3.3"
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
# PostgreSQL Database
|
||||||
build: .
|
postgres:
|
||||||
ports:
|
image: postgres:15-alpine
|
||||||
- "3300:3300"
|
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:
|
volumes:
|
||||||
- ./:/app/
|
- 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:
|
||||||
|
- "3300:3300"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- apskel-network
|
||||||
|
volumes:
|
||||||
|
- ./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
|
||||||
@ -1,463 +0,0 @@
|
|||||||
# Advanced Order Management API Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Advanced Order Management API provides comprehensive functionality for managing orders beyond basic operations. This includes partial refunds, void operations, and bill splitting capabilities.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ **Partial Refund**: Refund specific items from paid orders
|
|
||||||
- ✅ **Void Order**: Cancel ongoing orders (per item or entire order)
|
|
||||||
- ✅ **Split Bill**: Split orders by items or amounts
|
|
||||||
- ✅ **Order Status Management**: Support for PARTIAL and VOIDED statuses
|
|
||||||
- ✅ **Transaction Tracking**: Complete audit trail for all operations
|
|
||||||
- ✅ **Validation**: Comprehensive validation for all operations
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### 1. Partial Refund
|
|
||||||
|
|
||||||
**POST** `/order/partial-refund`
|
|
||||||
|
|
||||||
Refund specific items from a paid order while keeping the remaining items.
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer returned damaged items",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"quantity": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"order_item_id": 789,
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|--------|----------|--------------------------------|
|
|
||||||
| order_id | int64 | Yes | ID of the order to refund |
|
|
||||||
| reason | string | Yes | Reason for the partial refund |
|
|
||||||
| items | array | Yes | Array of items to refund |
|
|
||||||
|
|
||||||
#### Item Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|---------------|--------|----------|--------------------------------|
|
|
||||||
| order_item_id | int64 | Yes | ID of the order item to refund |
|
|
||||||
| quantity | int | Yes | Quantity to refund (min: 1) |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
**Success (200 OK)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"status": 200,
|
|
||||||
"data": {
|
|
||||||
"order_id": 123,
|
|
||||||
"status": "PARTIAL",
|
|
||||||
"refunded_amount": 75000,
|
|
||||||
"remaining_amount": 25000,
|
|
||||||
"reason": "Customer returned damaged items",
|
|
||||||
"refunded_at": "2024-01-15T10:30:00Z",
|
|
||||||
"customer_name": "John Doe",
|
|
||||||
"payment_type": "CASH",
|
|
||||||
"refunded_items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"item_name": "Bakso Special",
|
|
||||||
"quantity": 2,
|
|
||||||
"unit_price": 25000,
|
|
||||||
"total_price": 50000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"order_item_id": 789,
|
|
||||||
"item_name": "Es Teh Manis",
|
|
||||||
"quantity": 1,
|
|
||||||
"unit_price": 25000,
|
|
||||||
"total_price": 25000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Void Order
|
|
||||||
|
|
||||||
**POST** `/order/void`
|
|
||||||
|
|
||||||
Void an ongoing order (NEW or PENDING status) either entirely or by specific items.
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
**Void Entire Order:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer cancelled order",
|
|
||||||
"type": "ALL"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Void Specific Items:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer changed mind about some items",
|
|
||||||
"type": "ITEM",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|--------|----------|--------------------------------|
|
|
||||||
| order_id | int64 | Yes | ID of the order to void |
|
|
||||||
| reason | string | Yes | Reason for voiding |
|
|
||||||
| type | string | Yes | Type: "ALL" or "ITEM" |
|
|
||||||
| items | array | No | Required if type is "ITEM" |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
**Success (200 OK)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"status": 200,
|
|
||||||
"data": {
|
|
||||||
"order_id": 123,
|
|
||||||
"status": "VOIDED",
|
|
||||||
"reason": "Customer cancelled order",
|
|
||||||
"voided_at": "2024-01-15T10:30:00Z",
|
|
||||||
"customer_name": "John Doe",
|
|
||||||
"voided_items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"item_name": "Bakso Special",
|
|
||||||
"quantity": 1,
|
|
||||||
"unit_price": 25000,
|
|
||||||
"total_price": 25000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Split Bill
|
|
||||||
|
|
||||||
**POST** `/order/split-bill`
|
|
||||||
|
|
||||||
Split an order into a separate order by items or amounts.
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
**Split by Items:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"type": "ITEM",
|
|
||||||
"payment_method": "CASH",
|
|
||||||
"payment_provider": "CASH",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 789,
|
|
||||||
"quantity": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"order_item_id": 101,
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Split by Amount:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"type": "AMOUNT",
|
|
||||||
"payment_method": "CASH",
|
|
||||||
"payment_provider": "CASH",
|
|
||||||
"amount": 50000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|------------------|--------|----------|--------------------------------|
|
|
||||||
| order_id | int64 | Yes | ID of the order to split |
|
|
||||||
| type | string | Yes | Type: "ITEM" or "AMOUNT" |
|
|
||||||
| payment_method | string | Yes | Payment method for split order |
|
|
||||||
| payment_provider | string | No | Payment provider for split order|
|
|
||||||
| items | array | No | Required if type is "ITEM" |
|
|
||||||
| amount | float | No | Required if type is "AMOUNT" (must be less than order total) |
|
|
||||||
|
|
||||||
#### Item Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|---------------|--------|----------|--------------------------------|
|
|
||||||
| order_item_id | int64 | Yes | ID of the order item to split |
|
|
||||||
| quantity | int | Yes | Quantity to split (min: 1) |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
**Success (200 OK)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"status": 200,
|
|
||||||
"data": {
|
|
||||||
"id": 124,
|
|
||||||
"partner_id": 1,
|
|
||||||
"status": "PAID",
|
|
||||||
"amount": 100000,
|
|
||||||
"total": 110000,
|
|
||||||
"tax": 10000,
|
|
||||||
"customer_id": 456,
|
|
||||||
"customer_name": "John Doe",
|
|
||||||
"payment_type": "CASH",
|
|
||||||
"payment_provider": "CASH",
|
|
||||||
"source": "POS",
|
|
||||||
"created_at": "2024-01-15T10:30:00Z",
|
|
||||||
"updated_at": "2024-01-15T10:30:00Z",
|
|
||||||
"order_items": [
|
|
||||||
{
|
|
||||||
"id": 789,
|
|
||||||
"item_id": 1,
|
|
||||||
"item_name": "Bakso Special",
|
|
||||||
"price": 50000,
|
|
||||||
"quantity": 2,
|
|
||||||
"subtotal": 100000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Business Logic
|
|
||||||
|
|
||||||
### Partial Refund Process
|
|
||||||
|
|
||||||
1. **Validation**
|
|
||||||
- Verify order exists and belongs to partner
|
|
||||||
- Ensure order status is "PAID"
|
|
||||||
- Validate refund items exist and quantities are valid
|
|
||||||
|
|
||||||
2. **Item Updates**
|
|
||||||
- Reduce quantities of refunded items
|
|
||||||
- Remove items completely if quantity becomes 0
|
|
||||||
- Recalculate order totals
|
|
||||||
|
|
||||||
3. **Order Status Update**
|
|
||||||
- Set status to "PARTIAL" if items remain
|
|
||||||
- Set status to "REFUNDED" if all items refunded
|
|
||||||
|
|
||||||
4. **Transaction Creation**
|
|
||||||
- Create refund transaction with negative amount
|
|
||||||
- Track refund details
|
|
||||||
|
|
||||||
### Void Order Process
|
|
||||||
|
|
||||||
1. **Validation**
|
|
||||||
- Verify order exists and belongs to partner
|
|
||||||
- Ensure order status is "NEW" or "PENDING"
|
|
||||||
- Validate void items if type is "ITEM"
|
|
||||||
|
|
||||||
2. **Void Operations**
|
|
||||||
- **ALL**: Set order status to "VOIDED"
|
|
||||||
- **ITEM**: Reduce quantities and recalculate totals
|
|
||||||
|
|
||||||
3. **Status Management**
|
|
||||||
- Set status to "PARTIAL" if items remain
|
|
||||||
- Set status to "VOIDED" if all items voided
|
|
||||||
|
|
||||||
### Split Bill Process
|
|
||||||
|
|
||||||
1. **Validation**
|
|
||||||
- Verify order exists and belongs to partner
|
|
||||||
- Ensure order status is "NEW" or "PENDING"
|
|
||||||
- Validate split configuration
|
|
||||||
|
|
||||||
2. **Split Operations**
|
|
||||||
- **ITEM**: Create new PAID order with specified items, reduce quantities in original order
|
|
||||||
- **AMOUNT**: Create new PAID order with specified amount, reduce amount in original order
|
|
||||||
|
|
||||||
3. **Order Management**
|
|
||||||
- Original order remains PENDING with reduced items/amount
|
|
||||||
- New split order becomes PAID with specified payment method
|
|
||||||
- Recalculate totals for both orders
|
|
||||||
|
|
||||||
## Order Status Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
NEW → PENDING → PAID → REFUNDED
|
|
||||||
↓ ↓ ↓
|
|
||||||
VOIDED VOIDED PARTIAL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
### Common Error Responses
|
|
||||||
|
|
||||||
**Order Not Found (404)**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"status": 404,
|
|
||||||
"message": "order not found"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Invalid Order Status (400)**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"status": 400,
|
|
||||||
"message": "only paid order can be partially refunded"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Invalid Quantity (400)**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"status": 400,
|
|
||||||
"message": "refund quantity 3 exceeds available quantity 2 for item 456"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Split Amount Mismatch (400)**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"status": 400,
|
|
||||||
"message": "split amount 95000 must be less than order total 100000"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Schema Updates
|
|
||||||
|
|
||||||
### Orders Table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- New statuses supported
|
|
||||||
ALTER TABLE orders ADD CONSTRAINT check_status
|
|
||||||
CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL'));
|
|
||||||
```
|
|
||||||
|
|
||||||
### Order Items Table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Support for quantity updates
|
|
||||||
ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constants
|
|
||||||
|
|
||||||
### Order Status
|
|
||||||
|
|
||||||
```go
|
|
||||||
const (
|
|
||||||
New OrderStatus = "NEW"
|
|
||||||
Paid OrderStatus = "PAID"
|
|
||||||
Cancel OrderStatus = "CANCEL"
|
|
||||||
Pending OrderStatus = "PENDING"
|
|
||||||
Refunded OrderStatus = "REFUNDED"
|
|
||||||
Voided OrderStatus = "VOIDED" // New
|
|
||||||
Partial OrderStatus = "PARTIAL" // New
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Examples
|
|
||||||
|
|
||||||
### cURL Examples
|
|
||||||
|
|
||||||
**Partial Refund:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/order/partial-refund \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer returned damaged items",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"quantity": 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Void Order:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/order/void \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer cancelled order",
|
|
||||||
"type": "ALL"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
**Split Bill:**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/order/split-bill \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"type": "ITEM",
|
|
||||||
"payment_method": "CASH",
|
|
||||||
"payment_provider": "CASH",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"order_item_id": 456,
|
|
||||||
"quantity": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"order_item_id": 789,
|
|
||||||
"quantity": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Authorization**: Only authorized users can perform these operations
|
|
||||||
2. **Audit Trail**: All operations are logged with user and timestamp
|
|
||||||
3. **Validation**: Strict validation prevents invalid operations
|
|
||||||
4. **Data Integrity**: Transaction-based operations ensure consistency
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Bulk Operations**: Support for bulk partial refunds/voids
|
|
||||||
2. **Approval Workflow**: Multi-level approval for large operations
|
|
||||||
3. **Notification System**: Customer notifications for refunds/voids
|
|
||||||
4. **Analytics**: Dashboard for operation trends and analysis
|
|
||||||
5. **Integration**: Integration with inventory management systems
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For questions or issues with the Advanced Order Management API, please contact the development team or create an issue in the project repository.
|
|
||||||
@ -1,297 +0,0 @@
|
|||||||
# Advanced Order Management Implementation Summary
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document summarizes the complete implementation of advanced order management features for the Enaklo POS backend system. The implementation includes three major features: **Partial Refund**, **Void Order**, and **Split Bill** functionality.
|
|
||||||
|
|
||||||
## 🎯 Implemented Features
|
|
||||||
|
|
||||||
### 1. Partial Refund System
|
|
||||||
**Purpose**: Allow refunding specific items from paid orders while keeping remaining items.
|
|
||||||
|
|
||||||
**Key Components**:
|
|
||||||
- ✅ **API Endpoint**: `POST /order/partial-refund`
|
|
||||||
- ✅ **Service Method**: `PartialRefundRequest()`
|
|
||||||
- ✅ **Repository Methods**: `UpdateOrderItem()`, `UpdateOrderTotals()`
|
|
||||||
- ✅ **Validation**: Order status, item existence, quantity validation
|
|
||||||
- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts
|
|
||||||
- ✅ **Status Management**: Updates order to "PARTIAL" or "REFUNDED"
|
|
||||||
|
|
||||||
**Business Logic**:
|
|
||||||
```go
|
|
||||||
// Flow: PAID → PARTIAL/REFUNDED
|
|
||||||
// - Validate order is PAID
|
|
||||||
// - Reduce item quantities
|
|
||||||
// - Recalculate totals
|
|
||||||
// - Create refund transaction
|
|
||||||
// - Update order status
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Void Order System
|
|
||||||
**Purpose**: Cancel ongoing orders (NEW/PENDING) either entirely or by specific items.
|
|
||||||
|
|
||||||
**Key Components**:
|
|
||||||
- ✅ **API Endpoint**: `POST /order/void`
|
|
||||||
- ✅ **Service Method**: `VoidOrderRequest()`
|
|
||||||
- ✅ **Two Modes**: "ALL" (entire order) or "ITEM" (specific items)
|
|
||||||
- ✅ **Validation**: Order status, item existence, quantity validation
|
|
||||||
- ✅ **Status Management**: Updates order to "VOIDED" or "PARTIAL"
|
|
||||||
|
|
||||||
**Business Logic**:
|
|
||||||
```go
|
|
||||||
// Flow: NEW/PENDING → VOIDED/PARTIAL
|
|
||||||
// - Validate order is NEW or PENDING
|
|
||||||
// - ALL: Set status to VOIDED
|
|
||||||
// - ITEM: Reduce quantities, recalculate totals
|
|
||||||
// - Update order status accordingly
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Split Bill System
|
|
||||||
**Purpose**: Split orders into a separate order by items or amounts.
|
|
||||||
|
|
||||||
**Key Components**:
|
|
||||||
- ✅ **API Endpoint**: `POST /order/split-bill`
|
|
||||||
- ✅ **Service Method**: `SplitBillRequest()`
|
|
||||||
- ✅ **Two Modes**: "ITEM" (specify items) or "AMOUNT" (specify amount)
|
|
||||||
- ✅ **Order Creation**: Creates a new order for the split
|
|
||||||
- ✅ **Original Order**: Voids the original order after splitting
|
|
||||||
|
|
||||||
**Business Logic**:
|
|
||||||
```go
|
|
||||||
// Flow: NEW/PENDING → PENDING (reduced) + PAID (split)
|
|
||||||
// - Validate order is NEW or PENDING
|
|
||||||
// - ITEM: Create PAID order with specified items, reduce quantities in original
|
|
||||||
// - AMOUNT: Create PAID order with specified amount, reduce amount in original
|
|
||||||
// - Original order remains PENDING with reduced items/amount
|
|
||||||
// - New split order becomes PAID with specified payment method
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🏗️ Architecture Components
|
|
||||||
|
|
||||||
### 1. Constants & Status Management
|
|
||||||
```go
|
|
||||||
// Added new order statuses
|
|
||||||
const (
|
|
||||||
New OrderStatus = "NEW"
|
|
||||||
Paid OrderStatus = "PAID"
|
|
||||||
Cancel OrderStatus = "CANCEL"
|
|
||||||
Pending OrderStatus = "PENDING"
|
|
||||||
Refunded OrderStatus = "REFUNDED"
|
|
||||||
Voided OrderStatus = "VOIDED" // New
|
|
||||||
Partial OrderStatus = "PARTIAL" // New
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Entity Models
|
|
||||||
```go
|
|
||||||
// New entity types for request/response handling
|
|
||||||
type PartialRefundItem struct {
|
|
||||||
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
|
||||||
Quantity int `json:"quantity" validate:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VoidItem struct {
|
|
||||||
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
|
||||||
Quantity int `json:"quantity" validate:"required,min=1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SplitBillSplit struct {
|
|
||||||
CustomerName string `json:"customer_name" validate:"required"`
|
|
||||||
CustomerID *int64 `json:"customer_id"`
|
|
||||||
Items []SplitBillItem `json:"items,omitempty"`
|
|
||||||
Amount float64 `json:"amount,omitempty"`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Repository Layer
|
|
||||||
```go
|
|
||||||
// New repository methods
|
|
||||||
type Repository interface {
|
|
||||||
// ... existing methods
|
|
||||||
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
|
||||||
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Service Layer
|
|
||||||
```go
|
|
||||||
// New service methods
|
|
||||||
type Service interface {
|
|
||||||
// ... existing methods
|
|
||||||
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
|
|
||||||
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
|
|
||||||
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, splits []entity.SplitBillSplit) ([]*entity.Order, error)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. HTTP Handlers
|
|
||||||
```go
|
|
||||||
// New API endpoints
|
|
||||||
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
|
||||||
// ... existing routes
|
|
||||||
route.POST("/partial-refund", jwt, h.PartialRefund)
|
|
||||||
route.POST("/void", jwt, h.VoidOrder)
|
|
||||||
route.POST("/split-bill", jwt, h.SplitBill)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Order Status Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
NEW → PENDING → PAID → REFUNDED
|
|
||||||
↓ ↓ ↓
|
|
||||||
VOIDED VOIDED PARTIAL
|
|
||||||
```
|
|
||||||
|
|
||||||
**Status Transitions**:
|
|
||||||
- **NEW/PENDING** → **VOIDED**: When entire order is voided
|
|
||||||
- **NEW/PENDING** → **PARTIAL**: When some items are voided
|
|
||||||
- **PAID** → **PARTIAL**: When some items are refunded
|
|
||||||
- **PAID** → **REFUNDED**: When all items are refunded
|
|
||||||
|
|
||||||
## 🔒 Validation & Security
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
- ✅ **Order Existence**: Verify order exists and belongs to partner
|
|
||||||
- ✅ **Status Validation**: Ensure appropriate status for operations
|
|
||||||
- ✅ **Item Validation**: Verify items exist and quantities are valid
|
|
||||||
- ✅ **Quantity Validation**: Prevent refunding/voiding more than available
|
|
||||||
- ✅ **Split Validation**: Ensure split amounts match order total
|
|
||||||
|
|
||||||
### Business Rules
|
|
||||||
- ✅ **Partial Refund**: Only PAID orders can be partially refunded
|
|
||||||
- ✅ **Void Order**: Only NEW/PENDING orders can be voided
|
|
||||||
- ✅ **Split Bill**: Only NEW/PENDING orders can be split
|
|
||||||
- ✅ **Transaction Tracking**: All operations create audit trails
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
### Test Coverage
|
|
||||||
- ✅ **Unit Tests**: Comprehensive test coverage for all service methods
|
|
||||||
- ✅ **Mock Testing**: Uses testify/mock for dependency mocking
|
|
||||||
- ✅ **Edge Cases**: Tests for invalid states and error conditions
|
|
||||||
- ✅ **Success Scenarios**: Tests for successful operations
|
|
||||||
|
|
||||||
### Test Files
|
|
||||||
- `internal/services/v2/order/refund_test.go` - Original refund tests
|
|
||||||
- `internal/services/v2/order/advanced_order_management_test.go` - New feature tests
|
|
||||||
|
|
||||||
## 📚 Documentation
|
|
||||||
|
|
||||||
### API Documentation
|
|
||||||
- ✅ **REFUND_API.md**: Complete refund API documentation
|
|
||||||
- ✅ **ADVANCED_ORDER_MANAGEMENT.md**: Comprehensive feature documentation
|
|
||||||
- ✅ **IMPLEMENTATION_SUMMARY.md**: This summary document
|
|
||||||
|
|
||||||
### Documentation Features
|
|
||||||
- ✅ **Request/Response Examples**: Complete JSON examples
|
|
||||||
- ✅ **Error Handling**: Common error scenarios and responses
|
|
||||||
- ✅ **Business Logic**: Detailed process flows
|
|
||||||
- ✅ **cURL Examples**: Ready-to-use API testing commands
|
|
||||||
|
|
||||||
## 🚀 Usage Examples
|
|
||||||
|
|
||||||
### Partial Refund
|
|
||||||
```bash
|
|
||||||
curl -X POST /order/partial-refund \
|
|
||||||
-H "Authorization: Bearer TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer returned damaged items",
|
|
||||||
"items": [
|
|
||||||
{"order_item_id": 456, "quantity": 2}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Void Order
|
|
||||||
```bash
|
|
||||||
curl -X POST /order/void \
|
|
||||||
-H "Authorization: Bearer TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer cancelled order",
|
|
||||||
"type": "ALL"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Split Bill
|
|
||||||
```bash
|
|
||||||
curl -X POST /order/split-bill \
|
|
||||||
-H "Authorization: Bearer TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"type": "ITEM",
|
|
||||||
"payment_method": "CASH",
|
|
||||||
"payment_provider": "CASH",
|
|
||||||
"items": [
|
|
||||||
{"order_item_id": 456, "quantity": 1},
|
|
||||||
{"order_item_id": 789, "quantity": 1}
|
|
||||||
]
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Database Considerations
|
|
||||||
|
|
||||||
### Schema Updates
|
|
||||||
```sql
|
|
||||||
-- New statuses supported
|
|
||||||
ALTER TABLE orders ADD CONSTRAINT check_status
|
|
||||||
CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL'));
|
|
||||||
|
|
||||||
-- Support for quantity updates
|
|
||||||
ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW();
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transaction Management
|
|
||||||
- ✅ **Atomic Operations**: All operations use database transactions
|
|
||||||
- ✅ **Rollback Support**: Failed operations are properly rolled back
|
|
||||||
- ✅ **Data Consistency**: Ensures order totals match item totals
|
|
||||||
|
|
||||||
## 🎯 Benefits
|
|
||||||
|
|
||||||
### Business Benefits
|
|
||||||
1. **Flexibility**: Support for complex order management scenarios
|
|
||||||
2. **Customer Satisfaction**: Handle partial returns and cancellations
|
|
||||||
3. **Operational Efficiency**: Streamlined bill splitting for groups
|
|
||||||
4. **Audit Trail**: Complete tracking of all order modifications
|
|
||||||
|
|
||||||
### Technical Benefits
|
|
||||||
1. **Scalable Architecture**: Clean separation of concerns
|
|
||||||
2. **Comprehensive Testing**: High test coverage ensures reliability
|
|
||||||
3. **Extensible Design**: Easy to add new order management features
|
|
||||||
4. **Documentation**: Complete API documentation for integration
|
|
||||||
|
|
||||||
## 🔮 Future Enhancements
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
1. **Bulk Operations**: Support for bulk partial refunds/voids
|
|
||||||
2. **Approval Workflow**: Multi-level approval for large operations
|
|
||||||
3. **Notification System**: Customer notifications for refunds/voids
|
|
||||||
4. **Analytics Dashboard**: Order management trends and analysis
|
|
||||||
5. **Inventory Integration**: Automatic inventory updates for refunds/voids
|
|
||||||
|
|
||||||
### Integration Opportunities
|
|
||||||
1. **Payment Gateway**: Direct refund processing
|
|
||||||
2. **Customer Management**: Customer point adjustments
|
|
||||||
3. **Reporting System**: Enhanced order analytics
|
|
||||||
4. **Mobile App**: Real-time order management
|
|
||||||
|
|
||||||
## 📋 Implementation Checklist
|
|
||||||
|
|
||||||
- ✅ **Core Features**: All three main features implemented
|
|
||||||
- ✅ **API Endpoints**: Complete REST API implementation
|
|
||||||
- ✅ **Service Layer**: Business logic implementation
|
|
||||||
- ✅ **Repository Layer**: Database operations
|
|
||||||
- ✅ **Validation**: Comprehensive input validation
|
|
||||||
- ✅ **Error Handling**: Proper error responses
|
|
||||||
- ✅ **Testing**: Unit test coverage
|
|
||||||
- ✅ **Documentation**: Complete API documentation
|
|
||||||
- ✅ **Status Management**: New order statuses
|
|
||||||
- ✅ **Transaction Tracking**: Audit trail implementation
|
|
||||||
|
|
||||||
## 🎉 Conclusion
|
|
||||||
|
|
||||||
The Advanced Order Management system provides a comprehensive solution for complex order scenarios in the Enaklo POS system. The implementation follows best practices for scalability, maintainability, and reliability, with complete documentation and testing coverage.
|
|
||||||
|
|
||||||
The system is now ready for production use and provides the foundation for future enhancements and integrations.
|
|
||||||
@ -1,271 +0,0 @@
|
|||||||
# Refund Order API Documentation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Refund Order API provides comprehensive functionality to process refunds for paid orders. This includes order status updates, transaction creation, customer voucher reversal, payment gateway refunds, and customer notifications.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- ✅ **Order Status Management**: Updates order status to "REFUNDED"
|
|
||||||
- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts
|
|
||||||
- ✅ **Customer Voucher Reversal**: Reverses any vouchers/points given for the order
|
|
||||||
- ✅ **Payment Gateway Integration**: Handles refunds for non-cash payments
|
|
||||||
- ✅ **Customer Notifications**: Sends email notifications for refunds
|
|
||||||
- ✅ **Audit Trail**: Tracks who processed the refund and when
|
|
||||||
- ✅ **Refund History**: Provides endpoint to view refund history
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### 1. Process Refund
|
|
||||||
|
|
||||||
**POST** `/order/refund`
|
|
||||||
|
|
||||||
Process a refund for a paid order.
|
|
||||||
|
|
||||||
#### Request Body
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer request"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Request Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|--------|----------|--------------------------------|
|
|
||||||
| order_id | int64 | Yes | ID of the order to refund |
|
|
||||||
| reason | string | Yes | Reason for the refund |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
**Success (200 OK)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"status": 200,
|
|
||||||
"data": {
|
|
||||||
"order_id": 123,
|
|
||||||
"status": "REFUNDED",
|
|
||||||
"refund_amount": 100000,
|
|
||||||
"reason": "Customer request",
|
|
||||||
"refunded_at": "2024-01-15T10:30:00Z",
|
|
||||||
"customer_name": "John Doe",
|
|
||||||
"payment_type": "CASH"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error (400 Bad Request)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": false,
|
|
||||||
"status": 400,
|
|
||||||
"message": "only paid order can be refund"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Get Refund History
|
|
||||||
|
|
||||||
**GET** `/order/refund-history`
|
|
||||||
|
|
||||||
Retrieve refund history with filtering and pagination.
|
|
||||||
|
|
||||||
#### Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-------------|--------|----------|--------------------------------|
|
|
||||||
| limit | int | No | Number of records (max 100) |
|
|
||||||
| offset | int | No | Number of records to skip |
|
|
||||||
| start_date | string | No | Start date (RFC3339 format) |
|
|
||||||
| end_date | string | No | End date (RFC3339 format) |
|
|
||||||
|
|
||||||
#### Response
|
|
||||||
|
|
||||||
**Success (200 OK)**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true,
|
|
||||||
"status": 200,
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"order_id": 123,
|
|
||||||
"customer_name": "John Doe",
|
|
||||||
"customer_id": 456,
|
|
||||||
"is_member": true,
|
|
||||||
"status": "REFUNDED",
|
|
||||||
"amount": 95000,
|
|
||||||
"total": 100000,
|
|
||||||
"payment_type": "CASH",
|
|
||||||
"table_number": "A1",
|
|
||||||
"order_type": "DINE_IN",
|
|
||||||
"created_at": "2024-01-15T09:00:00Z",
|
|
||||||
"refunded_at": "2024-01-15T10:30:00Z",
|
|
||||||
"tax": 5000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"paging_meta": {
|
|
||||||
"page": 1,
|
|
||||||
"total": 25,
|
|
||||||
"limit": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Business Logic
|
|
||||||
|
|
||||||
### Refund Process Flow
|
|
||||||
|
|
||||||
1. **Validation**
|
|
||||||
- Verify order exists and belongs to partner
|
|
||||||
- Ensure order status is "PAID"
|
|
||||||
- Validate refund reason
|
|
||||||
|
|
||||||
2. **Order Update**
|
|
||||||
- Update order status to "REFUNDED"
|
|
||||||
- Store refund reason in order description
|
|
||||||
- Update timestamp
|
|
||||||
|
|
||||||
3. **Transaction Creation**
|
|
||||||
- Create refund transaction with negative amount
|
|
||||||
- Set transaction type to "REFUND"
|
|
||||||
- Track who processed the refund
|
|
||||||
|
|
||||||
4. **Customer Voucher Reversal**
|
|
||||||
- Find vouchers associated with the order
|
|
||||||
- Mark vouchers as reversed/cancelled
|
|
||||||
- Adjust customer points if applicable
|
|
||||||
|
|
||||||
5. **Payment Gateway Refund**
|
|
||||||
- For non-cash payments, call payment gateway refund API
|
|
||||||
- Handle gateway response and errors
|
|
||||||
- Update transaction with gateway details
|
|
||||||
|
|
||||||
6. **Customer Notification**
|
|
||||||
- Send email notification to customer
|
|
||||||
- Include refund details and reason
|
|
||||||
- Provide transaction reference
|
|
||||||
|
|
||||||
### Supported Payment Methods
|
|
||||||
|
|
||||||
| Payment Method | Refund Handling |
|
|
||||||
|----------------|-----------------------------------|
|
|
||||||
| CASH | Manual refund (no gateway call) |
|
|
||||||
| QRIS | Gateway refund via provider API |
|
|
||||||
| CARD | Gateway refund via provider API |
|
|
||||||
| TRANSFER | Gateway refund via provider API |
|
|
||||||
| ONLINE | Gateway refund via provider API |
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- **Order not found**: Returns 404 error
|
|
||||||
- **Order not paid**: Returns 400 error with message
|
|
||||||
- **Voucher reversal failure**: Logs warning but continues refund
|
|
||||||
- **Payment gateway failure**: Logs error but continues refund
|
|
||||||
- **Notification failure**: Logs warning but continues refund
|
|
||||||
|
|
||||||
## Database Schema
|
|
||||||
|
|
||||||
### Orders Table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
ALTER TABLE orders ADD COLUMN description TEXT;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transactions Table
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Refund transactions have negative amounts
|
|
||||||
-- Transaction type: "REFUND"
|
|
||||||
-- Status: "REFUND"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Constants
|
|
||||||
|
|
||||||
### Order Status
|
|
||||||
|
|
||||||
```go
|
|
||||||
const (
|
|
||||||
New OrderStatus = "NEW"
|
|
||||||
Paid OrderStatus = "PAID"
|
|
||||||
Cancel OrderStatus = "CANCEL"
|
|
||||||
Pending OrderStatus = "PENDING"
|
|
||||||
Refunded OrderStatus = "REFUNDED" // New status
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Transaction Status
|
|
||||||
|
|
||||||
```go
|
|
||||||
const (
|
|
||||||
New PaymentStatus = "NEW"
|
|
||||||
Paid PaymentStatus = "PAID"
|
|
||||||
Cancel PaymentStatus = "CANCEL"
|
|
||||||
Refund PaymentStatus = "REFUND" // New status
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run the refund tests:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go test ./internal/services/v2/order -v -run TestRefund
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
1. **Authorization**: Only authorized users can process refunds
|
|
||||||
2. **Audit Trail**: All refunds are logged with user and timestamp
|
|
||||||
3. **Validation**: Strict validation prevents invalid refunds
|
|
||||||
4. **Rate Limiting**: Consider implementing rate limiting for refund endpoints
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
1. **Partial Refunds**: Support for refunding specific order items
|
|
||||||
2. **Refund Approval Workflow**: Multi-level approval for large refunds
|
|
||||||
3. **Refund Analytics**: Dashboard for refund trends and analysis
|
|
||||||
4. **Automated Refunds**: Integration with customer service systems
|
|
||||||
5. **Refund Templates**: Predefined refund reasons and templates
|
|
||||||
|
|
||||||
## Integration Examples
|
|
||||||
|
|
||||||
### cURL Example
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8080/api/v1/order/refund \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
|
||||||
-d '{
|
|
||||||
"order_id": 123,
|
|
||||||
"reason": "Customer request"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### JavaScript Example
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const refundOrder = async (orderId, reason) => {
|
|
||||||
const response = await fetch('/api/v1/order/refund', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
order_id: orderId,
|
|
||||||
reason: reason
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.json();
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For questions or issues with the refund API, please contact the development team or create an issue in the project repository.
|
|
||||||
3101
docs/docs.go
3101
docs/docs.go
File diff suppressed because it is too large
Load Diff
3072
docs/swagger.json
3072
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1833
docs/swagger.yaml
1833
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
40
go.mod
40
go.mod
@ -1,41 +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/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
|
||||||
@ -43,21 +30,17 @@ 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/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.4 // 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
|
||||||
@ -66,31 +49,24 @@ require (
|
|||||||
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/net v0.30.0 // indirect
|
golang.org/x/net v0.30.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.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/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
|
||||||
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
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|||||||
84
go.sum
84
go.sum
@ -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=
|
||||||
@ -281,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=
|
||||||
@ -344,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=
|
||||||
@ -372,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=
|
||||||
@ -408,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=
|
||||||
@ -420,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=
|
||||||
@ -435,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=
|
||||||
@ -474,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=
|
||||||
@ -546,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=
|
||||||
@ -652,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
34
infra/development.yaml
Normal 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'
|
||||||
@ -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
|
|
||||||
242
internal/README.md
Normal file
242
internal/README.md
Normal 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!** 🎉
|
||||||
284
internal/app/app.go
Normal file
284
internal/app/app.go
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
orderRepo *repository.OrderRepositoryImpl
|
||||||
|
orderItemRepo *repository.OrderItemRepositoryImpl
|
||||||
|
paymentRepo *repository.PaymentRepositoryImpl
|
||||||
|
paymentMethodRepo *repository.PaymentMethodRepositoryImpl
|
||||||
|
fileRepo *repository.FileRepositoryImpl
|
||||||
|
customerRepo *repository.CustomerRepository
|
||||||
|
analyticsRepo *repository.AnalyticsRepositoryImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
80
internal/appcontext/context.go
Normal file
80
internal/appcontext/context.go
Normal 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 ""
|
||||||
|
}
|
||||||
81
internal/appcontext/context_info.go
Normal file
81
internal/appcontext/context_info.go
Normal 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
|
||||||
|
}
|
||||||
@ -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 ""
|
||||||
}
|
}
|
||||||
BIN
internal/common/.DS_Store
vendored
BIN
internal/common/.DS_Store
vendored
Binary file not shown.
@ -1,5 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
type Config interface {
|
|
||||||
ConnString() string
|
|
||||||
}
|
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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{})
|
|
||||||
}
|
|
||||||
@ -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{}
|
|
||||||
}
|
|
||||||
BIN
internal/constants/.DS_Store
vendored
BIN
internal/constants/.DS_Store
vendored
Binary file not shown.
@ -1,12 +0,0 @@
|
|||||||
package branch
|
|
||||||
|
|
||||||
type BranchStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Active BranchStatus = "Active"
|
|
||||||
Inactive BranchStatus = "Inactive"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b BranchStatus) toString() string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
63
internal/constants/business.go
Normal file
63
internal/constants/business.go
Normal 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
|
||||||
|
}
|
||||||
17
internal/constants/constant.go
Normal file
17
internal/constants/constant.go
Normal 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,
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
type DeviceStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
On DeviceStatus = "On"
|
|
||||||
Off DeviceStatus = "Off"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b DeviceStatus) toString() string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
type DeviceConnectionStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Connected DeviceConnectionStatus = "Connected"
|
|
||||||
Disconnected DeviceConnectionStatus = "Disconnected"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b DeviceConnectionStatus) toString() string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
57
internal/constants/error.go
Normal file
57
internal/constants/error.go
Normal 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")
|
||||||
|
)
|
||||||
80
internal/constants/file.go
Normal file
80
internal/constants/file.go
Normal 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
|
||||||
|
}
|
||||||
27
internal/constants/header.go
Normal file
27
internal/constants/header.go
Normal 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"
|
||||||
|
)
|
||||||
87
internal/constants/order.go
Normal file
87
internal/constants/order.go
Normal 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
|
||||||
|
}
|
||||||
@ -1,87 +0,0 @@
|
|||||||
package order
|
|
||||||
|
|
||||||
type OrderStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
New OrderStatus = "NEW"
|
|
||||||
Paid OrderStatus = "PAID"
|
|
||||||
Cancel OrderStatus = "CANCEL"
|
|
||||||
Pending OrderStatus = "PENDING"
|
|
||||||
Refunded OrderStatus = "REFUNDED"
|
|
||||||
Voided OrderStatus = "VOIDED"
|
|
||||||
Partial OrderStatus = "PARTIAL"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
26
internal/constants/organization.go
Normal file
26
internal/constants/organization.go
Normal 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
|
||||||
|
}
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package constants
|
|
||||||
|
|
||||||
const (
|
|
||||||
OssLogLevelLogOff = "LogOff"
|
|
||||||
OssLogLevelDebug = "Debug"
|
|
||||||
OssLogLevelError = "Error"
|
|
||||||
OssLogLevelWarn = "Warn"
|
|
||||||
OssLogLevelInfo = "Info"
|
|
||||||
)
|
|
||||||
27
internal/constants/outlet.go
Normal file
27
internal/constants/outlet.go
Normal 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",
|
||||||
|
}
|
||||||
82
internal/constants/payment.go
Normal file
82
internal/constants/payment.go
Normal 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
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
)
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package studio
|
|
||||||
|
|
||||||
type StudioStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Active StudioStatus = "active"
|
|
||||||
Inactive StudioStatus = "inactive"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (b StudioStatus) toString() string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package transaction
|
|
||||||
|
|
||||||
type PaymentStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
New PaymentStatus = "NEW"
|
|
||||||
Paid PaymentStatus = "PAID"
|
|
||||||
Cancel PaymentStatus = "CANCEL"
|
|
||||||
Refund PaymentStatus = "REFUND"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
28
internal/constants/user.go
Normal file
28
internal/constants/user.go
Normal 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
|
||||||
|
}
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package userstatus
|
|
||||||
|
|
||||||
type UserStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Active UserStatus = "Active"
|
|
||||||
Inactive UserStatus = "Inactive"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (u UserStatus) toString() string {
|
|
||||||
return string(u)
|
|
||||||
}
|
|
||||||
210
internal/contract/analytics_contract.go
Normal file
210
internal/contract/analytics_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
49
internal/contract/category_contract.go
Normal file
49
internal/contract/category_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
49
internal/contract/common.go
Normal file
49
internal/contract/common.go
Normal 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"`
|
||||||
|
}
|
||||||
58
internal/contract/customer_contract.go
Normal file
58
internal/contract/customer_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
73
internal/contract/file_contract.go
Normal file
73
internal/contract/file_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
72
internal/contract/inventory_contract.go
Normal file
72
internal/contract/inventory_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
215
internal/contract/order_contract.go
Normal file
215
internal/contract/order_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
61
internal/contract/organization_contract.go
Normal file
61
internal/contract/organization_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
57
internal/contract/outlet_contract.go
Normal file
57
internal/contract/outlet_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
53
internal/contract/payment_method_contract.go
Normal file
53
internal/contract/payment_method_contract.go
Normal 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"`
|
||||||
|
}
|
||||||
103
internal/contract/product_contract.go
Normal file
103
internal/contract/product_contract.go
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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"`
|
||||||
|
Image *string `json:"image,omitempty"` // Will be stored in metadata["image"]
|
||||||
|
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"`
|
||||||
|
Image *string `json:"image,omitempty"` // Will be stored in metadata["image"]
|
||||||
|
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"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
39
internal/contract/response.go
Normal file
39
internal/contract/response.go
Normal 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
|
||||||
|
}
|
||||||
33
internal/contract/response_error.go
Normal file
33
internal/contract/response_error.go
Normal 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())
|
||||||
|
}
|
||||||
69
internal/contract/user_contract.go
Normal file
69
internal/contract/user_contract.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
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 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"`
|
||||||
|
}
|
||||||
@ -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
|
||||||
102
internal/entities/analytics.go
Normal file
102
internal/entities/analytics.go
Normal 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"`
|
||||||
|
}
|
||||||
56
internal/entities/category.go
Normal file
56
internal/entities/category.go
Normal 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"
|
||||||
|
}
|
||||||
36
internal/entities/customer.go
Normal file
36
internal/entities/customer.go
Normal 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"
|
||||||
|
}
|
||||||
26
internal/entities/entities.go
Normal file
26
internal/entities/entities.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
func GetAllEntities() []interface{} {
|
||||||
|
return []interface{}{
|
||||||
|
&Organization{},
|
||||||
|
&Outlet{},
|
||||||
|
&OutletSetting{},
|
||||||
|
&User{},
|
||||||
|
&Category{},
|
||||||
|
&Product{},
|
||||||
|
&ProductVariant{},
|
||||||
|
&Inventory{},
|
||||||
|
&Order{},
|
||||||
|
&OrderItem{},
|
||||||
|
&PaymentMethod{},
|
||||||
|
&Payment{},
|
||||||
|
&Customer{},
|
||||||
|
// 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
28
internal/entities/file.go
Normal 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"
|
||||||
|
}
|
||||||
42
internal/entities/inventory.go
Normal file
42
internal/entities/inventory.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
110
internal/entities/order.go
Normal file
110
internal/entities/order.go
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderType string
|
||||||
|
type OrderStatus string
|
||||||
|
type PaymentStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderTypeDineIn OrderType = "dine_in"
|
||||||
|
OrderTypeTakeout OrderType = "takeout"
|
||||||
|
OrderTypeDelivery OrderType = "delivery"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderStatusPending OrderStatus = "pending"
|
||||||
|
OrderStatusPreparing OrderStatus = "preparing"
|
||||||
|
OrderStatusReady OrderStatus = "ready"
|
||||||
|
OrderStatusCompleted OrderStatus = "completed"
|
||||||
|
OrderStatusCancelled OrderStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaymentStatusPending PaymentStatus = "pending"
|
||||||
|
PaymentStatusCompleted PaymentStatus = "completed"
|
||||||
|
PaymentStatusFailed PaymentStatus = "failed"
|
||||||
|
PaymentStatusRefunded PaymentStatus = "refunded"
|
||||||
|
PaymentStatusPartiallyRefunded PaymentStatus = "partial-refunded"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Order 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"`
|
||||||
|
UserID uuid.UUID `gorm:"type:uuid;not null;index" json:"user_id" validate:"required"`
|
||||||
|
CustomerID *uuid.UUID `gorm:"type:uuid;index" json:"customer_id"`
|
||||||
|
OrderNumber string `gorm:"uniqueIndex;not null;size:50" json:"order_number" validate:"required"`
|
||||||
|
TableNumber *string `gorm:"size:20" json:"table_number"`
|
||||||
|
OrderType OrderType `gorm:"not null;size:50" json:"order_type" validate:"required,oneof=dine_in takeout delivery"`
|
||||||
|
Status OrderStatus `gorm:"default:'pending';size:50" json:"status"`
|
||||||
|
Subtotal float64 `gorm:"type:decimal(10,2);not null" json:"subtotal" validate:"required,min=0"`
|
||||||
|
TaxAmount float64 `gorm:"type:decimal(10,2);not null" json:"tax_amount" validate:"required,min=0"`
|
||||||
|
DiscountAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"discount_amount" validate:"min=0"`
|
||||||
|
TotalAmount float64 `gorm:"type:decimal(10,2);not null" json:"total_amount" validate:"required,min=0"`
|
||||||
|
TotalCost float64 `gorm:"type:decimal(10,2);default:0.00" json:"total_cost"`
|
||||||
|
PaymentStatus PaymentStatus `gorm:"default:'pending';size:50" json:"payment_status"`
|
||||||
|
RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"`
|
||||||
|
IsVoid bool `gorm:"default:false" json:"is_void"`
|
||||||
|
IsRefund bool `gorm:"default:false" json:"is_refund"`
|
||||||
|
VoidReason *string `gorm:"size:255" json:"void_reason,omitempty"`
|
||||||
|
VoidedAt *time.Time `gorm:"" json:"voided_at,omitempty"`
|
||||||
|
VoidedBy *uuid.UUID `gorm:"type:uuid" json:"voided_by,omitempty"`
|
||||||
|
RefundReason *string `gorm:"size:255" json:"refund_reason,omitempty"`
|
||||||
|
RefundedAt *time.Time `gorm:"" json:"refunded_at,omitempty"`
|
||||||
|
RefundedBy *uuid.UUID `gorm:"type:uuid" json:"refunded_by,omitempty"`
|
||||||
|
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"`
|
||||||
|
Outlet Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
|
||||||
|
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||||
|
OrderItems []OrderItem `gorm:"foreignKey:OrderID" json:"order_items,omitempty"`
|
||||||
|
Payments []Payment `gorm:"foreignKey:OrderID" json:"payments,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if o.ID == uuid.Nil {
|
||||||
|
o.ID = uuid.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.OrderNumber == "" {
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
o.OrderNumber = fmt.Sprintf("ORD/%d", timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Order) TableName() string {
|
||||||
|
return "orders"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) CanBeModified() bool {
|
||||||
|
return o.Status == OrderStatusPending
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) CanBeCancelled() bool {
|
||||||
|
return o.Status != OrderStatusCompleted && o.Status != OrderStatusCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) GetTotalPaid() float64 {
|
||||||
|
var total float64
|
||||||
|
for _, payment := range o.Payments {
|
||||||
|
if payment.Status == PaymentTransactionStatusCompleted {
|
||||||
|
total += payment.Amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Order) IsFullyPaid() bool {
|
||||||
|
return o.GetTotalPaid() >= o.TotalAmount
|
||||||
|
}
|
||||||
89
internal/entities/order_item.go
Normal file
89
internal/entities/order_item.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Modifiers []map[string]interface{}
|
||||||
|
|
||||||
|
func (m Modifiers) Value() (driver.Value, error) {
|
||||||
|
return json.Marshal(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modifiers) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
*m = make(Modifiers, 0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("type assertion to []byte failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(bytes, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrderItemStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderItemStatusPending OrderItemStatus = "pending"
|
||||||
|
OrderItemStatusPreparing OrderItemStatus = "preparing"
|
||||||
|
OrderItemStatusReady OrderItemStatus = "ready"
|
||||||
|
OrderItemStatusServed OrderItemStatus = "served"
|
||||||
|
OrderItemStatusCancelled OrderItemStatus = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderItem struct {
|
||||||
|
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
|
||||||
|
OrderID uuid.UUID `gorm:"type:uuid;not null;index" json:"order_id" validate:"required"`
|
||||||
|
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id" validate:"required"`
|
||||||
|
ProductVariantID *uuid.UUID `gorm:"type:uuid;index" json:"product_variant_id"`
|
||||||
|
Quantity int `gorm:"not null" json:"quantity" validate:"required,min=1"`
|
||||||
|
UnitPrice float64 `gorm:"type:decimal(10,2);not null" json:"unit_price" validate:"required,min=0"`
|
||||||
|
TotalPrice float64 `gorm:"type:decimal(10,2);not null" json:"total_price" validate:"required,min=0"`
|
||||||
|
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"`
|
||||||
|
RefundAmount float64 `gorm:"type:decimal(10,2);default:0.00" json:"refund_amount"`
|
||||||
|
RefundQuantity int `gorm:"default:0" json:"refund_quantity"`
|
||||||
|
IsPartiallyRefunded bool `gorm:"default:false" json:"is_partially_refunded"`
|
||||||
|
IsFullyRefunded bool `gorm:"default:false" json:"is_fully_refunded"`
|
||||||
|
RefundReason *string `gorm:"size:255" json:"refund_reason,omitempty"`
|
||||||
|
RefundedAt *time.Time `gorm:"" json:"refunded_at,omitempty"`
|
||||||
|
RefundedBy *uuid.UUID `gorm:"type:uuid" json:"refunded_by,omitempty"`
|
||||||
|
Modifiers Modifiers `gorm:"type:jsonb;default:'[]'" json:"modifiers"`
|
||||||
|
Notes *string `gorm:"size:500" json:"notes,omitempty"`
|
||||||
|
Metadata Metadata `gorm:"type:jsonb;default:'{}'" json:"metadata"`
|
||||||
|
Status OrderItemStatus `gorm:"default:'pending';size:50" json:"status"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||||
|
|
||||||
|
Order Order `gorm:"foreignKey:OrderID" json:"order,omitempty"`
|
||||||
|
Product Product `gorm:"foreignKey:ProductID" json:"product,omitempty"`
|
||||||
|
ProductVariant *ProductVariant `gorm:"foreignKey:ProductVariantID" json:"product_variant,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oi *OrderItem) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if oi.ID == uuid.Nil {
|
||||||
|
oi.ID = uuid.New()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OrderItem) TableName() string {
|
||||||
|
return "order_items"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oi *OrderItem) CalculateTotalPrice() {
|
||||||
|
oi.TotalPrice = float64(oi.Quantity) * oi.UnitPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (oi *OrderItem) CanBeModified() bool {
|
||||||
|
return oi.Status == OrderItemStatusPending
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user