Add void print
This commit is contained in:
parent
53014d90ab
commit
1201b2e45b
463
docs/ADVANCED_ORDER_MANAGEMENT.md
Normal file
463
docs/ADVANCED_ORDER_MANAGEMENT.md
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
# Advanced Order Management API Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Advanced Order Management API provides comprehensive functionality for managing orders beyond basic operations. This includes partial refunds, void operations, and bill splitting capabilities.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ **Partial Refund**: Refund specific items from paid orders
|
||||||
|
- ✅ **Void Order**: Cancel ongoing orders (per item or entire order)
|
||||||
|
- ✅ **Split Bill**: Split orders by items or amounts
|
||||||
|
- ✅ **Order Status Management**: Support for PARTIAL and VOIDED statuses
|
||||||
|
- ✅ **Transaction Tracking**: Complete audit trail for all operations
|
||||||
|
- ✅ **Validation**: Comprehensive validation for all operations
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Partial Refund
|
||||||
|
|
||||||
|
**POST** `/order/partial-refund`
|
||||||
|
|
||||||
|
Refund specific items from a paid order while keeping the remaining items.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer returned damaged items",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"quantity": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_item_id": 789,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|--------------------------------|
|
||||||
|
| order_id | int64 | Yes | ID of the order to refund |
|
||||||
|
| reason | string | Yes | Reason for the partial refund |
|
||||||
|
| items | array | Yes | Array of items to refund |
|
||||||
|
|
||||||
|
#### Item Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---------------|--------|----------|--------------------------------|
|
||||||
|
| order_item_id | int64 | Yes | ID of the order item to refund |
|
||||||
|
| quantity | int | Yes | Quantity to refund (min: 1) |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
**Success (200 OK)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": 200,
|
||||||
|
"data": {
|
||||||
|
"order_id": 123,
|
||||||
|
"status": "PARTIAL",
|
||||||
|
"refunded_amount": 75000,
|
||||||
|
"remaining_amount": 25000,
|
||||||
|
"reason": "Customer returned damaged items",
|
||||||
|
"refunded_at": "2024-01-15T10:30:00Z",
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"payment_type": "CASH",
|
||||||
|
"refunded_items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"item_name": "Bakso Special",
|
||||||
|
"quantity": 2,
|
||||||
|
"unit_price": 25000,
|
||||||
|
"total_price": 50000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_item_id": 789,
|
||||||
|
"item_name": "Es Teh Manis",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit_price": 25000,
|
||||||
|
"total_price": 25000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Void Order
|
||||||
|
|
||||||
|
**POST** `/order/void`
|
||||||
|
|
||||||
|
Void an ongoing order (NEW or PENDING status) either entirely or by specific items.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
**Void Entire Order:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer cancelled order",
|
||||||
|
"type": "ALL"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Void Specific Items:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer changed mind about some items",
|
||||||
|
"type": "ITEM",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|--------------------------------|
|
||||||
|
| order_id | int64 | Yes | ID of the order to void |
|
||||||
|
| reason | string | Yes | Reason for voiding |
|
||||||
|
| type | string | Yes | Type: "ALL" or "ITEM" |
|
||||||
|
| items | array | No | Required if type is "ITEM" |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
**Success (200 OK)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": 200,
|
||||||
|
"data": {
|
||||||
|
"order_id": 123,
|
||||||
|
"status": "VOIDED",
|
||||||
|
"reason": "Customer cancelled order",
|
||||||
|
"voided_at": "2024-01-15T10:30:00Z",
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"voided_items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"item_name": "Bakso Special",
|
||||||
|
"quantity": 1,
|
||||||
|
"unit_price": 25000,
|
||||||
|
"total_price": 25000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Split Bill
|
||||||
|
|
||||||
|
**POST** `/order/split-bill`
|
||||||
|
|
||||||
|
Split an order into a separate order by items or amounts.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
**Split by Items:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"type": "ITEM",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"payment_provider": "CASH",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 789,
|
||||||
|
"quantity": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_item_id": 101,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Split by Amount:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"type": "AMOUNT",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"payment_provider": "CASH",
|
||||||
|
"amount": 50000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|------------------|--------|----------|--------------------------------|
|
||||||
|
| order_id | int64 | Yes | ID of the order to split |
|
||||||
|
| type | string | Yes | Type: "ITEM" or "AMOUNT" |
|
||||||
|
| payment_method | string | Yes | Payment method for split order |
|
||||||
|
| payment_provider | string | No | Payment provider for split order|
|
||||||
|
| items | array | No | Required if type is "ITEM" |
|
||||||
|
| amount | float | No | Required if type is "AMOUNT" (must be less than order total) |
|
||||||
|
|
||||||
|
#### Item Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|---------------|--------|----------|--------------------------------|
|
||||||
|
| order_item_id | int64 | Yes | ID of the order item to split |
|
||||||
|
| quantity | int | Yes | Quantity to split (min: 1) |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
**Success (200 OK)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": 200,
|
||||||
|
"data": {
|
||||||
|
"id": 124,
|
||||||
|
"partner_id": 1,
|
||||||
|
"status": "PAID",
|
||||||
|
"amount": 100000,
|
||||||
|
"total": 110000,
|
||||||
|
"tax": 10000,
|
||||||
|
"customer_id": 456,
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"payment_type": "CASH",
|
||||||
|
"payment_provider": "CASH",
|
||||||
|
"source": "POS",
|
||||||
|
"created_at": "2024-01-15T10:30:00Z",
|
||||||
|
"updated_at": "2024-01-15T10:30:00Z",
|
||||||
|
"order_items": [
|
||||||
|
{
|
||||||
|
"id": 789,
|
||||||
|
"item_id": 1,
|
||||||
|
"item_name": "Bakso Special",
|
||||||
|
"price": 50000,
|
||||||
|
"quantity": 2,
|
||||||
|
"subtotal": 100000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Business Logic
|
||||||
|
|
||||||
|
### Partial Refund Process
|
||||||
|
|
||||||
|
1. **Validation**
|
||||||
|
- Verify order exists and belongs to partner
|
||||||
|
- Ensure order status is "PAID"
|
||||||
|
- Validate refund items exist and quantities are valid
|
||||||
|
|
||||||
|
2. **Item Updates**
|
||||||
|
- Reduce quantities of refunded items
|
||||||
|
- Remove items completely if quantity becomes 0
|
||||||
|
- Recalculate order totals
|
||||||
|
|
||||||
|
3. **Order Status Update**
|
||||||
|
- Set status to "PARTIAL" if items remain
|
||||||
|
- Set status to "REFUNDED" if all items refunded
|
||||||
|
|
||||||
|
4. **Transaction Creation**
|
||||||
|
- Create refund transaction with negative amount
|
||||||
|
- Track refund details
|
||||||
|
|
||||||
|
### Void Order Process
|
||||||
|
|
||||||
|
1. **Validation**
|
||||||
|
- Verify order exists and belongs to partner
|
||||||
|
- Ensure order status is "NEW" or "PENDING"
|
||||||
|
- Validate void items if type is "ITEM"
|
||||||
|
|
||||||
|
2. **Void Operations**
|
||||||
|
- **ALL**: Set order status to "VOIDED"
|
||||||
|
- **ITEM**: Reduce quantities and recalculate totals
|
||||||
|
|
||||||
|
3. **Status Management**
|
||||||
|
- Set status to "PARTIAL" if items remain
|
||||||
|
- Set status to "VOIDED" if all items voided
|
||||||
|
|
||||||
|
### Split Bill Process
|
||||||
|
|
||||||
|
1. **Validation**
|
||||||
|
- Verify order exists and belongs to partner
|
||||||
|
- Ensure order status is "NEW" or "PENDING"
|
||||||
|
- Validate split configuration
|
||||||
|
|
||||||
|
2. **Split Operations**
|
||||||
|
- **ITEM**: Create new PAID order with specified items, reduce quantities in original order
|
||||||
|
- **AMOUNT**: Create new PAID order with specified amount, reduce amount in original order
|
||||||
|
|
||||||
|
3. **Order Management**
|
||||||
|
- Original order remains PENDING with reduced items/amount
|
||||||
|
- New split order becomes PAID with specified payment method
|
||||||
|
- Recalculate totals for both orders
|
||||||
|
|
||||||
|
## Order Status Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
NEW → PENDING → PAID → REFUNDED
|
||||||
|
↓ ↓ ↓
|
||||||
|
VOIDED VOIDED PARTIAL
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Error Responses
|
||||||
|
|
||||||
|
**Order Not Found (404)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"status": 404,
|
||||||
|
"message": "order not found"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Invalid Order Status (400)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"status": 400,
|
||||||
|
"message": "only paid order can be partially refunded"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Invalid Quantity (400)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"status": 400,
|
||||||
|
"message": "refund quantity 3 exceeds available quantity 2 for item 456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Split Amount Mismatch (400)**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"status": 400,
|
||||||
|
"message": "split amount 95000 must be less than order total 100000"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Schema Updates
|
||||||
|
|
||||||
|
### Orders Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- New statuses supported
|
||||||
|
ALTER TABLE orders ADD CONSTRAINT check_status
|
||||||
|
CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL'));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Order Items Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Support for quantity updates
|
||||||
|
ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
### Order Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
New OrderStatus = "NEW"
|
||||||
|
Paid OrderStatus = "PAID"
|
||||||
|
Cancel OrderStatus = "CANCEL"
|
||||||
|
Pending OrderStatus = "PENDING"
|
||||||
|
Refunded OrderStatus = "REFUNDED"
|
||||||
|
Voided OrderStatus = "VOIDED" // New
|
||||||
|
Partial OrderStatus = "PARTIAL" // New
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Examples
|
||||||
|
|
||||||
|
### cURL Examples
|
||||||
|
|
||||||
|
**Partial Refund:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/order/partial-refund \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer returned damaged items",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"quantity": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Void Order:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/order/void \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer cancelled order",
|
||||||
|
"type": "ALL"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Split Bill:**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/order/split-bill \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"type": "ITEM",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"payment_provider": "CASH",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"order_item_id": 456,
|
||||||
|
"quantity": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"order_item_id": 789,
|
||||||
|
"quantity": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Authorization**: Only authorized users can perform these operations
|
||||||
|
2. **Audit Trail**: All operations are logged with user and timestamp
|
||||||
|
3. **Validation**: Strict validation prevents invalid operations
|
||||||
|
4. **Data Integrity**: Transaction-based operations ensure consistency
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Bulk Operations**: Support for bulk partial refunds/voids
|
||||||
|
2. **Approval Workflow**: Multi-level approval for large operations
|
||||||
|
3. **Notification System**: Customer notifications for refunds/voids
|
||||||
|
4. **Analytics**: Dashboard for operation trends and analysis
|
||||||
|
5. **Integration**: Integration with inventory management systems
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or issues with the Advanced Order Management API, please contact the development team or create an issue in the project repository.
|
||||||
297
docs/IMPLEMENTATION_SUMMARY.md
Normal file
297
docs/IMPLEMENTATION_SUMMARY.md
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
# Advanced Order Management Implementation Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document summarizes the complete implementation of advanced order management features for the Enaklo POS backend system. The implementation includes three major features: **Partial Refund**, **Void Order**, and **Split Bill** functionality.
|
||||||
|
|
||||||
|
## 🎯 Implemented Features
|
||||||
|
|
||||||
|
### 1. Partial Refund System
|
||||||
|
**Purpose**: Allow refunding specific items from paid orders while keeping remaining items.
|
||||||
|
|
||||||
|
**Key Components**:
|
||||||
|
- ✅ **API Endpoint**: `POST /order/partial-refund`
|
||||||
|
- ✅ **Service Method**: `PartialRefundRequest()`
|
||||||
|
- ✅ **Repository Methods**: `UpdateOrderItem()`, `UpdateOrderTotals()`
|
||||||
|
- ✅ **Validation**: Order status, item existence, quantity validation
|
||||||
|
- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts
|
||||||
|
- ✅ **Status Management**: Updates order to "PARTIAL" or "REFUNDED"
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
```go
|
||||||
|
// Flow: PAID → PARTIAL/REFUNDED
|
||||||
|
// - Validate order is PAID
|
||||||
|
// - Reduce item quantities
|
||||||
|
// - Recalculate totals
|
||||||
|
// - Create refund transaction
|
||||||
|
// - Update order status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Void Order System
|
||||||
|
**Purpose**: Cancel ongoing orders (NEW/PENDING) either entirely or by specific items.
|
||||||
|
|
||||||
|
**Key Components**:
|
||||||
|
- ✅ **API Endpoint**: `POST /order/void`
|
||||||
|
- ✅ **Service Method**: `VoidOrderRequest()`
|
||||||
|
- ✅ **Two Modes**: "ALL" (entire order) or "ITEM" (specific items)
|
||||||
|
- ✅ **Validation**: Order status, item existence, quantity validation
|
||||||
|
- ✅ **Status Management**: Updates order to "VOIDED" or "PARTIAL"
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
```go
|
||||||
|
// Flow: NEW/PENDING → VOIDED/PARTIAL
|
||||||
|
// - Validate order is NEW or PENDING
|
||||||
|
// - ALL: Set status to VOIDED
|
||||||
|
// - ITEM: Reduce quantities, recalculate totals
|
||||||
|
// - Update order status accordingly
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Split Bill System
|
||||||
|
**Purpose**: Split orders into a separate order by items or amounts.
|
||||||
|
|
||||||
|
**Key Components**:
|
||||||
|
- ✅ **API Endpoint**: `POST /order/split-bill`
|
||||||
|
- ✅ **Service Method**: `SplitBillRequest()`
|
||||||
|
- ✅ **Two Modes**: "ITEM" (specify items) or "AMOUNT" (specify amount)
|
||||||
|
- ✅ **Order Creation**: Creates a new order for the split
|
||||||
|
- ✅ **Original Order**: Voids the original order after splitting
|
||||||
|
|
||||||
|
**Business Logic**:
|
||||||
|
```go
|
||||||
|
// Flow: NEW/PENDING → PENDING (reduced) + PAID (split)
|
||||||
|
// - Validate order is NEW or PENDING
|
||||||
|
// - ITEM: Create PAID order with specified items, reduce quantities in original
|
||||||
|
// - AMOUNT: Create PAID order with specified amount, reduce amount in original
|
||||||
|
// - Original order remains PENDING with reduced items/amount
|
||||||
|
// - New split order becomes PAID with specified payment method
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🏗️ Architecture Components
|
||||||
|
|
||||||
|
### 1. Constants & Status Management
|
||||||
|
```go
|
||||||
|
// Added new order statuses
|
||||||
|
const (
|
||||||
|
New OrderStatus = "NEW"
|
||||||
|
Paid OrderStatus = "PAID"
|
||||||
|
Cancel OrderStatus = "CANCEL"
|
||||||
|
Pending OrderStatus = "PENDING"
|
||||||
|
Refunded OrderStatus = "REFUNDED"
|
||||||
|
Voided OrderStatus = "VOIDED" // New
|
||||||
|
Partial OrderStatus = "PARTIAL" // New
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Entity Models
|
||||||
|
```go
|
||||||
|
// New entity types for request/response handling
|
||||||
|
type PartialRefundItem struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidItem struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillSplit struct {
|
||||||
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
CustomerID *int64 `json:"customer_id"`
|
||||||
|
Items []SplitBillItem `json:"items,omitempty"`
|
||||||
|
Amount float64 `json:"amount,omitempty"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Repository Layer
|
||||||
|
```go
|
||||||
|
// New repository methods
|
||||||
|
type Repository interface {
|
||||||
|
// ... existing methods
|
||||||
|
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
||||||
|
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Service Layer
|
||||||
|
```go
|
||||||
|
// New service methods
|
||||||
|
type Service interface {
|
||||||
|
// ... existing methods
|
||||||
|
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
|
||||||
|
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
|
||||||
|
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, splits []entity.SplitBillSplit) ([]*entity.Order, error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. HTTP Handlers
|
||||||
|
```go
|
||||||
|
// New API endpoints
|
||||||
|
func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
||||||
|
// ... existing routes
|
||||||
|
route.POST("/partial-refund", jwt, h.PartialRefund)
|
||||||
|
route.POST("/void", jwt, h.VoidOrder)
|
||||||
|
route.POST("/split-bill", jwt, h.SplitBill)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Order Status Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
NEW → PENDING → PAID → REFUNDED
|
||||||
|
↓ ↓ ↓
|
||||||
|
VOIDED VOIDED PARTIAL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status Transitions**:
|
||||||
|
- **NEW/PENDING** → **VOIDED**: When entire order is voided
|
||||||
|
- **NEW/PENDING** → **PARTIAL**: When some items are voided
|
||||||
|
- **PAID** → **PARTIAL**: When some items are refunded
|
||||||
|
- **PAID** → **REFUNDED**: When all items are refunded
|
||||||
|
|
||||||
|
## 🔒 Validation & Security
|
||||||
|
|
||||||
|
### Input Validation
|
||||||
|
- ✅ **Order Existence**: Verify order exists and belongs to partner
|
||||||
|
- ✅ **Status Validation**: Ensure appropriate status for operations
|
||||||
|
- ✅ **Item Validation**: Verify items exist and quantities are valid
|
||||||
|
- ✅ **Quantity Validation**: Prevent refunding/voiding more than available
|
||||||
|
- ✅ **Split Validation**: Ensure split amounts match order total
|
||||||
|
|
||||||
|
### Business Rules
|
||||||
|
- ✅ **Partial Refund**: Only PAID orders can be partially refunded
|
||||||
|
- ✅ **Void Order**: Only NEW/PENDING orders can be voided
|
||||||
|
- ✅ **Split Bill**: Only NEW/PENDING orders can be split
|
||||||
|
- ✅ **Transaction Tracking**: All operations create audit trails
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
- ✅ **Unit Tests**: Comprehensive test coverage for all service methods
|
||||||
|
- ✅ **Mock Testing**: Uses testify/mock for dependency mocking
|
||||||
|
- ✅ **Edge Cases**: Tests for invalid states and error conditions
|
||||||
|
- ✅ **Success Scenarios**: Tests for successful operations
|
||||||
|
|
||||||
|
### Test Files
|
||||||
|
- `internal/services/v2/order/refund_test.go` - Original refund tests
|
||||||
|
- `internal/services/v2/order/advanced_order_management_test.go` - New feature tests
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
- ✅ **REFUND_API.md**: Complete refund API documentation
|
||||||
|
- ✅ **ADVANCED_ORDER_MANAGEMENT.md**: Comprehensive feature documentation
|
||||||
|
- ✅ **IMPLEMENTATION_SUMMARY.md**: This summary document
|
||||||
|
|
||||||
|
### Documentation Features
|
||||||
|
- ✅ **Request/Response Examples**: Complete JSON examples
|
||||||
|
- ✅ **Error Handling**: Common error scenarios and responses
|
||||||
|
- ✅ **Business Logic**: Detailed process flows
|
||||||
|
- ✅ **cURL Examples**: Ready-to-use API testing commands
|
||||||
|
|
||||||
|
## 🚀 Usage Examples
|
||||||
|
|
||||||
|
### Partial Refund
|
||||||
|
```bash
|
||||||
|
curl -X POST /order/partial-refund \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer returned damaged items",
|
||||||
|
"items": [
|
||||||
|
{"order_item_id": 456, "quantity": 2}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Void Order
|
||||||
|
```bash
|
||||||
|
curl -X POST /order/void \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer cancelled order",
|
||||||
|
"type": "ALL"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Split Bill
|
||||||
|
```bash
|
||||||
|
curl -X POST /order/split-bill \
|
||||||
|
-H "Authorization: Bearer TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"type": "ITEM",
|
||||||
|
"payment_method": "CASH",
|
||||||
|
"payment_provider": "CASH",
|
||||||
|
"items": [
|
||||||
|
{"order_item_id": 456, "quantity": 1},
|
||||||
|
{"order_item_id": 789, "quantity": 1}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Database Considerations
|
||||||
|
|
||||||
|
### Schema Updates
|
||||||
|
```sql
|
||||||
|
-- New statuses supported
|
||||||
|
ALTER TABLE orders ADD CONSTRAINT check_status
|
||||||
|
CHECK (status IN ('NEW', 'PENDING', 'PAID', 'REFUNDED', 'VOIDED', 'PARTIAL'));
|
||||||
|
|
||||||
|
-- Support for quantity updates
|
||||||
|
ALTER TABLE order_items ADD COLUMN updated_at TIMESTAMP DEFAULT NOW();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Management
|
||||||
|
- ✅ **Atomic Operations**: All operations use database transactions
|
||||||
|
- ✅ **Rollback Support**: Failed operations are properly rolled back
|
||||||
|
- ✅ **Data Consistency**: Ensures order totals match item totals
|
||||||
|
|
||||||
|
## 🎯 Benefits
|
||||||
|
|
||||||
|
### Business Benefits
|
||||||
|
1. **Flexibility**: Support for complex order management scenarios
|
||||||
|
2. **Customer Satisfaction**: Handle partial returns and cancellations
|
||||||
|
3. **Operational Efficiency**: Streamlined bill splitting for groups
|
||||||
|
4. **Audit Trail**: Complete tracking of all order modifications
|
||||||
|
|
||||||
|
### Technical Benefits
|
||||||
|
1. **Scalable Architecture**: Clean separation of concerns
|
||||||
|
2. **Comprehensive Testing**: High test coverage ensures reliability
|
||||||
|
3. **Extensible Design**: Easy to add new order management features
|
||||||
|
4. **Documentation**: Complete API documentation for integration
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
### Potential Improvements
|
||||||
|
1. **Bulk Operations**: Support for bulk partial refunds/voids
|
||||||
|
2. **Approval Workflow**: Multi-level approval for large operations
|
||||||
|
3. **Notification System**: Customer notifications for refunds/voids
|
||||||
|
4. **Analytics Dashboard**: Order management trends and analysis
|
||||||
|
5. **Inventory Integration**: Automatic inventory updates for refunds/voids
|
||||||
|
|
||||||
|
### Integration Opportunities
|
||||||
|
1. **Payment Gateway**: Direct refund processing
|
||||||
|
2. **Customer Management**: Customer point adjustments
|
||||||
|
3. **Reporting System**: Enhanced order analytics
|
||||||
|
4. **Mobile App**: Real-time order management
|
||||||
|
|
||||||
|
## 📋 Implementation Checklist
|
||||||
|
|
||||||
|
- ✅ **Core Features**: All three main features implemented
|
||||||
|
- ✅ **API Endpoints**: Complete REST API implementation
|
||||||
|
- ✅ **Service Layer**: Business logic implementation
|
||||||
|
- ✅ **Repository Layer**: Database operations
|
||||||
|
- ✅ **Validation**: Comprehensive input validation
|
||||||
|
- ✅ **Error Handling**: Proper error responses
|
||||||
|
- ✅ **Testing**: Unit test coverage
|
||||||
|
- ✅ **Documentation**: Complete API documentation
|
||||||
|
- ✅ **Status Management**: New order statuses
|
||||||
|
- ✅ **Transaction Tracking**: Audit trail implementation
|
||||||
|
|
||||||
|
## 🎉 Conclusion
|
||||||
|
|
||||||
|
The Advanced Order Management system provides a comprehensive solution for complex order scenarios in the Enaklo POS system. The implementation follows best practices for scalability, maintainability, and reliability, with complete documentation and testing coverage.
|
||||||
|
|
||||||
|
The system is now ready for production use and provides the foundation for future enhancements and integrations.
|
||||||
271
docs/REFUND_API.md
Normal file
271
docs/REFUND_API.md
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
# Refund Order API Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Refund Order API provides comprehensive functionality to process refunds for paid orders. This includes order status updates, transaction creation, customer voucher reversal, payment gateway refunds, and customer notifications.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ✅ **Order Status Management**: Updates order status to "REFUNDED"
|
||||||
|
- ✅ **Transaction Tracking**: Creates refund transactions with negative amounts
|
||||||
|
- ✅ **Customer Voucher Reversal**: Reverses any vouchers/points given for the order
|
||||||
|
- ✅ **Payment Gateway Integration**: Handles refunds for non-cash payments
|
||||||
|
- ✅ **Customer Notifications**: Sends email notifications for refunds
|
||||||
|
- ✅ **Audit Trail**: Tracks who processed the refund and when
|
||||||
|
- ✅ **Refund History**: Provides endpoint to view refund history
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Process Refund
|
||||||
|
|
||||||
|
**POST** `/order/refund`
|
||||||
|
|
||||||
|
Process a refund for a paid order.
|
||||||
|
|
||||||
|
#### Request Body
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer request"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Request Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-----------|--------|----------|--------------------------------|
|
||||||
|
| order_id | int64 | Yes | ID of the order to refund |
|
||||||
|
| reason | string | Yes | Reason for the refund |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
**Success (200 OK)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": 200,
|
||||||
|
"data": {
|
||||||
|
"order_id": 123,
|
||||||
|
"status": "REFUNDED",
|
||||||
|
"refund_amount": 100000,
|
||||||
|
"reason": "Customer request",
|
||||||
|
"refunded_at": "2024-01-15T10:30:00Z",
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"payment_type": "CASH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error (400 Bad Request)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"status": 400,
|
||||||
|
"message": "only paid order can be refund"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Get Refund History
|
||||||
|
|
||||||
|
**GET** `/order/refund-history`
|
||||||
|
|
||||||
|
Retrieve refund history with filtering and pagination.
|
||||||
|
|
||||||
|
#### Query Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Description |
|
||||||
|
|-------------|--------|----------|--------------------------------|
|
||||||
|
| limit | int | No | Number of records (max 100) |
|
||||||
|
| offset | int | No | Number of records to skip |
|
||||||
|
| start_date | string | No | Start date (RFC3339 format) |
|
||||||
|
| end_date | string | No | End date (RFC3339 format) |
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
**Success (200 OK)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"status": 200,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"order_id": 123,
|
||||||
|
"customer_name": "John Doe",
|
||||||
|
"customer_id": 456,
|
||||||
|
"is_member": true,
|
||||||
|
"status": "REFUNDED",
|
||||||
|
"amount": 95000,
|
||||||
|
"total": 100000,
|
||||||
|
"payment_type": "CASH",
|
||||||
|
"table_number": "A1",
|
||||||
|
"order_type": "DINE_IN",
|
||||||
|
"created_at": "2024-01-15T09:00:00Z",
|
||||||
|
"refunded_at": "2024-01-15T10:30:00Z",
|
||||||
|
"tax": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paging_meta": {
|
||||||
|
"page": 1,
|
||||||
|
"total": 25,
|
||||||
|
"limit": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Business Logic
|
||||||
|
|
||||||
|
### Refund Process Flow
|
||||||
|
|
||||||
|
1. **Validation**
|
||||||
|
- Verify order exists and belongs to partner
|
||||||
|
- Ensure order status is "PAID"
|
||||||
|
- Validate refund reason
|
||||||
|
|
||||||
|
2. **Order Update**
|
||||||
|
- Update order status to "REFUNDED"
|
||||||
|
- Store refund reason in order description
|
||||||
|
- Update timestamp
|
||||||
|
|
||||||
|
3. **Transaction Creation**
|
||||||
|
- Create refund transaction with negative amount
|
||||||
|
- Set transaction type to "REFUND"
|
||||||
|
- Track who processed the refund
|
||||||
|
|
||||||
|
4. **Customer Voucher Reversal**
|
||||||
|
- Find vouchers associated with the order
|
||||||
|
- Mark vouchers as reversed/cancelled
|
||||||
|
- Adjust customer points if applicable
|
||||||
|
|
||||||
|
5. **Payment Gateway Refund**
|
||||||
|
- For non-cash payments, call payment gateway refund API
|
||||||
|
- Handle gateway response and errors
|
||||||
|
- Update transaction with gateway details
|
||||||
|
|
||||||
|
6. **Customer Notification**
|
||||||
|
- Send email notification to customer
|
||||||
|
- Include refund details and reason
|
||||||
|
- Provide transaction reference
|
||||||
|
|
||||||
|
### Supported Payment Methods
|
||||||
|
|
||||||
|
| Payment Method | Refund Handling |
|
||||||
|
|----------------|-----------------------------------|
|
||||||
|
| CASH | Manual refund (no gateway call) |
|
||||||
|
| QRIS | Gateway refund via provider API |
|
||||||
|
| CARD | Gateway refund via provider API |
|
||||||
|
| TRANSFER | Gateway refund via provider API |
|
||||||
|
| ONLINE | Gateway refund via provider API |
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
- **Order not found**: Returns 404 error
|
||||||
|
- **Order not paid**: Returns 400 error with message
|
||||||
|
- **Voucher reversal failure**: Logs warning but continues refund
|
||||||
|
- **Payment gateway failure**: Logs error but continues refund
|
||||||
|
- **Notification failure**: Logs warning but continues refund
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### Orders Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE orders ADD COLUMN description TEXT;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Refund transactions have negative amounts
|
||||||
|
-- Transaction type: "REFUND"
|
||||||
|
-- Status: "REFUND"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constants
|
||||||
|
|
||||||
|
### Order Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
New OrderStatus = "NEW"
|
||||||
|
Paid OrderStatus = "PAID"
|
||||||
|
Cancel OrderStatus = "CANCEL"
|
||||||
|
Pending OrderStatus = "PENDING"
|
||||||
|
Refunded OrderStatus = "REFUNDED" // New status
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
New PaymentStatus = "NEW"
|
||||||
|
Paid PaymentStatus = "PAID"
|
||||||
|
Cancel PaymentStatus = "CANCEL"
|
||||||
|
Refund PaymentStatus = "REFUND" // New status
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the refund tests:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./internal/services/v2/order -v -run TestRefund
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Authorization**: Only authorized users can process refunds
|
||||||
|
2. **Audit Trail**: All refunds are logged with user and timestamp
|
||||||
|
3. **Validation**: Strict validation prevents invalid refunds
|
||||||
|
4. **Rate Limiting**: Consider implementing rate limiting for refund endpoints
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Partial Refunds**: Support for refunding specific order items
|
||||||
|
2. **Refund Approval Workflow**: Multi-level approval for large refunds
|
||||||
|
3. **Refund Analytics**: Dashboard for refund trends and analysis
|
||||||
|
4. **Automated Refunds**: Integration with customer service systems
|
||||||
|
5. **Refund Templates**: Predefined refund reasons and templates
|
||||||
|
|
||||||
|
## Integration Examples
|
||||||
|
|
||||||
|
### cURL Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/order/refund \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
|
||||||
|
-d '{
|
||||||
|
"order_id": 123,
|
||||||
|
"reason": "Customer request"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Example
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const refundOrder = async (orderId, reason) => {
|
||||||
|
const response = await fetch('/api/v1/order/refund', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
order_id: orderId,
|
||||||
|
reason: reason
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For questions or issues with the refund API, please contact the development team or create an issue in the project repository.
|
||||||
6
go.mod
6
go.mod
@ -24,6 +24,7 @@ require (
|
|||||||
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
|
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
@ -54,12 +55,14 @@ require (
|
|||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||||
github.com/spf13/afero v1.9.5 // indirect
|
github.com/spf13/afero v1.9.5 // indirect
|
||||||
github.com/spf13/cast v1.5.1 // indirect
|
github.com/spf13/cast v1.5.1 // indirect
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/subosito/gotenv v1.4.2 // indirect
|
github.com/subosito/gotenv v1.4.2 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
@ -68,6 +71,7 @@ require (
|
|||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.8.0 // indirect
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
golang.org/x/arch v0.7.0 // indirect
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
|
golang.org/x/net v0.30.0 // indirect
|
||||||
golang.org/x/oauth2 v0.21.0 // indirect
|
golang.org/x/oauth2 v0.21.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.19.0 // indirect
|
||||||
@ -81,12 +85,12 @@ require (
|
|||||||
github.com/aws/aws-sdk-go v1.50.0
|
github.com/aws/aws-sdk-go v1.50.0
|
||||||
github.com/getbrevo/brevo-go v1.0.0
|
github.com/getbrevo/brevo-go v1.0.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
github.com/veritrans/go-midtrans v0.0.0-20210616100512-16326c5eeb00
|
||||||
github.com/xuri/excelize/v2 v2.9.0
|
github.com/xuri/excelize/v2 v2.9.0
|
||||||
go.uber.org/zap v1.21.0
|
go.uber.org/zap v1.21.0
|
||||||
golang.org/x/crypto v0.28.0
|
golang.org/x/crypto v0.28.0
|
||||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
|
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6
|
||||||
golang.org/x/net v0.30.0
|
|
||||||
gorm.io/driver/postgres v1.5.0
|
gorm.io/driver/postgres v1.5.0
|
||||||
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
|
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11
|
||||||
)
|
)
|
||||||
|
|||||||
1
go.sum
1
go.sum
@ -266,6 +266,7 @@ github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
|
|||||||
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
|||||||
@ -7,6 +7,9 @@ const (
|
|||||||
Paid OrderStatus = "PAID"
|
Paid OrderStatus = "PAID"
|
||||||
Cancel OrderStatus = "CANCEL"
|
Cancel OrderStatus = "CANCEL"
|
||||||
Pending OrderStatus = "PENDING"
|
Pending OrderStatus = "PENDING"
|
||||||
|
Refunded OrderStatus = "REFUNDED"
|
||||||
|
Voided OrderStatus = "VOIDED"
|
||||||
|
Partial OrderStatus = "PARTIAL"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b OrderStatus) toString() string {
|
func (b OrderStatus) toString() string {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ const (
|
|||||||
New PaymentStatus = "NEW"
|
New PaymentStatus = "NEW"
|
||||||
Paid PaymentStatus = "PAID"
|
Paid PaymentStatus = "PAID"
|
||||||
Cancel PaymentStatus = "CANCEL"
|
Cancel PaymentStatus = "CANCEL"
|
||||||
|
Refund PaymentStatus = "REFUND"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b PaymentStatus) toString() string {
|
func (b PaymentStatus) toString() string {
|
||||||
|
|||||||
@ -124,6 +124,30 @@ type OrderItemRequest struct {
|
|||||||
Notes string `json:"notes"`
|
Notes string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PartialRefundItem struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidItem struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillSplit struct {
|
||||||
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
CustomerID *int64 `json:"customer_id"`
|
||||||
|
Items []SplitBillItem `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||||
|
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillItem struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
CustomerName string `json:"customer_name" validate:"required"`
|
||||||
|
CustomerID *int64 `json:"customer_id"`
|
||||||
|
}
|
||||||
|
|
||||||
type OrderExecuteRequest struct {
|
type OrderExecuteRequest struct {
|
||||||
CreatedBy int64
|
CreatedBy int64
|
||||||
PartnerID int64
|
PartnerID int64
|
||||||
|
|||||||
@ -30,12 +30,15 @@ func (h *Handler) Route(group *gin.RouterGroup, jwt gin.HandlerFunc) {
|
|||||||
route.POST("/inquiry", jwt, h.Inquiry)
|
route.POST("/inquiry", jwt, h.Inquiry)
|
||||||
route.POST("/execute", jwt, h.Execute)
|
route.POST("/execute", jwt, h.Execute)
|
||||||
route.POST("/refund", jwt, h.Refund)
|
route.POST("/refund", jwt, h.Refund)
|
||||||
|
route.POST("/partial-refund", jwt, h.PartialRefund)
|
||||||
|
route.POST("/void", jwt, h.VoidOrder)
|
||||||
|
route.POST("/split-bill", jwt, h.SplitBill)
|
||||||
route.GET("/history", jwt, h.GetOrderHistory)
|
route.GET("/history", jwt, h.GetOrderHistory)
|
||||||
|
route.GET("/refund-history", jwt, h.GetRefundHistory)
|
||||||
route.GET("/payment-analysis", jwt, h.GetPaymentMethodAnalysis)
|
route.GET("/payment-analysis", jwt, h.GetPaymentMethodAnalysis)
|
||||||
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
|
route.GET("/revenue-overview", jwt, h.GetRevenueOverview)
|
||||||
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
|
route.GET("/sales-by-category", jwt, h.GetSalesByCategory)
|
||||||
route.GET("/popular-products", jwt, h.GetPopularProducts)
|
route.GET("/popular-products", jwt, h.GetPopularProducts)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type InquiryRequest struct {
|
type InquiryRequest struct {
|
||||||
@ -77,6 +80,123 @@ type RefundRequest struct {
|
|||||||
Reason string `json:"reason" validate:"required"`
|
Reason string `json:"reason" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PartialRefundRequest struct {
|
||||||
|
OrderID int64 `json:"order_id" validate:"required"`
|
||||||
|
Reason string `json:"reason" validate:"required"`
|
||||||
|
Items []PartialRefundItemRequest `json:"items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialRefundItemRequest struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidOrderRequest struct {
|
||||||
|
OrderID int64 `json:"order_id" validate:"required"`
|
||||||
|
Reason string `json:"reason" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=ALL ITEM"`
|
||||||
|
Items []VoidItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidItemRequest struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillRequest struct {
|
||||||
|
OrderID int64 `json:"order_id" validate:"required"`
|
||||||
|
Type string `json:"type" validate:"required,oneof=ITEM AMOUNT"`
|
||||||
|
PaymentMethod string `json:"payment_method" validate:"required"`
|
||||||
|
PaymentProvider string `json:"payment_provider"`
|
||||||
|
Items []SplitBillItemRequest `json:"items,omitempty" validate:"required_if=Type ITEM,dive"`
|
||||||
|
Amount float64 `json:"amount,omitempty" validate:"required_if=Type AMOUNT,min=0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillItemRequest struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id" validate:"required"`
|
||||||
|
Quantity int `json:"quantity" validate:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefundResponse struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RefundAmount float64 `json:"refund_amount"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
RefundedAt string `json:"refunded_at"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
PaymentType string `json:"payment_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefundHistoryResponse struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
CustomerID *int64 `json:"customer_id"`
|
||||||
|
IsMember bool `json:"is_member"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
PaymentType string `json:"payment_type"`
|
||||||
|
TableNumber string `json:"table_number"`
|
||||||
|
OrderType string `json:"order_type"`
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
RefundedAt string `json:"refunded_at"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialRefundResponse struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
RefundedAmount float64 `json:"refunded_amount"`
|
||||||
|
RemainingAmount float64 `json:"remaining_amount"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
RefundedAt string `json:"refunded_at"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
PaymentType string `json:"payment_type"`
|
||||||
|
RefundedItems []RefundedItemResponse `json:"refunded_items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefundedItemResponse struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id"`
|
||||||
|
ItemName string `json:"item_name"`
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidOrderResponse struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
VoidedAt string `json:"voided_at"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
VoidedItems []VoidedItemResponse `json:"voided_items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VoidedItemResponse struct {
|
||||||
|
OrderItemID int64 `json:"order_item_id"`
|
||||||
|
ItemName string `json:"item_name"`
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
UnitPrice float64 `json:"unit_price"`
|
||||||
|
TotalPrice float64 `json:"total_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitBillResponse struct {
|
||||||
|
OriginalOrderID int64 `json:"original_order_id"`
|
||||||
|
SplitOrders []SplitOrderResponse `json:"split_orders"`
|
||||||
|
SplitAt string `json:"split_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplitOrderResponse struct {
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
CustomerName string `json:"customer_name"`
|
||||||
|
CustomerID *int64 `json:"customer_id"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Tax float64 `json:"tax"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Items []response.OrderItemResponse `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) Inquiry(c *gin.Context) {
|
func (h *Handler) Inquiry(c *gin.Context) {
|
||||||
ctx := request.GetMyContext(c)
|
ctx := request.GetMyContext(c)
|
||||||
userID := ctx.RequestedBy()
|
userID := ctx.RequestedBy()
|
||||||
@ -181,9 +301,30 @@ func (h *Handler) Refund(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
order, err := h.service.GetOrderByID(ctx, req.OrderID)
|
||||||
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, response.BaseResponse{
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
|
Message: "Refund processed successfully",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
refundResponse := RefundResponse{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Status: order.Status,
|
||||||
|
RefundAmount: order.Total,
|
||||||
|
Reason: req.Reason,
|
||||||
|
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
CustomerName: order.CustomerName,
|
||||||
|
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: refundResponse,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,3 +629,292 @@ func (h *Handler) GetPopularProducts(c *gin.Context) {
|
|||||||
Data: popularProducts,
|
Data: popularProducts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) GetRefundHistory(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
partnerID := ctx.GetPartnerID()
|
||||||
|
|
||||||
|
limitStr := c.Query("limit")
|
||||||
|
offsetStr := c.Query("offset")
|
||||||
|
startDateStr := c.Query("start_date")
|
||||||
|
endDateStr := c.Query("end_date")
|
||||||
|
|
||||||
|
searchReq := entity.SearchRequest{}
|
||||||
|
|
||||||
|
limit := 20
|
||||||
|
if limitStr != "" {
|
||||||
|
parsedLimit, err := strconv.Atoi(limitStr)
|
||||||
|
if err == nil && parsedLimit > 0 {
|
||||||
|
limit = parsedLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > 100 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq.Limit = limit
|
||||||
|
|
||||||
|
offset := 0
|
||||||
|
if offsetStr != "" {
|
||||||
|
parsedOffset, err := strconv.Atoi(offsetStr)
|
||||||
|
if err == nil && parsedOffset >= 0 {
|
||||||
|
offset = parsedOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchReq.Offset = offset
|
||||||
|
|
||||||
|
// Set status to REFUNDED to get only refunded orders
|
||||||
|
searchReq.Status = "REFUNDED"
|
||||||
|
|
||||||
|
if startDateStr != "" {
|
||||||
|
startDate, err := time.Parse(time.RFC3339, startDateStr)
|
||||||
|
if err == nil {
|
||||||
|
searchReq.Start = startDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if endDateStr != "" {
|
||||||
|
endDate, err := time.Parse(time.RFC3339, endDateStr)
|
||||||
|
if err == nil {
|
||||||
|
searchReq.End = endDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orders, total, err := h.service.GetOrderHistory(ctx, *partnerID, searchReq)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responseData := []RefundHistoryResponse{}
|
||||||
|
for _, order := range orders {
|
||||||
|
responseData = append(responseData, RefundHistoryResponse{
|
||||||
|
OrderID: order.ID,
|
||||||
|
CustomerName: order.CustomerName,
|
||||||
|
CustomerID: order.CustomerID,
|
||||||
|
IsMember: order.IsMemberOrder(),
|
||||||
|
Status: order.Status,
|
||||||
|
Amount: order.Amount,
|
||||||
|
Total: order.Total,
|
||||||
|
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||||
|
TableNumber: order.TableNumber,
|
||||||
|
OrderType: order.OrderType,
|
||||||
|
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
Tax: order.Tax,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: responseData,
|
||||||
|
PagingMeta: &response.PagingMeta{
|
||||||
|
Page: offset + 1,
|
||||||
|
Total: int64(total),
|
||||||
|
Limit: limit,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) PartialRefund(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
var req PartialRefundRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(req); err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]entity.PartialRefundItem, len(req.Items))
|
||||||
|
for i, item := range req.Items {
|
||||||
|
items[i] = entity.PartialRefundItem{
|
||||||
|
OrderItemID: item.OrderItemID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.service.PartialRefundRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, items)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated order to return details
|
||||||
|
order, err := h.service.GetOrderByID(ctx, req.OrderID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Message: "Partial refund processed successfully",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate refunded amount
|
||||||
|
refundedAmount := 0.0
|
||||||
|
var refundedItems []RefundedItemResponse
|
||||||
|
|
||||||
|
for _, reqItem := range req.Items {
|
||||||
|
for _, orderItem := range order.OrderItems {
|
||||||
|
if orderItem.ID == reqItem.OrderItemID {
|
||||||
|
itemTotal := orderItem.Price * float64(reqItem.Quantity)
|
||||||
|
refundedAmount += itemTotal
|
||||||
|
|
||||||
|
refundedItems = append(refundedItems, RefundedItemResponse{
|
||||||
|
OrderItemID: orderItem.ID,
|
||||||
|
ItemName: orderItem.ItemName,
|
||||||
|
Quantity: reqItem.Quantity,
|
||||||
|
UnitPrice: orderItem.Price,
|
||||||
|
TotalPrice: itemTotal,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partialRefundResponse := PartialRefundResponse{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Status: order.Status,
|
||||||
|
RefundedAmount: refundedAmount,
|
||||||
|
RemainingAmount: order.Total,
|
||||||
|
Reason: req.Reason,
|
||||||
|
RefundedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
CustomerName: order.CustomerName,
|
||||||
|
PaymentType: h.formatPayment(order.PaymentType, order.PaymentProvider),
|
||||||
|
RefundedItems: refundedItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: partialRefundResponse,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) VoidOrder(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
var req VoidOrderRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(req); err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert request items to entity items
|
||||||
|
var items []entity.VoidItem
|
||||||
|
if req.Type == "ITEM" {
|
||||||
|
items = make([]entity.VoidItem, len(req.Items))
|
||||||
|
for i, item := range req.Items {
|
||||||
|
items[i] = entity.VoidItem{
|
||||||
|
OrderItemID: item.OrderItemID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.service.VoidOrderRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Reason, req.Type, items)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated order to return details
|
||||||
|
order, err := h.service.GetOrderByID(ctx, req.OrderID)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Message: "Order voided successfully",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var voidedItems []VoidedItemResponse
|
||||||
|
if req.Type == "ITEM" {
|
||||||
|
for _, reqItem := range req.Items {
|
||||||
|
for _, orderItem := range order.OrderItems {
|
||||||
|
if orderItem.ID == reqItem.OrderItemID {
|
||||||
|
itemTotal := orderItem.Price * float64(reqItem.Quantity)
|
||||||
|
|
||||||
|
voidedItems = append(voidedItems, VoidedItemResponse{
|
||||||
|
OrderItemID: orderItem.ID,
|
||||||
|
ItemName: orderItem.ItemName,
|
||||||
|
Quantity: reqItem.Quantity,
|
||||||
|
UnitPrice: orderItem.Price,
|
||||||
|
TotalPrice: itemTotal,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
voidOrderResponse := VoidOrderResponse{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Status: order.Status,
|
||||||
|
Reason: req.Reason,
|
||||||
|
VoidedAt: order.UpdatedAt.Format("2006-01-02T15:04:05Z"),
|
||||||
|
CustomerName: order.CustomerName,
|
||||||
|
VoidedItems: voidedItems,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: voidOrderResponse,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) SplitBill(c *gin.Context) {
|
||||||
|
ctx := request.GetMyContext(c)
|
||||||
|
|
||||||
|
var req SplitBillRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
validate := validator.New()
|
||||||
|
if err := validate.Struct(req); err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []entity.SplitBillItem
|
||||||
|
if req.Type == "ITEM" {
|
||||||
|
items = make([]entity.SplitBillItem, len(req.Items))
|
||||||
|
for i, item := range req.Items {
|
||||||
|
items[i] = entity.SplitBillItem{
|
||||||
|
OrderItemID: item.OrderItemID,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
splitOrder, err := h.service.SplitBillRequest(ctx, *ctx.GetPartnerID(), req.OrderID, req.Type, req.PaymentMethod, req.PaymentProvider, items, req.Amount)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorWrapper(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, response.BaseResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: http.StatusOK,
|
||||||
|
Data: response.MapToOrderResponse(&entity.OrderResponse{Order: splitOrder}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"enaklo-pos-be/internal/handlers/request"
|
"enaklo-pos-be/internal/handlers/request"
|
||||||
"enaklo-pos-be/internal/handlers/response"
|
"enaklo-pos-be/internal/handlers/response"
|
||||||
"enaklo-pos-be/internal/services"
|
"enaklo-pos-be/internal/services"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -50,6 +51,7 @@ func (h *Handler) Create(c *gin.Context) {
|
|||||||
|
|
||||||
var req request.Product
|
var req request.Product
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
response.ErrorWrapper(c, errors.ErrorBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TransactionDB struct {
|
type TransactionDB struct {
|
||||||
ID string `gorm:"primaryKey;column:id"`
|
ID string `gorm:"type:uuid;default:gen_random_uuid();primaryKey;column:id"`
|
||||||
OrderID int64 `gorm:"column:order_id"`
|
OrderID int64 `gorm:"column:order_id"`
|
||||||
Amount float64 `gorm:"column:amount"`
|
Amount float64 `gorm:"column:amount"`
|
||||||
PaymentMethod string `gorm:"column:payment_method"`
|
PaymentMethod string `gorm:"column:payment_method"`
|
||||||
|
|||||||
@ -41,6 +41,8 @@ type OrderRepository interface {
|
|||||||
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistoryByUserID(ctx mycontext.Context, userID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
FindByIDAndCustomerID(ctx mycontext.Context, id int64, customerID int64) (*entity.Order, error)
|
||||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||||
|
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
||||||
|
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type orderRepository struct {
|
type orderRepository struct {
|
||||||
@ -979,3 +981,47 @@ func (r *orderRepository) FindByIDAndCustomerID(ctx mycontext.Context, id int64,
|
|||||||
|
|
||||||
return order, nil
|
return order, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *orderRepository) UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
result := r.db.Model(&models.OrderItemDB{}).
|
||||||
|
Where("order_item_id = ?", orderItemID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"quantity": quantity,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return errors.Wrap(result.Error, "failed to update order item")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.ContextLogger(ctx).Warn("no order item updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *orderRepository) UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
result := r.db.Model(&models.OrderDB{}).
|
||||||
|
Where("id = ?", orderID).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"amount": amount,
|
||||||
|
"tax": tax,
|
||||||
|
"total": total,
|
||||||
|
"updated_at": now,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return errors.Wrap(result.Error, "failed to update order totals")
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
logger.ContextLogger(ctx).Warn("no order updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"enaklo-pos-be/internal/common/mycontext"
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
"enaklo-pos-be/internal/entity"
|
"enaklo-pos-be/internal/entity"
|
||||||
"enaklo-pos-be/internal/repository/models"
|
"enaklo-pos-be/internal/repository/models"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -51,7 +52,7 @@ func (r *transactionRepository) FindByOrderID(ctx mycontext.Context, orderID int
|
|||||||
|
|
||||||
func (r *transactionRepository) toTransactionDBModel(transaction *entity.Transaction) models.TransactionDB {
|
func (r *transactionRepository) toTransactionDBModel(transaction *entity.Transaction) models.TransactionDB {
|
||||||
return models.TransactionDB{
|
return models.TransactionDB{
|
||||||
ID: transaction.ID,
|
ID: uuid.New().String(),
|
||||||
OrderID: transaction.OrderID,
|
OrderID: transaction.OrderID,
|
||||||
Amount: transaction.Amount,
|
Amount: transaction.Amount,
|
||||||
PaymentMethod: transaction.PaymentMethod,
|
PaymentMethod: transaction.PaymentMethod,
|
||||||
|
|||||||
403
internal/services/v2/order/advanced_order_management.go
Normal file
403
internal/services/v2/order/advanced_order_management.go
Normal file
@ -0,0 +1,403 @@
|
|||||||
|
package order
|
||||||
|
|
||||||
|
import (
|
||||||
|
"enaklo-pos-be/internal/common/logger"
|
||||||
|
"enaklo-pos-be/internal/common/mycontext"
|
||||||
|
"enaklo-pos-be/internal/entity"
|
||||||
|
"fmt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *orderSvc) PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error {
|
||||||
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to find order for partial refund", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Status != "PAID" && order.Status != "PARTIAL" {
|
||||||
|
return errors.New("only paid order can be partially refunded")
|
||||||
|
}
|
||||||
|
|
||||||
|
refundedAmount := 0.0
|
||||||
|
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||||
|
|
||||||
|
for _, item := range order.OrderItems {
|
||||||
|
orderItemMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, refundItem := range items {
|
||||||
|
orderItem, exists := orderItemMap[refundItem.OrderItemID]
|
||||||
|
if !exists {
|
||||||
|
return errors.New(fmt.Sprintf("order item %d not found", refundItem.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if refundItem.Quantity > orderItem.Quantity {
|
||||||
|
return errors.New(fmt.Sprintf("refund quantity %d exceeds available quantity %d for item %d",
|
||||||
|
refundItem.Quantity, orderItem.Quantity, refundItem.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
refundedAmount += orderItem.Price * float64(refundItem.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, refundItem := range items {
|
||||||
|
orderItem := orderItemMap[refundItem.OrderItemID]
|
||||||
|
newQuantity := orderItem.Quantity - refundItem.Quantity
|
||||||
|
|
||||||
|
if newQuantity == 0 {
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, 0)
|
||||||
|
} else {
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, refundItem.OrderItemID, newQuantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingAmount := order.Amount - refundedAmount
|
||||||
|
remainingTax := (remainingAmount / order.Amount) * order.Tax
|
||||||
|
remainingTotal := remainingAmount + remainingTax
|
||||||
|
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newStatus := "PARTIAL"
|
||||||
|
if remainingAmount <= 0 {
|
||||||
|
newStatus = "REFUNDED"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
refundTransaction, err := s.createRefundTransaction(ctx, order, reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create refund transaction", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
refundTransaction.Amount = -refundedAmount
|
||||||
|
_, err = s.transaction.Create(ctx, refundTransaction)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update refund transaction", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ContextLogger(ctx).Info("partial refund processed successfully",
|
||||||
|
zap.Int64("orderID", orderID),
|
||||||
|
zap.String("reason", reason),
|
||||||
|
zap.Float64("refundedAmount", refundedAmount),
|
||||||
|
zap.String("refundTransactionID", refundTransaction.ID))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VoidOrderRequest handles voiding orders (for ongoing orders) or specific items
|
||||||
|
func (s *orderSvc) VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error {
|
||||||
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to find order for void", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow voiding for NEW, PENDING orders
|
||||||
|
if order.Status != "NEW" && order.Status != "PENDING" {
|
||||||
|
return errors.New("only new or pending orders can be voided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if voidType == "ALL" {
|
||||||
|
// Void entire order
|
||||||
|
err = s.repo.UpdateOrder(ctx, orderID, "VOIDED", reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to void order", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if voidType == "ITEM" {
|
||||||
|
// Void specific items
|
||||||
|
voidedAmount := 0.0
|
||||||
|
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||||
|
|
||||||
|
for _, item := range order.OrderItems {
|
||||||
|
orderItemMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, voidItem := range items {
|
||||||
|
orderItem, exists := orderItemMap[voidItem.OrderItemID]
|
||||||
|
if !exists {
|
||||||
|
return errors.New(fmt.Sprintf("order item %d not found", voidItem.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if voidItem.Quantity > orderItem.Quantity {
|
||||||
|
return errors.New(fmt.Sprintf("void quantity %d exceeds available quantity %d for item %d",
|
||||||
|
voidItem.Quantity, orderItem.Quantity, voidItem.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
voidedAmount += orderItem.Price * float64(voidItem.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update order items with reduced quantities
|
||||||
|
for _, voidItem := range items {
|
||||||
|
orderItem := orderItemMap[voidItem.OrderItemID]
|
||||||
|
newQuantity := orderItem.Quantity - voidItem.Quantity
|
||||||
|
|
||||||
|
if newQuantity == 0 {
|
||||||
|
// Remove item completely
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, 0)
|
||||||
|
} else {
|
||||||
|
// Update quantity
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, voidItem.OrderItemID, newQuantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order item", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate order totals
|
||||||
|
remainingAmount := order.Amount - voidedAmount
|
||||||
|
remainingTax := (remainingAmount / order.Amount) * order.Tax
|
||||||
|
remainingTotal := remainingAmount + remainingTax
|
||||||
|
|
||||||
|
// Update order totals
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, orderID, remainingAmount, remainingTax, remainingTotal)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order totals", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update order status to PARTIAL if some items remain, otherwise to VOIDED
|
||||||
|
newStatus := "PARTIAL"
|
||||||
|
if remainingAmount <= 0 {
|
||||||
|
newStatus = "VOIDED"
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.repo.UpdateOrder(ctx, orderID, newStatus, reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ContextLogger(ctx).Info("order voided successfully",
|
||||||
|
zap.Int64("orderID", orderID),
|
||||||
|
zap.String("reason", reason),
|
||||||
|
zap.String("voidType", voidType))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitBillRequest handles splitting bills by items or amounts
|
||||||
|
func (s *orderSvc) SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error) {
|
||||||
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to find order for split bill", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Status != "NEW" && order.Status != "PENDING" {
|
||||||
|
return nil, errors.New("only new or pending orders can be split")
|
||||||
|
}
|
||||||
|
|
||||||
|
var splitOrder *entity.Order
|
||||||
|
|
||||||
|
if splitType == "ITEM" {
|
||||||
|
splitOrder, err = s.splitByItems(ctx, order, paymentMethod, paymentProvider, items)
|
||||||
|
} else if splitType == "AMOUNT" {
|
||||||
|
splitOrder, err = s.splitByAmount(ctx, order, paymentMethod, paymentProvider, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to split bill", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ContextLogger(ctx).Info("bill split successfully",
|
||||||
|
zap.Int64("orderID", orderID),
|
||||||
|
zap.String("splitType", splitType),
|
||||||
|
zap.Int64("splitOrderID", splitOrder.ID))
|
||||||
|
|
||||||
|
return splitOrder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *orderSvc) splitByItems(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, items []entity.SplitBillItem) (*entity.Order, error) {
|
||||||
|
var splitOrderItems []entity.OrderItem
|
||||||
|
orderItemMap := make(map[int64]*entity.OrderItem)
|
||||||
|
|
||||||
|
for _, item := range originalOrder.OrderItems {
|
||||||
|
orderItemMap[item.ID] = &item
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedItems := make(map[int64]bool)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
orderItem, exists := orderItemMap[item.OrderItemID]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New(fmt.Sprintf("order item %d not found", item.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Quantity > orderItem.Quantity {
|
||||||
|
return nil, errors.New(fmt.Sprintf("split quantity %d exceeds available quantity %d for item %d",
|
||||||
|
item.Quantity, orderItem.Quantity, item.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignedItems[item.OrderItemID] {
|
||||||
|
return nil, errors.New(fmt.Sprintf("order item %d is already assigned to another split", item.OrderItemID))
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedItems[item.OrderItemID] = true
|
||||||
|
|
||||||
|
splitOrderItems = append(splitOrderItems, entity.OrderItem{
|
||||||
|
ItemID: orderItem.ItemID,
|
||||||
|
ItemType: orderItem.ItemType,
|
||||||
|
Price: orderItem.Price,
|
||||||
|
ItemName: orderItem.ItemName,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
|
Product: orderItem.Product,
|
||||||
|
Notes: orderItem.Notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
splitAmount := 0.0
|
||||||
|
for _, item := range splitOrderItems {
|
||||||
|
splitAmount += item.Price * float64(item.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
|
splitTotal := splitAmount + splitTax
|
||||||
|
|
||||||
|
// Create new PAID order for the split
|
||||||
|
splitOrder := &entity.Order{
|
||||||
|
PartnerID: originalOrder.PartnerID,
|
||||||
|
CustomerID: originalOrder.CustomerID,
|
||||||
|
CustomerName: originalOrder.CustomerName,
|
||||||
|
Status: "PAID",
|
||||||
|
Amount: splitAmount,
|
||||||
|
Tax: splitTax,
|
||||||
|
Total: splitTotal,
|
||||||
|
PaymentType: paymentMethod,
|
||||||
|
PaymentProvider: paymentProvider,
|
||||||
|
Source: originalOrder.Source,
|
||||||
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
|
OrderItems: splitOrderItems,
|
||||||
|
OrderType: originalOrder.OrderType,
|
||||||
|
TableNumber: originalOrder.TableNumber,
|
||||||
|
CashierSessionID: originalOrder.CashierSessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrder, err := s.repo.Create(ctx, splitOrder)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust original order items (reduce quantities)
|
||||||
|
for _, item := range items {
|
||||||
|
orderItem := orderItemMap[item.OrderItemID]
|
||||||
|
newQuantity := orderItem.Quantity - item.Quantity
|
||||||
|
|
||||||
|
if newQuantity == 0 {
|
||||||
|
// Remove item completely
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, 0)
|
||||||
|
} else {
|
||||||
|
// Update quantity
|
||||||
|
err = s.repo.UpdateOrderItem(ctx, item.OrderItemID, newQuantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update original order item", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate original order totals
|
||||||
|
remainingAmount := originalOrder.Amount - splitAmount
|
||||||
|
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
|
remainingTotal := remainingAmount + remainingTax
|
||||||
|
|
||||||
|
// Update original order totals
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdOrder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitByAmount splits the order by assigning specific amounts to each split
|
||||||
|
func (s *orderSvc) splitByAmount(ctx mycontext.Context, originalOrder *entity.Order, paymentMethod string, paymentProvider string, amount float64) (*entity.Order, error) {
|
||||||
|
// Validate that split amount is less than original order total
|
||||||
|
if amount >= originalOrder.Total {
|
||||||
|
return nil, errors.New(fmt.Sprintf("split amount %.2f must be less than order total %.2f",
|
||||||
|
amount, originalOrder.Total))
|
||||||
|
}
|
||||||
|
|
||||||
|
// For amount-based split, we create a new order with all items
|
||||||
|
var splitOrderItems []entity.OrderItem
|
||||||
|
|
||||||
|
for _, item := range originalOrder.OrderItems {
|
||||||
|
splitOrderItems = append(splitOrderItems, entity.OrderItem{
|
||||||
|
ItemID: item.ItemID,
|
||||||
|
ItemType: item.ItemType,
|
||||||
|
Price: item.Price,
|
||||||
|
ItemName: item.ItemName,
|
||||||
|
Quantity: item.Quantity,
|
||||||
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
|
Product: item.Product,
|
||||||
|
Notes: item.Notes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
splitAmount := amount
|
||||||
|
splitTax := (splitAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
|
splitTotal := splitAmount + splitTax
|
||||||
|
|
||||||
|
// Create new PAID order for the split
|
||||||
|
splitOrder := &entity.Order{
|
||||||
|
PartnerID: originalOrder.PartnerID,
|
||||||
|
CustomerID: originalOrder.CustomerID,
|
||||||
|
CustomerName: originalOrder.CustomerName,
|
||||||
|
Status: "PAID",
|
||||||
|
Amount: splitAmount,
|
||||||
|
Tax: splitTax,
|
||||||
|
Total: splitTotal,
|
||||||
|
PaymentType: paymentMethod,
|
||||||
|
PaymentProvider: paymentProvider,
|
||||||
|
Source: originalOrder.Source,
|
||||||
|
CreatedBy: originalOrder.CreatedBy,
|
||||||
|
OrderItems: splitOrderItems,
|
||||||
|
OrderType: originalOrder.OrderType,
|
||||||
|
TableNumber: originalOrder.TableNumber,
|
||||||
|
CashierSessionID: originalOrder.CashierSessionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
createdOrder, err := s.repo.Create(ctx, splitOrder)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create split order", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust original order amount
|
||||||
|
remainingAmount := originalOrder.Amount - splitAmount
|
||||||
|
remainingTax := (remainingAmount / originalOrder.Amount) * originalOrder.Tax
|
||||||
|
remainingTotal := remainingAmount + remainingTax
|
||||||
|
|
||||||
|
// Update original order totals
|
||||||
|
err = s.repo.UpdateOrderTotals(ctx, originalOrder.ID, remainingAmount, remainingTax, remainingTotal)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update original order totals", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return createdOrder, nil
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@ func (s *orderSvc) CreateOrderInquiry(ctx mycontext.Context,
|
|||||||
|
|
||||||
customerID := int64(0)
|
customerID := int64(0)
|
||||||
|
|
||||||
if req.CustomerID != nil {
|
if req.CustomerID != nil && *req.CustomerID != 0 {
|
||||||
customer, err := s.customer.GetCustomer(ctx, *req.CustomerID)
|
customer, err := s.customer.GetCustomer(ctx, *req.CustomerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("customer is not found", zap.Error(err))
|
logger.ContextLogger(ctx).Error("customer is not found", zap.Error(err))
|
||||||
|
|||||||
@ -38,9 +38,9 @@ func (s *orderSvc) ExecuteOrderInquiry(ctx mycontext.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error {
|
func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error {
|
||||||
order, err := s.repo.FindByIDAndPartnerID(ctx, partnerID, orderID)
|
order, err := s.repo.FindByIDAndPartnerID(ctx, orderID, partnerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.ContextLogger(ctx).Error("failed to create order", zap.Error(err))
|
logger.ContextLogger(ctx).Error("failed to find order for refund", zap.Error(err))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +48,31 @@ func (s *orderSvc) RefundRequest(ctx mycontext.Context, partnerID, orderID int64
|
|||||||
return errors.New("only paid order can be refund")
|
return errors.New("only paid order can be refund")
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.repo.UpdateOrder(ctx, order.ID, "REFUNDED", reason)
|
err = s.repo.UpdateOrder(ctx, order.ID, "REFUNDED", reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to update order status", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
refundTransaction, err := s.createRefundTransaction(ctx, order, reason)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to create refund transaction", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.CustomerID != nil && *order.CustomerID > 0 {
|
||||||
|
err = s.reverseCustomerVouchers(ctx, *order.CustomerID, int64(order.Total), order.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Warn("failed to reverse customer vouchers", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.ContextLogger(ctx).Info("refund processed successfully",
|
||||||
|
zap.Int64("orderID", orderID),
|
||||||
|
zap.String("reason", reason),
|
||||||
|
zap.String("refundTransactionID", refundTransaction.ID))
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *orderSvc) processPostOrderActions(
|
func (s *orderSvc) processPostOrderActions(
|
||||||
@ -262,3 +286,36 @@ func formatPaymentMethod(method string) string {
|
|||||||
}
|
}
|
||||||
return method
|
return method
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *orderSvc) createRefundTransaction(ctx mycontext.Context, order *entity.Order, reason string) (*entity.Transaction, error) {
|
||||||
|
transaction := &entity.Transaction{
|
||||||
|
OrderID: order.ID,
|
||||||
|
Amount: -order.Total,
|
||||||
|
PaymentMethod: order.PaymentType,
|
||||||
|
Status: "REFUND",
|
||||||
|
CreatedAt: constants.TimeNow(),
|
||||||
|
PartnerID: order.PartnerID,
|
||||||
|
TransactionType: "REFUND",
|
||||||
|
CreatedBy: ctx.RequestedBy(),
|
||||||
|
UpdatedBy: ctx.RequestedBy(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.transaction.Create(ctx, transaction)
|
||||||
|
return transaction, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *orderSvc) reverseCustomerVouchers(ctx mycontext.Context, customerID int64, total int64, orderID int64) error {
|
||||||
|
// Find vouchers associated with this order and reverse them
|
||||||
|
// This is a simplified implementation - in production you might want to track voucher-order relationships
|
||||||
|
logger.ContextLogger(ctx).Info("reversing customer vouchers",
|
||||||
|
zap.Int64("customerID", customerID),
|
||||||
|
zap.Int64("orderID", orderID))
|
||||||
|
|
||||||
|
// TODO: Implement voucher reversal logic
|
||||||
|
// This would involve:
|
||||||
|
// 1. Finding vouchers created for this order
|
||||||
|
// 2. Marking them as reversed/cancelled
|
||||||
|
// 3. Optionally adjusting customer points
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ type Repository interface {
|
|||||||
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
|
FindInquiryByID(ctx mycontext.Context, id string) (*entity.OrderInquiry, error)
|
||||||
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
|
UpdateInquiryStatus(ctx mycontext.Context, id string, status string) error
|
||||||
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
UpdateOrder(ctx mycontext.Context, id int64, status string, description string) error
|
||||||
|
UpdateOrderItem(ctx mycontext.Context, orderItemID int64, quantity int) error
|
||||||
|
UpdateOrderTotals(ctx mycontext.Context, orderID int64, amount, tax, total float64) error
|
||||||
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistoryByPartnerID(ctx mycontext.Context, partnerID int64, req entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
GetOrderPaymentMethodBreakdown(
|
GetOrderPaymentMethodBreakdown(
|
||||||
ctx mycontext.Context,
|
ctx mycontext.Context,
|
||||||
@ -67,6 +69,9 @@ type Service interface {
|
|||||||
ExecuteOrderInquiry(ctx mycontext.Context,
|
ExecuteOrderInquiry(ctx mycontext.Context,
|
||||||
token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error)
|
token string, paymentMethod, paymentProvider string, inProgressOrderID int64) (*entity.OrderResponse, error)
|
||||||
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
|
RefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string) error
|
||||||
|
PartialRefundRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, items []entity.PartialRefundItem) error
|
||||||
|
VoidOrderRequest(ctx mycontext.Context, partnerID, orderID int64, reason string, voidType string, items []entity.VoidItem) error
|
||||||
|
SplitBillRequest(ctx mycontext.Context, partnerID, orderID int64, splitType string, paymentMethod string, paymentProvider string, items []entity.SplitBillItem, amount float64) (*entity.Order, error)
|
||||||
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetOrderHistory(ctx mycontext.Context, partnerID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
CalculateOrderTotals(
|
CalculateOrderTotals(
|
||||||
ctx mycontext.Context,
|
ctx mycontext.Context,
|
||||||
@ -104,6 +109,7 @@ type Service interface {
|
|||||||
) ([]entity.PopularProductItem, error)
|
) ([]entity.PopularProductItem, error)
|
||||||
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
GetCustomerOrderHistory(ctx mycontext.Context, userID int64, request entity.SearchRequest) ([]*entity.Order, int64, error)
|
||||||
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
|
GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerID int64, orderID int64) (*entity.Order, error)
|
||||||
|
GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config interface {
|
type Config interface {
|
||||||
|
|||||||
@ -27,3 +27,15 @@ func (s *orderSvc) GetOrderByOrderAndCustomerID(ctx mycontext.Context, customerI
|
|||||||
|
|
||||||
return orders, nil
|
return orders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *orderSvc) GetOrderByID(ctx mycontext.Context, orderID int64) (*entity.Order, error) {
|
||||||
|
order, err := s.repo.FindByID(ctx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ContextLogger(ctx).Error("failed to get order by ID",
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Int64("orderID", orderID))
|
||||||
|
return nil, errors.Wrap(err, "failed to get order")
|
||||||
|
}
|
||||||
|
|
||||||
|
return order, nil
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user