Update
This commit is contained in:
parent
a759e0f57c
commit
96743cf50b
292
ANALYTICS_API.md
292
ANALYTICS_API.md
@ -1,292 +0,0 @@
|
||||
# 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
|
||||
@ -1,120 +0,0 @@
|
||||
# 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.
|
||||
@ -1,155 +0,0 @@
|
||||
# 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
|
||||
@ -1,157 +0,0 @@
|
||||
# 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.
|
||||
@ -1,127 +0,0 @@
|
||||
# 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
|
||||
@ -1,241 +0,0 @@
|
||||
# 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
|
||||
@ -1,330 +0,0 @@
|
||||
# Table Management API
|
||||
|
||||
This document describes the Table Management API endpoints for managing restaurant tables in the POS system.
|
||||
|
||||
## Overview
|
||||
|
||||
The Table Management API allows you to:
|
||||
- Create, read, update, and delete tables
|
||||
- Manage table status (available, occupied, reserved, cleaning, maintenance)
|
||||
- Occupy and release tables with orders
|
||||
- Track table positions and capacity
|
||||
- Get available and occupied tables for specific outlets
|
||||
|
||||
## Table Entity
|
||||
|
||||
A table has the following properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"organization_id": "uuid",
|
||||
"outlet_id": "uuid",
|
||||
"table_name": "string",
|
||||
"start_time": "datetime (optional)",
|
||||
"status": "available|occupied|reserved|cleaning|maintenance",
|
||||
"order_id": "uuid (optional)",
|
||||
"payment_amount": "decimal",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal",
|
||||
"capacity": "integer (1-20)",
|
||||
"is_active": "boolean",
|
||||
"metadata": "object",
|
||||
"created_at": "datetime",
|
||||
"updated_at": "datetime",
|
||||
"order": "OrderResponse (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Create Table
|
||||
|
||||
**POST** `/api/v1/tables`
|
||||
|
||||
Create a new table for an outlet.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"outlet_id": "uuid",
|
||||
"table_name": "string",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal",
|
||||
"capacity": "integer (1-20)",
|
||||
"metadata": "object (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `201 Created`
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"organization_id": "uuid",
|
||||
"outlet_id": "uuid",
|
||||
"table_name": "string",
|
||||
"status": "available",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal",
|
||||
"capacity": "integer",
|
||||
"is_active": true,
|
||||
"metadata": "object",
|
||||
"created_at": "datetime",
|
||||
"updated_at": "datetime"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Get Table by ID
|
||||
|
||||
**GET** `/api/v1/tables/{id}`
|
||||
|
||||
Get table details by ID.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"organization_id": "uuid",
|
||||
"outlet_id": "uuid",
|
||||
"table_name": "string",
|
||||
"start_time": "datetime (optional)",
|
||||
"status": "string",
|
||||
"order_id": "uuid (optional)",
|
||||
"payment_amount": "decimal",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal",
|
||||
"capacity": "integer",
|
||||
"is_active": "boolean",
|
||||
"metadata": "object",
|
||||
"created_at": "datetime",
|
||||
"updated_at": "datetime",
|
||||
"order": "OrderResponse (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update Table
|
||||
|
||||
**PUT** `/api/v1/tables/{id}`
|
||||
|
||||
Update table details.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"table_name": "string (optional)",
|
||||
"status": "available|occupied|reserved|cleaning|maintenance (optional)",
|
||||
"position_x": "decimal (optional)",
|
||||
"position_y": "decimal (optional)",
|
||||
"capacity": "integer (1-20) (optional)",
|
||||
"is_active": "boolean (optional)",
|
||||
"metadata": "object (optional)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK` - Updated table object
|
||||
|
||||
### 4. Delete Table
|
||||
|
||||
**DELETE** `/api/v1/tables/{id}`
|
||||
|
||||
Delete a table. Cannot delete occupied tables.
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
### 5. List Tables
|
||||
|
||||
**GET** `/api/v1/tables`
|
||||
|
||||
Get paginated list of tables with optional filters.
|
||||
|
||||
**Query Parameters:**
|
||||
- `organization_id` (optional): Filter by organization
|
||||
- `outlet_id` (optional): Filter by outlet
|
||||
- `status` (optional): Filter by status
|
||||
- `is_active` (optional): Filter by active status
|
||||
- `search` (optional): Search in table names
|
||||
- `page` (default: 1): Page number
|
||||
- `limit` (default: 10, max: 100): Page size
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
{
|
||||
"tables": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"table_name": "string",
|
||||
"status": "string",
|
||||
"capacity": "integer",
|
||||
"is_active": "boolean",
|
||||
"created_at": "datetime",
|
||||
"updated_at": "datetime"
|
||||
}
|
||||
],
|
||||
"total_count": "integer",
|
||||
"page": "integer",
|
||||
"limit": "integer",
|
||||
"total_pages": "integer"
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Occupy Table
|
||||
|
||||
**POST** `/api/v1/tables/{id}/occupy`
|
||||
|
||||
Occupy a table with an order.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"order_id": "uuid",
|
||||
"start_time": "datetime"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK` - Updated table object with order information
|
||||
|
||||
### 7. Release Table
|
||||
|
||||
**POST** `/api/v1/tables/{id}/release`
|
||||
|
||||
Release a table and record payment amount.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"payment_amount": "decimal"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `200 OK` - Updated table object
|
||||
|
||||
### 8. Get Available Tables
|
||||
|
||||
**GET** `/api/v1/outlets/{outlet_id}/tables/available`
|
||||
|
||||
Get list of available tables for a specific outlet.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"table_name": "string",
|
||||
"status": "available",
|
||||
"capacity": "integer",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 9. Get Occupied Tables
|
||||
|
||||
**GET** `/api/v1/outlets/{outlet_id}/tables/occupied`
|
||||
|
||||
Get list of occupied tables for a specific outlet.
|
||||
|
||||
**Response:** `200 OK`
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "uuid",
|
||||
"table_name": "string",
|
||||
"status": "occupied",
|
||||
"start_time": "datetime",
|
||||
"order_id": "uuid",
|
||||
"capacity": "integer",
|
||||
"position_x": "decimal",
|
||||
"position_y": "decimal",
|
||||
"order": "OrderResponse"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Table Statuses
|
||||
|
||||
- **available**: Table is free and ready for use
|
||||
- **occupied**: Table is currently in use with an order
|
||||
- **reserved**: Table is reserved for future use
|
||||
- **cleaning**: Table is being cleaned
|
||||
- **maintenance**: Table is under maintenance
|
||||
|
||||
## Business Rules
|
||||
|
||||
1. **Table Creation**: Tables must have unique names within an outlet
|
||||
2. **Table Occupation**: Only available or cleaning tables can be occupied
|
||||
3. **Table Release**: Only occupied tables can be released
|
||||
4. **Table Deletion**: Occupied tables cannot be deleted
|
||||
5. **Capacity**: Table capacity must be between 1 and 20
|
||||
6. **Position**: Tables have X and Y coordinates for layout positioning
|
||||
|
||||
## Error Responses
|
||||
|
||||
**400 Bad Request:**
|
||||
```json
|
||||
{
|
||||
"error": "Error description",
|
||||
"message": "Detailed error message"
|
||||
}
|
||||
```
|
||||
|
||||
**404 Not Found:**
|
||||
```json
|
||||
{
|
||||
"error": "Table not found",
|
||||
"message": "Table with specified ID does not exist"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Internal Server Error:**
|
||||
```json
|
||||
{
|
||||
"error": "Failed to create table",
|
||||
"message": "Database error or other internal error"
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require authentication via JWT token in the Authorization header:
|
||||
|
||||
```
|
||||
Authorization: Bearer <jwt_token>
|
||||
```
|
||||
|
||||
## Authorization
|
||||
|
||||
All table management endpoints require admin or manager role permissions.
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Creating a Table
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/tables \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{
|
||||
"outlet_id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"table_name": "Table 1",
|
||||
"position_x": 100.0,
|
||||
"position_y": 200.0,
|
||||
"capacity": 4
|
||||
}'
|
||||
```
|
||||
|
||||
### Occupying a Table
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/tables/123e4567-e89b-12d3-a456-426614174000/occupy \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{
|
||||
"order_id": "123e4567-e89b-12d3-a456-426614174001",
|
||||
"start_time": "2024-01-15T10:30:00Z"
|
||||
}'
|
||||
```
|
||||
|
||||
### Getting Available Tables
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/v1/outlets/123e4567-e89b-12d3-a456-426614174000/tables/available \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/client"
|
||||
"apskel-pos-be/internal/transformer"
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
@ -74,6 +75,7 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||
validators.paymentMethodValidator,
|
||||
services.analyticsService,
|
||||
services.tableService,
|
||||
validators.tableValidator,
|
||||
)
|
||||
|
||||
return nil
|
||||
@ -213,7 +215,7 @@ type services struct {
|
||||
fileService service.FileService
|
||||
customerService service.CustomerService
|
||||
analyticsService *service.AnalyticsServiceImpl
|
||||
tableService *service.TableService
|
||||
tableService *service.TableServiceImpl
|
||||
}
|
||||
|
||||
func (a *App) initServices(processors *processors, cfg *config.Config) *services {
|
||||
|
||||
@ -11,6 +11,7 @@ const (
|
||||
MalformedFieldErrorCode = "310"
|
||||
ValidationErrorCode = "304"
|
||||
InvalidFieldErrorCode = "305"
|
||||
NotFoundErrorCode = "404"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -37,6 +38,7 @@ const (
|
||||
PaymentMethodValidatorEntity = "payment_method_validator"
|
||||
PaymentMethodHandlerEntity = "payment_method_handler"
|
||||
OutletServiceEntity = "outlet_service"
|
||||
TableEntity = "table"
|
||||
)
|
||||
|
||||
var HttpErrorMap = map[string]int{
|
||||
@ -45,6 +47,7 @@ var HttpErrorMap = map[string]int{
|
||||
MalformedFieldErrorCode: http.StatusBadRequest,
|
||||
ValidationErrorCode: http.StatusBadRequest,
|
||||
InvalidFieldErrorCode: http.StatusBadRequest,
|
||||
NotFoundErrorCode: http.StatusNotFound,
|
||||
}
|
||||
|
||||
// Error messages
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Inventory Request DTOs
|
||||
type CreateInventoryRequest struct {
|
||||
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
|
||||
ProductID uuid.UUID `json:"product_id" validate:"required"`
|
||||
@ -37,7 +35,6 @@ type ListInventoryRequest struct {
|
||||
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"`
|
||||
|
||||
@ -36,7 +36,7 @@ func (t *Table) BeforeCreate(tx *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (Table) TableName() string {
|
||||
func (Table) GetTableName() string {
|
||||
return "tables"
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/service"
|
||||
"net/http"
|
||||
"apskel-pos-be/internal/logger"
|
||||
"apskel-pos-be/internal/util"
|
||||
"apskel-pos-be/internal/validator"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -11,189 +14,126 @@ import (
|
||||
)
|
||||
|
||||
type TableHandler struct {
|
||||
tableService *service.TableService
|
||||
tableService TableService
|
||||
tableValidator *validator.TableValidator
|
||||
}
|
||||
|
||||
func NewTableHandler(tableService *service.TableService) *TableHandler {
|
||||
func NewTableHandler(tableService TableService, tableValidator *validator.TableValidator) *TableHandler {
|
||||
return &TableHandler{
|
||||
tableService: tableService,
|
||||
tableValidator: tableValidator,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTable godoc
|
||||
// @Summary Create a new table
|
||||
// @Description Create a new table for the organization
|
||||
// @Tags tables
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param table body contract.CreateTableRequest true "Table data"
|
||||
// @Success 201 {object} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 401 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables [post]
|
||||
func (h *TableHandler) Create(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
contextInfo := appcontext.FromGinContext(ctx)
|
||||
|
||||
var req contract.CreateTableRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Create -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
organizationID := c.GetString("organization_id")
|
||||
orgID, err := uuid.Parse(organizationID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid organization ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err := h.tableValidator.ValidateCreateTableRequest(req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Create -> validation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Create")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.tableService.Create(c.Request.Context(), req, orgID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to create table",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
response := h.tableService.CreateTable(ctx, contextInfo, &req)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::Create -> service failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::Create")
|
||||
}
|
||||
|
||||
// GetTable godoc
|
||||
// @Summary Get table by ID
|
||||
// @Description Get table details by ID
|
||||
// @Tags tables
|
||||
// @Produce json
|
||||
// @Param id path string true "Table ID"
|
||||
// @Success 200 {object} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 404 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables/{id} [get]
|
||||
func (h *TableHandler) GetByID(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
id := c.Param("id")
|
||||
tableID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid table ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GetByID -> Invalid table ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GetByID")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.tableService.GetByID(c.Request.Context(), tableID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, contract.ResponseError{
|
||||
Error: "Table not found",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
response := h.tableService.GetTableByID(ctx, tableID)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::GetByID -> service failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::GetByID")
|
||||
}
|
||||
|
||||
// UpdateTable godoc
|
||||
// @Summary Update table
|
||||
// @Description Update table details
|
||||
// @Tags tables
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Table ID"
|
||||
// @Param table body contract.UpdateTableRequest true "Table update data"
|
||||
// @Success 200 {object} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 404 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables/{id} [put]
|
||||
func (h *TableHandler) Update(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
id := c.Param("id")
|
||||
tableID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid table ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Update -> Invalid table ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.UpdateTableRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Update -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.tableService.Update(c.Request.Context(), tableID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to update table",
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err := h.tableValidator.ValidateUpdateTableRequest(req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Update -> validation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Update")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
response := h.tableService.UpdateTable(ctx, tableID, &req)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::Update -> service failed")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::Update")
|
||||
}
|
||||
|
||||
// DeleteTable godoc
|
||||
// @Summary Delete table
|
||||
// @Description Delete table by ID
|
||||
// @Tags tables
|
||||
// @Produce json
|
||||
// @Param id path string true "Table ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 404 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables/{id} [delete]
|
||||
func (h *TableHandler) Delete(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
id := c.Param("id")
|
||||
tableID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid table ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::Delete -> Invalid table ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::Delete")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.tableService.Delete(c.Request.Context(), tableID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to delete table",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
response := h.tableService.DeleteTable(ctx, tableID)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::Delete -> service failed")
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::Delete")
|
||||
}
|
||||
|
||||
// ListTables godoc
|
||||
// @Summary List tables
|
||||
// @Description Get paginated list of tables
|
||||
// @Tags tables
|
||||
// @Produce json
|
||||
// @Param organization_id query string false "Organization ID"
|
||||
// @Param outlet_id query string false "Outlet ID"
|
||||
// @Param status query string false "Table status"
|
||||
// @Param is_active query string false "Is active"
|
||||
// @Param search query string false "Search term"
|
||||
// @Param page query int false "Page number" default(1)
|
||||
// @Param limit query int false "Page size" default(10)
|
||||
// @Success 200 {object} contract.ListTablesResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables [get]
|
||||
func (h *TableHandler) List(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
query := contract.ListTablesQuery{
|
||||
OrganizationID: c.Query("organization_id"),
|
||||
OutletID: c.Query("outlet_id"),
|
||||
@ -216,170 +156,132 @@ func (h *TableHandler) List(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
response, err := h.tableService.List(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to list tables",
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err := h.tableValidator.ValidateListTablesQuery(query); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::List -> validation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::List")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
response := h.tableService.ListTables(ctx, &query)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::List -> service failed")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::List")
|
||||
}
|
||||
|
||||
// OccupyTable godoc
|
||||
// @Summary Occupy table
|
||||
// @Description Occupy a table with an order
|
||||
// @Tags tables
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Table ID"
|
||||
// @Param request body contract.OccupyTableRequest true "Occupy table data"
|
||||
// @Success 200 {object} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 404 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables/{id}/occupy [post]
|
||||
func (h *TableHandler) OccupyTable(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
id := c.Param("id")
|
||||
tableID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid table ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::OccupyTable -> Invalid table ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::OccupyTable")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.OccupyTableRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::OccupyTable -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::OccupyTable")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.tableService.OccupyTable(c.Request.Context(), tableID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to occupy table",
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err := h.tableValidator.ValidateOccupyTableRequest(req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::OccupyTable -> validation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::OccupyTable")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
response := h.tableService.OccupyTable(ctx, tableID, &req)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::OccupyTable -> service failed")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::OccupyTable")
|
||||
}
|
||||
|
||||
// ReleaseTable godoc
|
||||
// @Summary Release table
|
||||
// @Description Release a table and record payment amount
|
||||
// @Tags tables
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Table ID"
|
||||
// @Param request body contract.ReleaseTableRequest true "Release table data"
|
||||
// @Success 200 {object} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 404 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /tables/{id}/release [post]
|
||||
func (h *TableHandler) ReleaseTable(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
id := c.Param("id")
|
||||
tableID, err := uuid.Parse(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid table ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::ReleaseTable -> Invalid table ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid table ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::ReleaseTable")
|
||||
return
|
||||
}
|
||||
|
||||
var req contract.ReleaseTableRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::ReleaseTable -> request binding failed")
|
||||
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::ReleaseTable")
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.tableService.ReleaseTable(c.Request.Context(), tableID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to release table",
|
||||
Message: err.Error(),
|
||||
})
|
||||
if err := h.tableValidator.ValidateReleaseTableRequest(req); err != nil {
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::ReleaseTable -> validation failed")
|
||||
validationResponseError := contract.NewResponseError(constants.ValidationErrorCode, constants.RequestEntity, err.Error())
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::ReleaseTable")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
response := h.tableService.ReleaseTable(ctx, tableID, &req)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::ReleaseTable -> service failed")
|
||||
}
|
||||
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::ReleaseTable")
|
||||
}
|
||||
|
||||
// GetAvailableTables godoc
|
||||
// @Summary Get available tables
|
||||
// @Description Get list of available tables for an outlet
|
||||
// @Tags tables
|
||||
// @Produce json
|
||||
// @Param outlet_id path string true "Outlet ID"
|
||||
// @Success 200 {array} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /outlets/{outlet_id}/tables/available [get]
|
||||
func (h *TableHandler) GetAvailableTables(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
outletIDStr := c.Param("outlet_id")
|
||||
outletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid outlet ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GetAvailableTables -> Invalid outlet ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GetAvailableTables")
|
||||
return
|
||||
}
|
||||
|
||||
tables, err := h.tableService.GetAvailableTables(c.Request.Context(), outletID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to get available tables",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
response := h.tableService.GetAvailableTables(ctx, outletID)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::GetAvailableTables -> service failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tables)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::GetAvailableTables")
|
||||
}
|
||||
|
||||
// GetOccupiedTables godoc
|
||||
// @Summary Get occupied tables
|
||||
// @Description Get list of occupied tables for an outlet
|
||||
// @Tags tables
|
||||
// @Produce json
|
||||
// @Param outlet_id path string true "Outlet ID"
|
||||
// @Success 200 {array} contract.TableResponse
|
||||
// @Failure 400 {object} contract.ResponseError
|
||||
// @Failure 500 {object} contract.ResponseError
|
||||
// @Router /outlets/{outlet_id}/tables/occupied [get]
|
||||
func (h *TableHandler) GetOccupiedTables(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
|
||||
outletIDStr := c.Param("outlet_id")
|
||||
outletID, err := uuid.Parse(outletIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, contract.ResponseError{
|
||||
Error: "Invalid outlet ID",
|
||||
Message: err.Error(),
|
||||
})
|
||||
logger.FromContext(ctx).WithError(err).Error("TableHandler::GetOccupiedTables -> Invalid outlet ID")
|
||||
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID")
|
||||
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "TableHandler::GetOccupiedTables")
|
||||
return
|
||||
}
|
||||
|
||||
tables, err := h.tableService.GetOccupiedTables(c.Request.Context(), outletID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, contract.ResponseError{
|
||||
Error: "Failed to get occupied tables",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
response := h.tableService.GetOccupiedTables(ctx, outletID)
|
||||
if response.HasErrors() {
|
||||
errorResp := response.GetErrors()[0]
|
||||
logger.FromContext(ctx).WithError(errorResp).Error("TableHandler::GetOccupiedTables -> service failed")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tables)
|
||||
util.HandleResponse(c.Writer, c.Request, response, "TableHandler::GetOccupiedTables")
|
||||
}
|
||||
|
||||
20
internal/handler/table_service.go
Normal file
20
internal/handler/table_service.go
Normal file
@ -0,0 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TableService interface {
|
||||
CreateTable(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateTableRequest) *contract.Response
|
||||
UpdateTable(ctx context.Context, id uuid.UUID, req *contract.UpdateTableRequest) *contract.Response
|
||||
DeleteTable(ctx context.Context, id uuid.UUID) *contract.Response
|
||||
GetTableByID(ctx context.Context, id uuid.UUID) *contract.Response
|
||||
ListTables(ctx context.Context, req *contract.ListTablesQuery) *contract.Response
|
||||
OccupyTable(ctx context.Context, tableID uuid.UUID, req *contract.OccupyTableRequest) *contract.Response
|
||||
ReleaseTable(ctx context.Context, tableID uuid.UUID, req *contract.ReleaseTableRequest) *contract.Response
|
||||
GetAvailableTables(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||
GetOccupiedTables(ctx context.Context, outletID uuid.UUID) *contract.Response
|
||||
}
|
||||
@ -14,10 +14,10 @@ import (
|
||||
|
||||
type TableProcessor struct {
|
||||
tableRepo *repository.TableRepository
|
||||
orderRepo *repository.OrderRepository
|
||||
orderRepo repository.OrderRepository
|
||||
}
|
||||
|
||||
func NewTableProcessor(tableRepo *repository.TableRepository, orderRepo *repository.OrderRepository) *TableProcessor {
|
||||
func NewTableProcessor(tableRepo *repository.TableRepository, orderRepo repository.OrderRepository) *TableProcessor {
|
||||
return &TableProcessor{
|
||||
tableRepo: tableRepo,
|
||||
orderRepo: orderRepo,
|
||||
@ -136,7 +136,7 @@ func (p *TableProcessor) OccupyTable(ctx context.Context, tableID uuid.UUID, req
|
||||
}
|
||||
|
||||
// Verify order exists
|
||||
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
_, err = p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, errors.New("order not found")
|
||||
}
|
||||
|
||||
@ -60,7 +60,8 @@ func NewRouter(cfg *config.Config,
|
||||
paymentMethodService service.PaymentMethodService,
|
||||
paymentMethodValidator validator.PaymentMethodValidator,
|
||||
analyticsService *service.AnalyticsServiceImpl,
|
||||
tableService *service.TableService) *Router {
|
||||
tableService *service.TableServiceImpl,
|
||||
tableValidator *validator.TableValidator) *Router {
|
||||
|
||||
return &Router{
|
||||
config: cfg,
|
||||
@ -78,7 +79,7 @@ func NewRouter(cfg *config.Config,
|
||||
customerHandler: handler.NewCustomerHandler(customerService, customerValidator),
|
||||
paymentMethodHandler: handler.NewPaymentMethodHandler(paymentMethodService, paymentMethodValidator),
|
||||
analyticsHandler: handler.NewAnalyticsHandler(analyticsService, transformer.NewTransformer()),
|
||||
tableHandler: handler.NewTableHandler(tableService),
|
||||
tableHandler: handler.NewTableHandler(tableService, tableValidator),
|
||||
authMiddleware: authMiddleware,
|
||||
}
|
||||
}
|
||||
@ -261,19 +262,13 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
|
||||
outlets.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
{
|
||||
outlets.GET("/list", r.outletHandler.ListOutlets)
|
||||
outlets.GET("/:id", r.outletHandler.GetOutlet)
|
||||
outlets.PUT("/:id", r.outletHandler.UpdateOutlet)
|
||||
outlets.GET("/detail/:id", r.outletHandler.GetOutlet)
|
||||
outlets.PUT("/detail/:id", r.outletHandler.UpdateOutlet)
|
||||
outlets.GET("/printer-setting/:outlet_id", r.outletSettingHandler.GetPrinterSettings)
|
||||
outlets.PUT("/printer-setting/:outlet_id", r.outletSettingHandler.UpdatePrinterSettings)
|
||||
outlets.GET("/:outlet_id/tables/available", r.tableHandler.GetAvailableTables)
|
||||
outlets.GET("/:outlet_id/tables/occupied", r.tableHandler.GetOccupiedTables)
|
||||
}
|
||||
|
||||
//outletPrinterSettings := protected.Group("/outlets/:outlet_id/settings")
|
||||
//outletPrinterSettings.Use(r.authMiddleware.RequireAdminOrManager())
|
||||
//{
|
||||
//
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,115 +1,84 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/processor"
|
||||
"apskel-pos-be/internal/transformer"
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TableService struct {
|
||||
type TableServiceImpl struct {
|
||||
tableProcessor *processor.TableProcessor
|
||||
tableTransformer *transformer.TableTransformer
|
||||
}
|
||||
|
||||
func NewTableService(tableProcessor *processor.TableProcessor, tableTransformer *transformer.TableTransformer) *TableService {
|
||||
return &TableService{
|
||||
func NewTableService(tableProcessor *processor.TableProcessor, tableTransformer *transformer.TableTransformer) *TableServiceImpl {
|
||||
return &TableServiceImpl{
|
||||
tableProcessor: tableProcessor,
|
||||
tableTransformer: tableTransformer,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TableService) Create(ctx context.Context, req contract.CreateTableRequest, organizationID uuid.UUID) (*contract.TableResponse, error) {
|
||||
modelReq := models.CreateTableRequest{
|
||||
OutletID: req.OutletID,
|
||||
TableName: req.TableName,
|
||||
PositionX: req.PositionX,
|
||||
PositionY: req.PositionY,
|
||||
Capacity: req.Capacity,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
func (s *TableServiceImpl) CreateTable(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateTableRequest) *contract.Response {
|
||||
modelReq := transformer.CreateTableRequestToModel(apctx, req)
|
||||
|
||||
response, err := s.tableProcessor.Create(ctx, modelReq, organizationID)
|
||||
response, err := s.tableProcessor.Create(ctx, modelReq, apctx.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return s.tableTransformer.ToContract(*response), nil
|
||||
contractResponse := s.tableTransformer.ToContract(*response)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableService) GetByID(ctx context.Context, id uuid.UUID) (*contract.TableResponse, error) {
|
||||
response, err := s.tableProcessor.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.tableTransformer.ToContract(*response), nil
|
||||
}
|
||||
|
||||
func (s *TableService) Update(ctx context.Context, id uuid.UUID, req contract.UpdateTableRequest) (*contract.TableResponse, error) {
|
||||
modelReq := models.UpdateTableRequest{
|
||||
TableName: req.TableName,
|
||||
PositionX: req.PositionX,
|
||||
PositionY: req.PositionY,
|
||||
Capacity: req.Capacity,
|
||||
IsActive: req.IsActive,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
if req.Status != nil {
|
||||
status := models.TableStatus(*req.Status)
|
||||
modelReq.Status = &status
|
||||
}
|
||||
func (s *TableServiceImpl) UpdateTable(ctx context.Context, id uuid.UUID, req *contract.UpdateTableRequest) *contract.Response {
|
||||
modelReq := transformer.UpdateTableRequestToModel(req)
|
||||
|
||||
response, err := s.tableProcessor.Update(ctx, id, modelReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return s.tableTransformer.ToContract(*response), nil
|
||||
contractResponse := s.tableTransformer.ToContract(*response)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return s.tableProcessor.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *TableService) List(ctx context.Context, query contract.ListTablesQuery) (*contract.ListTablesResponse, error) {
|
||||
req := models.ListTablesRequest{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
Search: query.Search,
|
||||
}
|
||||
|
||||
if query.OrganizationID != "" {
|
||||
if orgID, err := uuid.Parse(query.OrganizationID); err == nil {
|
||||
req.OrganizationID = &orgID
|
||||
}
|
||||
}
|
||||
|
||||
if query.OutletID != "" {
|
||||
if outletID, err := uuid.Parse(query.OutletID); err == nil {
|
||||
req.OutletID = &outletID
|
||||
}
|
||||
}
|
||||
|
||||
if query.Status != "" {
|
||||
status := models.TableStatus(query.Status)
|
||||
req.Status = &status
|
||||
}
|
||||
|
||||
if query.IsActive != "" {
|
||||
if isActive, err := strconv.ParseBool(query.IsActive); err == nil {
|
||||
req.IsActive = &isActive
|
||||
}
|
||||
}
|
||||
|
||||
response, err := s.tableProcessor.List(ctx, req)
|
||||
func (s *TableServiceImpl) DeleteTable(ctx context.Context, id uuid.UUID) *contract.Response {
|
||||
err := s.tableProcessor.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return contract.BuildSuccessResponse(map[string]interface{}{
|
||||
"message": "Table deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (s *TableServiceImpl) GetTableByID(ctx context.Context, id uuid.UUID) *contract.Response {
|
||||
response, err := s.tableProcessor.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
errorResp := contract.NewResponseError(constants.NotFoundErrorCode, constants.TableEntity, "Table not found")
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
contractResponse := s.tableTransformer.ToContract(*response)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableServiceImpl) ListTables(ctx context.Context, req *contract.ListTablesQuery) *contract.Response {
|
||||
modelReq := transformer.ListTablesQueryToModel(req)
|
||||
|
||||
response, err := s.tableProcessor.List(ctx, modelReq)
|
||||
if err != nil {
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
contractTables := make([]contract.TableResponse, len(response.Tables))
|
||||
@ -117,46 +86,48 @@ func (s *TableService) List(ctx context.Context, query contract.ListTablesQuery)
|
||||
contractTables[i] = *s.tableTransformer.ToContract(table)
|
||||
}
|
||||
|
||||
return &contract.ListTablesResponse{
|
||||
contractResponse := &contract.ListTablesResponse{
|
||||
Tables: contractTables,
|
||||
TotalCount: response.TotalCount,
|
||||
Page: response.Page,
|
||||
Limit: response.Limit,
|
||||
TotalPages: response.TotalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableService) OccupyTable(ctx context.Context, tableID uuid.UUID, req contract.OccupyTableRequest) (*contract.TableResponse, error) {
|
||||
modelReq := models.OccupyTableRequest{
|
||||
OrderID: req.OrderID,
|
||||
StartTime: req.StartTime,
|
||||
}
|
||||
func (s *TableServiceImpl) OccupyTable(ctx context.Context, tableID uuid.UUID, req *contract.OccupyTableRequest) *contract.Response {
|
||||
modelReq := transformer.OccupyTableRequestToModel(req)
|
||||
|
||||
response, err := s.tableProcessor.OccupyTable(ctx, tableID, modelReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return s.tableTransformer.ToContract(*response), nil
|
||||
contractResponse := s.tableTransformer.ToContract(*response)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableService) ReleaseTable(ctx context.Context, tableID uuid.UUID, req contract.ReleaseTableRequest) (*contract.TableResponse, error) {
|
||||
modelReq := models.ReleaseTableRequest{
|
||||
PaymentAmount: req.PaymentAmount,
|
||||
}
|
||||
func (s *TableServiceImpl) ReleaseTable(ctx context.Context, tableID uuid.UUID, req *contract.ReleaseTableRequest) *contract.Response {
|
||||
modelReq := transformer.ReleaseTableRequestToModel(req)
|
||||
|
||||
response, err := s.tableProcessor.ReleaseTable(ctx, tableID, modelReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
return s.tableTransformer.ToContract(*response), nil
|
||||
contractResponse := s.tableTransformer.ToContract(*response)
|
||||
return contract.BuildSuccessResponse(contractResponse)
|
||||
}
|
||||
|
||||
func (s *TableService) GetAvailableTables(ctx context.Context, outletID uuid.UUID) ([]contract.TableResponse, error) {
|
||||
func (s *TableServiceImpl) GetAvailableTables(ctx context.Context, outletID uuid.UUID) *contract.Response {
|
||||
tables, err := s.tableProcessor.GetAvailableTables(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
responses := make([]contract.TableResponse, len(tables))
|
||||
@ -164,13 +135,14 @@ func (s *TableService) GetAvailableTables(ctx context.Context, outletID uuid.UUI
|
||||
responses[i] = *s.tableTransformer.ToContract(table)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
return contract.BuildSuccessResponse(responses)
|
||||
}
|
||||
|
||||
func (s *TableService) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) ([]contract.TableResponse, error) {
|
||||
func (s *TableServiceImpl) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) *contract.Response {
|
||||
tables, err := s.tableProcessor.GetOccupiedTables(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.TableEntity, err.Error())
|
||||
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
|
||||
}
|
||||
|
||||
responses := make([]contract.TableResponse, len(tables))
|
||||
@ -178,5 +150,5 @@ func (s *TableService) GetOccupiedTables(ctx context.Context, outletID uuid.UUID
|
||||
responses[i] = *s.tableTransformer.ToContract(table)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
return contract.BuildSuccessResponse(responses)
|
||||
}
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/contract"
|
||||
"apskel-pos-be/internal/models"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TableTransformer struct{}
|
||||
@ -33,10 +38,8 @@ func (t *TableTransformer) ToContract(model models.TableResponse) *contract.Tabl
|
||||
if model.Order != nil {
|
||||
response.Order = &contract.OrderResponse{
|
||||
ID: model.Order.ID,
|
||||
OrganizationID: model.Order.OrganizationID,
|
||||
OutletID: model.Order.OutletID,
|
||||
UserID: model.Order.UserID,
|
||||
CustomerID: model.Order.CustomerID,
|
||||
OrderNumber: model.Order.OrderNumber,
|
||||
TableNumber: model.Order.TableNumber,
|
||||
OrderType: string(model.Order.OrderType),
|
||||
@ -45,17 +48,6 @@ func (t *TableTransformer) ToContract(model models.TableResponse) *contract.Tabl
|
||||
TaxAmount: model.Order.TaxAmount,
|
||||
DiscountAmount: model.Order.DiscountAmount,
|
||||
TotalAmount: model.Order.TotalAmount,
|
||||
TotalCost: model.Order.TotalCost,
|
||||
PaymentStatus: string(model.Order.PaymentStatus),
|
||||
RefundAmount: model.Order.RefundAmount,
|
||||
IsVoid: model.Order.IsVoid,
|
||||
IsRefund: model.Order.IsRefund,
|
||||
VoidReason: model.Order.VoidReason,
|
||||
VoidedAt: model.Order.VoidedAt,
|
||||
VoidedBy: model.Order.VoidedBy,
|
||||
RefundReason: model.Order.RefundReason,
|
||||
RefundedAt: model.Order.RefundedAt,
|
||||
RefundedBy: model.Order.RefundedBy,
|
||||
Metadata: model.Order.Metadata,
|
||||
CreatedAt: model.Order.CreatedAt,
|
||||
UpdatedAt: model.Order.UpdatedAt,
|
||||
@ -65,56 +57,77 @@ func (t *TableTransformer) ToContract(model models.TableResponse) *contract.Tabl
|
||||
return response
|
||||
}
|
||||
|
||||
func (t *TableTransformer) ToModel(contract contract.TableResponse) *models.TableResponse {
|
||||
response := &models.TableResponse{
|
||||
ID: contract.ID,
|
||||
OrganizationID: contract.OrganizationID,
|
||||
OutletID: contract.OutletID,
|
||||
TableName: contract.TableName,
|
||||
StartTime: contract.StartTime,
|
||||
Status: models.TableStatus(contract.Status),
|
||||
OrderID: contract.OrderID,
|
||||
PaymentAmount: contract.PaymentAmount,
|
||||
PositionX: contract.PositionX,
|
||||
PositionY: contract.PositionY,
|
||||
Capacity: contract.Capacity,
|
||||
IsActive: contract.IsActive,
|
||||
Metadata: contract.Metadata,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
UpdatedAt: contract.UpdatedAt,
|
||||
func CreateTableRequestToModel(apctx *appcontext.ContextInfo, req *contract.CreateTableRequest) models.CreateTableRequest {
|
||||
return models.CreateTableRequest{
|
||||
OutletID: req.OutletID,
|
||||
TableName: req.TableName,
|
||||
PositionX: req.PositionX,
|
||||
PositionY: req.PositionY,
|
||||
Capacity: req.Capacity,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
if contract.Order != nil {
|
||||
response.Order = &models.OrderResponse{
|
||||
ID: contract.Order.ID,
|
||||
OrganizationID: contract.Order.OrganizationID,
|
||||
OutletID: contract.Order.OutletID,
|
||||
UserID: contract.Order.UserID,
|
||||
CustomerID: contract.Order.CustomerID,
|
||||
OrderNumber: contract.Order.OrderNumber,
|
||||
TableNumber: contract.Order.TableNumber,
|
||||
OrderType: models.OrderType(contract.Order.OrderType),
|
||||
Status: models.OrderStatus(contract.Order.Status),
|
||||
Subtotal: contract.Order.Subtotal,
|
||||
TaxAmount: contract.Order.TaxAmount,
|
||||
DiscountAmount: contract.Order.DiscountAmount,
|
||||
TotalAmount: contract.Order.TotalAmount,
|
||||
TotalCost: contract.Order.TotalCost,
|
||||
PaymentStatus: models.PaymentStatus(contract.Order.PaymentStatus),
|
||||
RefundAmount: contract.Order.RefundAmount,
|
||||
IsVoid: contract.Order.IsVoid,
|
||||
IsRefund: contract.Order.IsRefund,
|
||||
VoidReason: contract.Order.VoidReason,
|
||||
VoidedAt: contract.Order.VoidedAt,
|
||||
VoidedBy: contract.Order.VoidedBy,
|
||||
RefundReason: contract.Order.RefundReason,
|
||||
RefundedAt: contract.Order.RefundedAt,
|
||||
RefundedBy: contract.Order.RefundedBy,
|
||||
Metadata: contract.Order.Metadata,
|
||||
CreatedAt: contract.Order.CreatedAt,
|
||||
UpdatedAt: contract.Order.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func UpdateTableRequestToModel(req *contract.UpdateTableRequest) models.UpdateTableRequest {
|
||||
modelReq := models.UpdateTableRequest{
|
||||
TableName: req.TableName,
|
||||
PositionX: req.PositionX,
|
||||
PositionY: req.PositionY,
|
||||
Capacity: req.Capacity,
|
||||
IsActive: req.IsActive,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
if req.Status != nil {
|
||||
status := constants.TableStatus(*req.Status)
|
||||
modelReq.Status = &status
|
||||
}
|
||||
|
||||
return modelReq
|
||||
}
|
||||
|
||||
func OccupyTableRequestToModel(req *contract.OccupyTableRequest) models.OccupyTableRequest {
|
||||
return models.OccupyTableRequest{
|
||||
OrderID: req.OrderID,
|
||||
StartTime: req.StartTime,
|
||||
}
|
||||
}
|
||||
|
||||
func ReleaseTableRequestToModel(req *contract.ReleaseTableRequest) models.ReleaseTableRequest {
|
||||
return models.ReleaseTableRequest{
|
||||
PaymentAmount: req.PaymentAmount,
|
||||
}
|
||||
}
|
||||
|
||||
func ListTablesQueryToModel(req *contract.ListTablesQuery) models.ListTablesRequest {
|
||||
modelReq := models.ListTablesRequest{
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
Search: req.Search,
|
||||
}
|
||||
|
||||
if req.OrganizationID != "" {
|
||||
if orgID, err := uuid.Parse(req.OrganizationID); err == nil {
|
||||
modelReq.OrganizationID = &orgID
|
||||
}
|
||||
}
|
||||
|
||||
if req.OutletID != "" {
|
||||
if outletID, err := uuid.Parse(req.OutletID); err == nil {
|
||||
modelReq.OutletID = &outletID
|
||||
}
|
||||
}
|
||||
|
||||
if req.Status != "" {
|
||||
status := constants.TableStatus(req.Status)
|
||||
modelReq.Status = &status
|
||||
}
|
||||
|
||||
if req.IsActive != "" {
|
||||
if isActive, err := strconv.ParseBool(req.IsActive); err == nil {
|
||||
modelReq.IsActive = &isActive
|
||||
}
|
||||
}
|
||||
|
||||
return modelReq
|
||||
}
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
package validator
|
||||
|
||||
import "regexp"
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// Shared helper functions for validators
|
||||
func isValidEmail(email string) bool {
|
||||
@ -30,3 +36,27 @@ func isValidPlanType(planType string) bool {
|
||||
}
|
||||
return validPlanTypes[planType]
|
||||
}
|
||||
|
||||
func formatValidationError(err error) error {
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
var errorMessages []string
|
||||
for _, fieldError := range validationErrors {
|
||||
switch fieldError.Tag() {
|
||||
case "required":
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" is required")
|
||||
case "email":
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" must be a valid email")
|
||||
case "min":
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" must be at least "+fieldError.Param())
|
||||
case "max":
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" must be at most "+fieldError.Param())
|
||||
case "oneof":
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" must be one of: "+fieldError.Param())
|
||||
default:
|
||||
errorMessages = append(errorMessages, fieldError.Field()+" is invalid")
|
||||
}
|
||||
}
|
||||
return errors.New(strings.Join(errorMessages, "; "))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
1703
postman.json
Normal file
1703
postman.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,37 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test build script for apskel-pos-backend
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔨 Testing Go build..."
|
||||
|
||||
# Check Go version
|
||||
echo "Go version:"
|
||||
go version
|
||||
|
||||
# Clean previous builds
|
||||
echo "🧹 Cleaning previous builds..."
|
||||
rm -f server
|
||||
rm -rf tmp/
|
||||
|
||||
# Test local build
|
||||
echo "🏗️ Building application..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o server cmd/server/main.go
|
||||
|
||||
if [ -f "server" ]; then
|
||||
echo "✅ Build successful! Binary created: server"
|
||||
ls -la server
|
||||
|
||||
# Test if binary can run (quick test)
|
||||
echo "🧪 Testing binary..."
|
||||
timeout 5s ./server || true
|
||||
|
||||
echo "🧹 Cleaning up..."
|
||||
rm -f server
|
||||
|
||||
echo "✅ All tests passed! Docker build should work."
|
||||
else
|
||||
echo "❌ Build failed! Binary not created."
|
||||
exit 1
|
||||
fi
|
||||
@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for inventory movement functionality
|
||||
echo "Testing Inventory Movement Integration..."
|
||||
|
||||
# Build the application
|
||||
echo "Building application..."
|
||||
go build -o server cmd/server/main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful!"
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
migrate -path migrations -database "postgres://postgres:password@localhost:5432/pos_db?sslmode=disable" up
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Migration failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Migrations completed successfully!"
|
||||
|
||||
echo "Inventory Movement Integration Test Complete!"
|
||||
echo ""
|
||||
echo "Features implemented:"
|
||||
echo "1. ✅ Inventory Movement Table (migrations/000023_create_inventory_movements_table.up.sql)"
|
||||
echo "2. ✅ Inventory Movement Entity (internal/entities/inventory_movement.go)"
|
||||
echo "3. ✅ Inventory Movement Model (internal/models/inventory_movement.go)"
|
||||
echo "4. ✅ Inventory Movement Mapper (internal/mappers/inventory_movement_mapper.go)"
|
||||
echo "5. ✅ Inventory Movement Repository (internal/repository/inventory_movement_repository.go)"
|
||||
echo "6. ✅ Inventory Movement Processor (internal/processor/inventory_movement_processor.go)"
|
||||
echo "7. ✅ Transaction Isolation in Payment Processing"
|
||||
echo "8. ✅ Inventory Movement Integration with Payment Processor"
|
||||
echo "9. ✅ Inventory Movement Integration with Refund Processor"
|
||||
echo ""
|
||||
echo "Transaction Isolation Features:"
|
||||
echo "- All payment operations use database transactions"
|
||||
echo "- Inventory adjustments are atomic within payment transactions"
|
||||
echo "- Inventory movements are recorded with transaction isolation"
|
||||
echo "- Refund operations restore inventory with proper audit trail"
|
||||
echo ""
|
||||
echo "The system now tracks all inventory changes with:"
|
||||
echo "- Movement type (sale, refund, void, etc.)"
|
||||
echo "- Previous and new quantities"
|
||||
echo "- Cost tracking"
|
||||
echo "- Reference to orders and payments"
|
||||
echo "- User audit trail"
|
||||
echo "- Timestamps and metadata"
|
||||
@ -1,89 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for Product CRUD operations with image_url and printer_type
|
||||
echo "Testing Product CRUD Operations with Image URL and Printer Type..."
|
||||
|
||||
# Build the application
|
||||
echo "Building application..."
|
||||
go build -o server cmd/server/main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful!"
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
migrate -path migrations -database "postgres://postgres:password@localhost:5432/pos_db?sslmode=disable" up
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Migration failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Migrations completed successfully!"
|
||||
|
||||
echo "Product CRUD Operations with Image URL and Printer Type Test Complete!"
|
||||
echo ""
|
||||
echo "✅ Features implemented:"
|
||||
echo "1. ✅ Database Migration (migrations/000024_add_image_and_printer_type_to_products.up.sql)"
|
||||
echo "2. ✅ Product Entity Updated (internal/entities/product.go)"
|
||||
echo "3. ✅ Product Models Updated (internal/models/product.go)"
|
||||
echo "4. ✅ Product Mapper Updated (internal/mappers/product_mapper.go)"
|
||||
echo "5. ✅ Product Contract Updated (internal/contract/product_contract.go)"
|
||||
echo "6. ✅ Product Transformer Updated (internal/transformer/product_transformer.go)"
|
||||
echo "7. ✅ Product Validator Updated (internal/validator/product_validator.go)"
|
||||
echo ""
|
||||
echo "✅ CRUD Operations Updated:"
|
||||
echo "1. ✅ CREATE Product - Supports image_url and printer_type"
|
||||
echo "2. ✅ READ Product - Returns image_url and printer_type"
|
||||
echo "3. ✅ UPDATE Product - Supports updating image_url and printer_type"
|
||||
echo "4. ✅ DELETE Product - No changes needed"
|
||||
echo "5. ✅ LIST Products - Returns image_url and printer_type"
|
||||
echo ""
|
||||
echo "✅ API Contract Changes:"
|
||||
echo "- CreateProductRequest: Added image_url and printer_type fields"
|
||||
echo "- UpdateProductRequest: Added image_url and printer_type fields"
|
||||
echo "- ProductResponse: Added image_url and printer_type fields"
|
||||
echo ""
|
||||
echo "✅ Validation Rules:"
|
||||
echo "- image_url: Optional, max 500 characters"
|
||||
echo "- printer_type: Optional, max 50 characters, default 'kitchen'"
|
||||
echo ""
|
||||
echo "✅ Database Schema:"
|
||||
echo "- image_url: VARCHAR(500), nullable"
|
||||
echo "- printer_type: VARCHAR(50), default 'kitchen'"
|
||||
echo "- Index on printer_type for efficient filtering"
|
||||
echo ""
|
||||
echo "✅ Example API Usage:"
|
||||
echo ""
|
||||
echo "CREATE Product:"
|
||||
echo 'curl -X POST /api/products \\'
|
||||
echo ' -H "Content-Type: application/json" \\'
|
||||
echo ' -d "{'
|
||||
echo ' \"category_id\": \"uuid\",'
|
||||
echo ' \"name\": \"Pizza Margherita\",'
|
||||
echo ' \"price\": 12.99,'
|
||||
echo ' \"image_url\": \"https://example.com/pizza.jpg\",'
|
||||
echo ' \"printer_type\": \"kitchen\"'
|
||||
echo ' }"'
|
||||
echo ""
|
||||
echo "UPDATE Product:"
|
||||
echo 'curl -X PUT /api/products/{id} \\'
|
||||
echo ' -H "Content-Type: application/json" \\'
|
||||
echo ' -d "{'
|
||||
echo ' \"image_url\": \"https://example.com/new-pizza.jpg\",'
|
||||
echo ' \"printer_type\": \"bar\"'
|
||||
echo ' }"'
|
||||
echo ""
|
||||
echo "GET Product Response:"
|
||||
echo '{'
|
||||
echo ' \"id\": \"uuid\",'
|
||||
echo ' \"name\": \"Pizza Margherita\",'
|
||||
echo ' \"price\": 12.99,'
|
||||
echo ' \"image_url\": \"https://example.com/pizza.jpg\",'
|
||||
echo ' \"printer_type\": \"kitchen\",'
|
||||
echo ' \"is_active\": true'
|
||||
echo '}'
|
||||
@ -1,51 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for product image and printer_type functionality
|
||||
echo "Testing Product Image and Printer Type Integration..."
|
||||
|
||||
# Build the application
|
||||
echo "Building application..."
|
||||
go build -o server cmd/server/main.go
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful!"
|
||||
|
||||
# Run database migrations
|
||||
echo "Running database migrations..."
|
||||
migrate -path migrations -database "postgres://postgres:password@localhost:5432/pos_db?sslmode=disable" up
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Migration failed!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Migrations completed successfully!"
|
||||
|
||||
echo "Product Image and Printer Type Integration Test Complete!"
|
||||
echo ""
|
||||
echo "Features implemented:"
|
||||
echo "1. ✅ Database Migration (migrations/000024_add_image_and_printer_type_to_products.up.sql)"
|
||||
echo "2. ✅ Product Entity Updated (internal/entities/product.go)"
|
||||
echo "3. ✅ Product Models Updated (internal/models/product.go)"
|
||||
echo "4. ✅ Product Mapper Updated (internal/mappers/product_mapper.go)"
|
||||
echo "5. ✅ Default Printer Type: 'kitchen'"
|
||||
echo "6. ✅ Image URL Support (VARCHAR(500))"
|
||||
echo "7. ✅ Printer Type Support (VARCHAR(50))"
|
||||
echo ""
|
||||
echo "New Product Fields:"
|
||||
echo "- image_url: Optional image URL for product display"
|
||||
echo "- printer_type: Printer type for order printing (default: 'kitchen')"
|
||||
echo ""
|
||||
echo "API Changes:"
|
||||
echo "- CreateProductRequest: Added image_url and printer_type fields"
|
||||
echo "- UpdateProductRequest: Added image_url and printer_type fields"
|
||||
echo "- ProductResponse: Added image_url and printer_type fields"
|
||||
echo ""
|
||||
echo "Database Changes:"
|
||||
echo "- Added image_url column (VARCHAR(500), nullable)"
|
||||
echo "- Added printer_type column (VARCHAR(50), default 'kitchen')"
|
||||
echo "- Added index on printer_type for efficient filtering"
|
||||
@ -1,77 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test script for Table Management API
|
||||
# Make sure the server is running on localhost:8080
|
||||
|
||||
BASE_URL="http://localhost:8080/api/v1"
|
||||
TOKEN="your_jwt_token_here" # Replace with actual JWT token
|
||||
|
||||
echo "Testing Table Management API"
|
||||
echo "=========================="
|
||||
|
||||
# Test 1: Create a table
|
||||
echo -e "\n1. Creating a table..."
|
||||
CREATE_RESPONSE=$(curl -s -X POST "$BASE_URL/tables" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"outlet_id": "your_outlet_id_here",
|
||||
"table_name": "Table 1",
|
||||
"position_x": 100.0,
|
||||
"position_y": 200.0,
|
||||
"capacity": 4,
|
||||
"metadata": {
|
||||
"description": "Window table"
|
||||
}
|
||||
}')
|
||||
|
||||
echo "Create Response: $CREATE_RESPONSE"
|
||||
|
||||
# Extract table ID from response (you'll need to parse this)
|
||||
TABLE_ID=$(echo $CREATE_RESPONSE | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$TABLE_ID" ]; then
|
||||
echo "Created table with ID: $TABLE_ID"
|
||||
|
||||
# Test 2: Get table by ID
|
||||
echo -e "\n2. Getting table by ID..."
|
||||
GET_RESPONSE=$(curl -s -X GET "$BASE_URL/tables/$TABLE_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
echo "Get Response: $GET_RESPONSE"
|
||||
|
||||
# Test 3: Update table
|
||||
echo -e "\n3. Updating table..."
|
||||
UPDATE_RESPONSE=$(curl -s -X PUT "$BASE_URL/tables/$TABLE_ID" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-d '{
|
||||
"table_name": "Table 1 Updated",
|
||||
"capacity": 6,
|
||||
"position_x": 150.0,
|
||||
"position_y": 250.0
|
||||
}')
|
||||
echo "Update Response: $UPDATE_RESPONSE"
|
||||
|
||||
# Test 4: List tables
|
||||
echo -e "\n4. Listing tables..."
|
||||
LIST_RESPONSE=$(curl -s -X GET "$BASE_URL/tables?page=1&limit=10" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
echo "List Response: $LIST_RESPONSE"
|
||||
|
||||
# Test 5: Get available tables for outlet
|
||||
echo -e "\n5. Getting available tables..."
|
||||
AVAILABLE_RESPONSE=$(curl -s -X GET "$BASE_URL/outlets/your_outlet_id_here/tables/available" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
echo "Available Tables Response: $AVAILABLE_RESPONSE"
|
||||
|
||||
# Test 6: Delete table
|
||||
echo -e "\n6. Deleting table..."
|
||||
DELETE_RESPONSE=$(curl -s -X DELETE "$BASE_URL/tables/$TABLE_ID" \
|
||||
-H "Authorization: Bearer $TOKEN")
|
||||
echo "Delete Response: $DELETE_RESPONSE"
|
||||
|
||||
else
|
||||
echo "Failed to create table or extract table ID"
|
||||
fi
|
||||
|
||||
echo -e "\nAPI Testing completed!"
|
||||
Loading…
x
Reference in New Issue
Block a user