init
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
|
||||
type AnalyticsProcessor interface {
|
||||
GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error)
|
||||
GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error)
|
||||
GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error)
|
||||
GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error)
|
||||
GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error)
|
||||
}
|
||||
|
||||
type AnalyticsProcessorImpl struct {
|
||||
analyticsRepo repository.AnalyticsRepository
|
||||
}
|
||||
|
||||
func NewAnalyticsProcessorImpl(analyticsRepo repository.AnalyticsRepository) *AnalyticsProcessorImpl {
|
||||
return &AnalyticsProcessorImpl{
|
||||
analyticsRepo: analyticsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetPaymentMethodAnalytics(ctx context.Context, req *models.PaymentMethodAnalyticsRequest) (*models.PaymentMethodAnalyticsResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
analyticsData, err := p.analyticsRepo.GetPaymentMethodAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get payment method analytics: %w", err)
|
||||
}
|
||||
|
||||
// Calculate summary
|
||||
var totalAmount float64
|
||||
var totalOrders int64
|
||||
var totalPayments int64
|
||||
|
||||
for _, data := range analyticsData {
|
||||
totalAmount += data.TotalAmount
|
||||
totalOrders += data.OrderCount
|
||||
totalPayments += data.PaymentCount
|
||||
}
|
||||
|
||||
var averageOrderValue float64
|
||||
if totalOrders > 0 {
|
||||
averageOrderValue = totalAmount / float64(totalOrders)
|
||||
}
|
||||
|
||||
// Calculate percentages
|
||||
var resultData []models.PaymentMethodAnalyticsData
|
||||
for _, data := range analyticsData {
|
||||
var percentage float64
|
||||
if totalAmount > 0 {
|
||||
percentage = (data.TotalAmount / totalAmount) * 100
|
||||
}
|
||||
|
||||
resultData = append(resultData, models.PaymentMethodAnalyticsData{
|
||||
PaymentMethodID: data.PaymentMethodID,
|
||||
PaymentMethodName: data.PaymentMethodName,
|
||||
PaymentMethodType: data.PaymentMethodType,
|
||||
TotalAmount: data.TotalAmount,
|
||||
OrderCount: data.OrderCount,
|
||||
PaymentCount: data.PaymentCount,
|
||||
Percentage: percentage,
|
||||
})
|
||||
}
|
||||
|
||||
summary := models.PaymentMethodSummary{
|
||||
TotalAmount: totalAmount,
|
||||
TotalOrders: totalOrders,
|
||||
TotalPayments: totalPayments,
|
||||
AverageOrderValue: averageOrderValue,
|
||||
}
|
||||
|
||||
return &models.PaymentMethodAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: summary,
|
||||
Data: resultData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetSalesAnalytics(ctx context.Context, req *models.SalesAnalyticsRequest) (*models.SalesAnalyticsResponse, error) {
|
||||
// Validate date range
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
// Validate groupBy
|
||||
if req.GroupBy == "" {
|
||||
req.GroupBy = "day"
|
||||
}
|
||||
|
||||
// Get analytics data from repository
|
||||
analyticsData, err := p.analyticsRepo.GetSalesAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sales analytics: %w", err)
|
||||
}
|
||||
|
||||
// Calculate summary
|
||||
var totalSales float64
|
||||
var totalOrders int64
|
||||
var totalItems int64
|
||||
var totalTax float64
|
||||
var totalDiscount float64
|
||||
var netSales float64
|
||||
|
||||
for _, data := range analyticsData {
|
||||
totalSales += data.Sales
|
||||
totalOrders += data.Orders
|
||||
totalItems += data.Items
|
||||
totalTax += data.Tax
|
||||
totalDiscount += data.Discount
|
||||
netSales += data.NetSales
|
||||
}
|
||||
|
||||
var averageOrderValue float64
|
||||
if totalOrders > 0 {
|
||||
averageOrderValue = totalSales / float64(totalOrders)
|
||||
}
|
||||
|
||||
// Transform data
|
||||
var resultData []models.SalesAnalyticsData
|
||||
for _, data := range analyticsData {
|
||||
resultData = append(resultData, models.SalesAnalyticsData{
|
||||
Date: data.Date,
|
||||
Sales: data.Sales,
|
||||
Orders: data.Orders,
|
||||
Items: data.Items,
|
||||
Tax: data.Tax,
|
||||
Discount: data.Discount,
|
||||
NetSales: data.NetSales,
|
||||
})
|
||||
}
|
||||
|
||||
summary := models.SalesSummary{
|
||||
TotalSales: totalSales,
|
||||
TotalOrders: totalOrders,
|
||||
TotalItems: totalItems,
|
||||
AverageOrderValue: averageOrderValue,
|
||||
TotalTax: totalTax,
|
||||
TotalDiscount: totalDiscount,
|
||||
NetSales: netSales,
|
||||
}
|
||||
|
||||
return &models.SalesAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: summary,
|
||||
Data: resultData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetProductAnalytics(ctx context.Context, req *models.ProductAnalyticsRequest) (*models.ProductAnalyticsResponse, error) {
|
||||
// Validate date range
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
// Set default limit
|
||||
if req.Limit <= 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
// Get analytics data from repository
|
||||
analyticsData, err := p.analyticsRepo.GetProductAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.Limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product analytics: %w", err)
|
||||
}
|
||||
|
||||
// Transform data
|
||||
var resultData []models.ProductAnalyticsData
|
||||
for _, data := range analyticsData {
|
||||
resultData = append(resultData, models.ProductAnalyticsData{
|
||||
ProductID: data.ProductID,
|
||||
ProductName: data.ProductName,
|
||||
CategoryID: data.CategoryID,
|
||||
CategoryName: data.CategoryName,
|
||||
QuantitySold: data.QuantitySold,
|
||||
Revenue: data.Revenue,
|
||||
AveragePrice: data.AveragePrice,
|
||||
OrderCount: data.OrderCount,
|
||||
})
|
||||
}
|
||||
|
||||
return &models.ProductAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Data: resultData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetDashboardAnalytics(ctx context.Context, req *models.DashboardAnalyticsRequest) (*models.DashboardAnalyticsResponse, error) {
|
||||
// Validate date range
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
// Get dashboard overview
|
||||
overview, err := p.analyticsRepo.GetDashboardOverview(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get dashboard overview: %w", err)
|
||||
}
|
||||
|
||||
// Get top products (limit to 5 for dashboard)
|
||||
productReq := &models.ProductAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Limit: 5,
|
||||
}
|
||||
topProducts, err := p.GetProductAnalytics(ctx, productReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get top products: %w", err)
|
||||
}
|
||||
|
||||
// Get payment methods
|
||||
paymentReq := &models.PaymentMethodAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: "day",
|
||||
}
|
||||
paymentMethods, err := p.GetPaymentMethodAnalytics(ctx, paymentReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get payment methods: %w", err)
|
||||
}
|
||||
|
||||
// Get recent sales (last 7 days)
|
||||
recentDateFrom := time.Now().AddDate(0, 0, -7)
|
||||
salesReq := &models.SalesAnalyticsRequest{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: recentDateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: "day",
|
||||
}
|
||||
recentSales, err := p.GetSalesAnalytics(ctx, salesReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get recent sales: %w", err)
|
||||
}
|
||||
|
||||
return &models.DashboardAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
Overview: models.DashboardOverview{
|
||||
TotalSales: overview.TotalSales,
|
||||
TotalOrders: overview.TotalOrders,
|
||||
AverageOrderValue: overview.AverageOrderValue,
|
||||
TotalCustomers: overview.TotalCustomers,
|
||||
VoidedOrders: overview.VoidedOrders,
|
||||
RefundedOrders: overview.RefundedOrders,
|
||||
},
|
||||
TopProducts: topProducts.Data,
|
||||
PaymentMethods: paymentMethods.Data,
|
||||
RecentSales: recentSales.Data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *AnalyticsProcessorImpl) GetProfitLossAnalytics(ctx context.Context, req *models.ProfitLossAnalyticsRequest) (*models.ProfitLossAnalyticsResponse, error) {
|
||||
if req.DateFrom.After(req.DateTo) {
|
||||
return nil, fmt.Errorf("date_from cannot be after date_to")
|
||||
}
|
||||
|
||||
// Get analytics data from repository
|
||||
result, err := p.analyticsRepo.GetProfitLossAnalytics(ctx, req.OrganizationID, req.OutletID, req.DateFrom, req.DateTo, req.GroupBy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get profit/loss analytics: %w", err)
|
||||
}
|
||||
|
||||
// Transform entities to models
|
||||
data := make([]models.ProfitLossData, len(result.Data))
|
||||
for i, item := range result.Data {
|
||||
data[i] = models.ProfitLossData{
|
||||
Date: item.Date,
|
||||
Revenue: item.Revenue,
|
||||
Cost: item.Cost,
|
||||
GrossProfit: item.GrossProfit,
|
||||
GrossProfitMargin: item.GrossProfitMargin,
|
||||
Tax: item.Tax,
|
||||
Discount: item.Discount,
|
||||
NetProfit: item.NetProfit,
|
||||
NetProfitMargin: item.NetProfitMargin,
|
||||
Orders: item.Orders,
|
||||
}
|
||||
}
|
||||
|
||||
productData := make([]models.ProductProfitData, len(result.ProductData))
|
||||
for i, item := range result.ProductData {
|
||||
productData[i] = models.ProductProfitData{
|
||||
ProductID: item.ProductID,
|
||||
ProductName: item.ProductName,
|
||||
CategoryID: item.CategoryID,
|
||||
CategoryName: item.CategoryName,
|
||||
QuantitySold: item.QuantitySold,
|
||||
Revenue: item.Revenue,
|
||||
Cost: item.Cost,
|
||||
GrossProfit: item.GrossProfit,
|
||||
GrossProfitMargin: item.GrossProfitMargin,
|
||||
AveragePrice: item.AveragePrice,
|
||||
AverageCost: item.AverageCost,
|
||||
ProfitPerUnit: item.ProfitPerUnit,
|
||||
}
|
||||
}
|
||||
|
||||
return &models.ProfitLossAnalyticsResponse{
|
||||
OrganizationID: req.OrganizationID,
|
||||
OutletID: req.OutletID,
|
||||
DateFrom: req.DateFrom,
|
||||
DateTo: req.DateTo,
|
||||
GroupBy: req.GroupBy,
|
||||
Summary: models.ProfitLossSummary{
|
||||
TotalRevenue: result.Summary.TotalRevenue,
|
||||
TotalCost: result.Summary.TotalCost,
|
||||
GrossProfit: result.Summary.GrossProfit,
|
||||
GrossProfitMargin: result.Summary.GrossProfitMargin,
|
||||
TotalTax: result.Summary.TotalTax,
|
||||
TotalDiscount: result.Summary.TotalDiscount,
|
||||
NetProfit: result.Summary.NetProfit,
|
||||
NetProfitMargin: result.Summary.NetProfitMargin,
|
||||
TotalOrders: result.Summary.TotalOrders,
|
||||
AverageProfit: result.Summary.AverageProfit,
|
||||
ProfitabilityRatio: result.Summary.ProfitabilityRatio,
|
||||
},
|
||||
Data: data,
|
||||
ProductData: productData,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CategoryProcessor interface {
|
||||
CreateCategory(ctx context.Context, req *models.CreateCategoryRequest) (*models.CategoryResponse, error)
|
||||
UpdateCategory(ctx context.Context, id uuid.UUID, req *models.UpdateCategoryRequest) (*models.CategoryResponse, error)
|
||||
DeleteCategory(ctx context.Context, id uuid.UUID) error
|
||||
GetCategoryByID(ctx context.Context, id uuid.UUID) (*models.CategoryResponse, error)
|
||||
ListCategories(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.CategoryResponse, int, error)
|
||||
}
|
||||
|
||||
type CategoryRepository interface {
|
||||
Create(ctx context.Context, category *entities.Category) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Category, error)
|
||||
GetWithProducts(ctx context.Context, id uuid.UUID) (*entities.Category, error)
|
||||
GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Category, error)
|
||||
GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Category, error)
|
||||
Update(ctx context.Context, category *entities.Category) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Category, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Category, error)
|
||||
ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error)
|
||||
}
|
||||
|
||||
type CategoryProcessorImpl struct {
|
||||
categoryRepo CategoryRepository
|
||||
}
|
||||
|
||||
func NewCategoryProcessorImpl(categoryRepo CategoryRepository) *CategoryProcessorImpl {
|
||||
return &CategoryProcessorImpl{
|
||||
categoryRepo: categoryRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *CategoryProcessorImpl) CreateCategory(ctx context.Context, req *models.CreateCategoryRequest) (*models.CategoryResponse, error) {
|
||||
// Check if category with same name exists for this organization
|
||||
exists, err := p.categoryRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check category name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("category with name '%s' already exists for this organization", req.Name)
|
||||
}
|
||||
|
||||
// Map request to entity
|
||||
categoryEntity := mappers.CreateCategoryRequestToEntity(req)
|
||||
|
||||
// Create category
|
||||
if err := p.categoryRepo.Create(ctx, categoryEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create category: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.CategoryEntityToResponse(categoryEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *CategoryProcessorImpl) UpdateCategory(ctx context.Context, id uuid.UUID, req *models.UpdateCategoryRequest) (*models.CategoryResponse, error) {
|
||||
// Get existing category
|
||||
existingCategory, err := p.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("category not found: %w", err)
|
||||
}
|
||||
|
||||
// Check name uniqueness if name is being updated
|
||||
if req.Name != nil && *req.Name != existingCategory.Name {
|
||||
exists, err := p.categoryRepo.ExistsByName(ctx, existingCategory.OrganizationID, *req.Name, &id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check category name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("category with name '%s' already exists for this organization", *req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates to entity
|
||||
mappers.UpdateCategoryEntityFromRequest(existingCategory, req)
|
||||
|
||||
// Update category
|
||||
if err := p.categoryRepo.Update(ctx, existingCategory); err != nil {
|
||||
return nil, fmt.Errorf("failed to update category: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.CategoryEntityToResponse(existingCategory)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *CategoryProcessorImpl) DeleteCategory(ctx context.Context, id uuid.UUID) error {
|
||||
// Check if category exists
|
||||
_, err := p.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("category not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if category has products
|
||||
categoryWithProducts, err := p.categoryRepo.GetWithProducts(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check category products: %w", err)
|
||||
}
|
||||
|
||||
if len(categoryWithProducts.Products) > 0 {
|
||||
return fmt.Errorf("cannot delete category: it has %d products associated with it", len(categoryWithProducts.Products))
|
||||
}
|
||||
|
||||
// Delete category
|
||||
if err := p.categoryRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete category: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CategoryProcessorImpl) GetCategoryByID(ctx context.Context, id uuid.UUID) (*models.CategoryResponse, error) {
|
||||
categoryEntity, err := p.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("category not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.CategoryEntityToResponse(categoryEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *CategoryProcessorImpl) ListCategories(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.CategoryResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
categoryEntities, total, err := p.categoryRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list categories: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.CategoryResponse, len(categoryEntities))
|
||||
for i, entity := range categoryEntities {
|
||||
response := mappers.CategoryEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, int(total), nil
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CustomerProcessor struct {
|
||||
customerRepo *repository.CustomerRepository
|
||||
}
|
||||
|
||||
func NewCustomerProcessor(customerRepo *repository.CustomerRepository) *CustomerProcessor {
|
||||
return &CustomerProcessor{
|
||||
customerRepo: customerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCustomer creates a new customer
|
||||
func (p *CustomerProcessor) CreateCustomer(ctx context.Context, req *models.CreateCustomerRequest, organizationID uuid.UUID) (*models.CustomerResponse, error) {
|
||||
if req.Email != nil {
|
||||
existingCustomer, err := p.customerRepo.GetByEmail(ctx, *req.Email, organizationID)
|
||||
if err == nil && existingCustomer != nil {
|
||||
return nil, errors.New("email already exists for this organization")
|
||||
}
|
||||
}
|
||||
|
||||
// Convert request to entity
|
||||
customer := mappers.ToCustomerEntity(req, organizationID)
|
||||
|
||||
// Create customer
|
||||
err := p.customerRepo.Create(ctx, customer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create customer: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(customer), nil
|
||||
}
|
||||
|
||||
// GetCustomer retrieves a customer by ID
|
||||
func (p *CustomerProcessor) GetCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*models.CustomerResponse, error) {
|
||||
customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customer not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(customer), nil
|
||||
}
|
||||
|
||||
// ListCustomers retrieves customers with pagination and filtering
|
||||
func (p *CustomerProcessor) ListCustomers(ctx context.Context, query *models.ListCustomersQuery, organizationID uuid.UUID) (*models.PaginatedResponse[models.CustomerResponse], error) {
|
||||
// Set default values
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.Limit <= 0 {
|
||||
query.Limit = 10
|
||||
}
|
||||
if query.Limit > 100 {
|
||||
query.Limit = 100
|
||||
}
|
||||
|
||||
offset := (query.Page - 1) * query.Limit
|
||||
|
||||
// Get customers from repository
|
||||
customers, total, err := p.customerRepo.List(
|
||||
ctx,
|
||||
organizationID,
|
||||
offset,
|
||||
query.Limit,
|
||||
query.Search,
|
||||
query.IsActive,
|
||||
query.IsDefault,
|
||||
query.SortBy,
|
||||
query.SortOrder,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list customers: %w", err)
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
responses := mappers.ToCustomerResponses(customers)
|
||||
|
||||
// Calculate pagination info
|
||||
totalPages := int((total + int64(query.Limit) - 1) / int64(query.Limit))
|
||||
|
||||
return &models.PaginatedResponse[models.CustomerResponse]{
|
||||
Data: responses,
|
||||
Pagination: models.Pagination{
|
||||
Page: query.Page,
|
||||
Limit: query.Limit,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateCustomer updates an existing customer
|
||||
func (p *CustomerProcessor) UpdateCustomer(ctx context.Context, customerID, organizationID uuid.UUID, req *models.UpdateCustomerRequest) (*models.CustomerResponse, error) {
|
||||
// Get existing customer
|
||||
customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customer not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if email is already taken by another customer within the organization
|
||||
if req.Email != nil && *req.Email != *customer.Email {
|
||||
existingCustomer, err := p.customerRepo.GetByEmail(ctx, *req.Email, organizationID)
|
||||
if err == nil && existingCustomer != nil && existingCustomer.ID != customerID {
|
||||
return nil, errors.New("email already exists for this organization")
|
||||
}
|
||||
}
|
||||
|
||||
// Update customer fields
|
||||
mappers.UpdateCustomerEntity(customer, req)
|
||||
|
||||
// Save updated customer
|
||||
err = p.customerRepo.Update(ctx, customer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update customer: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(customer), nil
|
||||
}
|
||||
|
||||
// DeleteCustomer deletes a customer
|
||||
func (p *CustomerProcessor) DeleteCustomer(ctx context.Context, customerID, organizationID uuid.UUID) error {
|
||||
// Get existing customer
|
||||
customer, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("customer not found: %w", err)
|
||||
}
|
||||
|
||||
// Prevent deletion of default customer
|
||||
if customer.IsDefault {
|
||||
return errors.New("cannot delete default customer")
|
||||
}
|
||||
|
||||
// Delete customer
|
||||
err = p.customerRepo.Delete(ctx, customerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete customer: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *CustomerProcessor) SetDefaultCustomer(ctx context.Context, customerID, organizationID uuid.UUID) (*models.CustomerResponse, error) {
|
||||
_, err := p.customerRepo.GetByIDAndOrganization(ctx, customerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customer not found: %w", err)
|
||||
}
|
||||
|
||||
// Set as default
|
||||
err = p.customerRepo.SetAsDefault(ctx, customerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to set default customer: %w", err)
|
||||
}
|
||||
|
||||
// Get updated customer
|
||||
updatedCustomer, err := p.customerRepo.GetByID(ctx, customerID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get updated customer: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(updatedCustomer), nil
|
||||
}
|
||||
|
||||
func (p *CustomerProcessor) GetDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*models.CustomerResponse, error) {
|
||||
customer, err := p.customerRepo.GetDefaultCustomer(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("default customer not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(customer), nil
|
||||
}
|
||||
|
||||
func (p *CustomerProcessor) EnsureDefaultCustomer(ctx context.Context, organizationID uuid.UUID) (*models.CustomerResponse, error) {
|
||||
customer, err := p.customerRepo.GetDefaultCustomer(ctx, organizationID)
|
||||
if err == nil {
|
||||
return mappers.ToCustomerResponse(customer), nil
|
||||
}
|
||||
|
||||
defaultCustomer, err := p.customerRepo.CreateDefaultCustomer(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create default customer: %w", err)
|
||||
}
|
||||
|
||||
return mappers.ToCustomerResponse(defaultCustomer), nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package processor
|
||||
|
||||
import "context"
|
||||
|
||||
type FileClient interface {
|
||||
UploadFile(ctx context.Context, fileName string, fileContent []byte) (fileUrl string, err error)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type FileProcessor interface {
|
||||
UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error)
|
||||
GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error)
|
||||
UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error)
|
||||
ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error)
|
||||
GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error)
|
||||
GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error)
|
||||
}
|
||||
|
||||
type FileRepository interface {
|
||||
Create(ctx context.Context, file *entities.File) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.File, error)
|
||||
GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.File, error)
|
||||
GetByUserID(ctx context.Context, userID uuid.UUID) ([]*entities.File, error)
|
||||
Update(ctx context.Context, file *entities.File) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.File, int64, error)
|
||||
GetByFileName(ctx context.Context, fileName string) (*entities.File, error)
|
||||
ExistsByFileName(ctx context.Context, fileName string) (bool, error)
|
||||
}
|
||||
|
||||
type FileProcessorImpl struct {
|
||||
fileRepo FileRepository
|
||||
fileClient FileClient
|
||||
}
|
||||
|
||||
func NewFileProcessorImpl(fileRepo FileRepository, fileClient FileClient) *FileProcessorImpl {
|
||||
return &FileProcessorImpl{
|
||||
fileRepo: fileRepo,
|
||||
fileClient: fileClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) UploadFile(ctx context.Context, file *multipart.FileHeader, req *models.UploadFileRequest, organizationID, userID uuid.UUID) (*models.FileResponse, error) {
|
||||
if file == nil {
|
||||
return nil, fmt.Errorf("file is required")
|
||||
}
|
||||
|
||||
if file.Size == 0 {
|
||||
return nil, fmt.Errorf("file cannot be empty")
|
||||
}
|
||||
|
||||
const maxFileSize = 10 * 1024 * 1024 // 10MB
|
||||
if file.Size > maxFileSize {
|
||||
return nil, fmt.Errorf("file size exceeds maximum limit of 10MB")
|
||||
}
|
||||
|
||||
if !constants.IsValidFileType(req.FileType) {
|
||||
return nil, fmt.Errorf("invalid file type: %s", req.FileType)
|
||||
}
|
||||
|
||||
originalName := file.Filename
|
||||
fileName := mappers.GenerateFileName(originalName, organizationID, userID)
|
||||
|
||||
for {
|
||||
exists, err := p.fileRepo.ExistsByFileName(ctx, fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check filename uniqueness: %w", err)
|
||||
}
|
||||
if !exists {
|
||||
break
|
||||
}
|
||||
|
||||
ext := filepath.Ext(fileName)
|
||||
base := strings.TrimSuffix(fileName, ext)
|
||||
fileName = fmt.Sprintf("%s_%d%s", base, time.Now().UnixNano(), ext)
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
fileContent := make([]byte, file.Size)
|
||||
_, err = src.Read(fileContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file content: %w", err)
|
||||
}
|
||||
|
||||
fileURL, err := p.fileClient.UploadFile(ctx, fileName, fileContent)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to upload file to storage: %w", err)
|
||||
}
|
||||
|
||||
mimeType := file.Header.Get("Content-Type")
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
fileType := req.FileType
|
||||
if fileType == "" {
|
||||
fileType = constants.GetFileTypeFromMimeType(mimeType)
|
||||
}
|
||||
|
||||
fileEntity := mappers.UploadFileRequestToEntity(
|
||||
&models.UploadFileRequest{
|
||||
FileType: fileType,
|
||||
IsPublic: req.IsPublic,
|
||||
Metadata: req.Metadata,
|
||||
},
|
||||
organizationID,
|
||||
userID,
|
||||
fileName,
|
||||
originalName,
|
||||
fileURL,
|
||||
mimeType,
|
||||
fileName,
|
||||
file.Size,
|
||||
)
|
||||
|
||||
if err := p.fileRepo.Create(ctx, fileEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to save file metadata: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.FileEntityToResponse(fileEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) GetFileByID(ctx context.Context, id uuid.UUID) (*models.FileResponse, error) {
|
||||
file, err := p.fileRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.FileEntityToResponse(file)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) UpdateFile(ctx context.Context, id uuid.UUID, req *models.UpdateFileRequest) (*models.FileResponse, error) {
|
||||
file, err := p.fileRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get file: %w", err)
|
||||
}
|
||||
|
||||
updates := mappers.UpdateFileRequestToEntityUpdates(req)
|
||||
for key, value := range updates {
|
||||
switch key {
|
||||
case "is_public":
|
||||
file.IsPublic = value.(bool)
|
||||
case "metadata":
|
||||
file.Metadata = entities.Metadata(value.(map[string]interface{}))
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.fileRepo.Update(ctx, file); err != nil {
|
||||
return nil, fmt.Errorf("failed to update file: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.FileEntityToResponse(file)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) ListFiles(ctx context.Context, req *models.ListFilesRequest) (*models.ListFilesResponse, error) {
|
||||
filters := make(map[string]interface{})
|
||||
|
||||
if req.OrganizationID != nil {
|
||||
filters["organization_id"] = *req.OrganizationID
|
||||
}
|
||||
if req.UserID != nil {
|
||||
filters["user_id"] = *req.UserID
|
||||
}
|
||||
if req.FileType != nil {
|
||||
filters["file_type"] = string(*req.FileType)
|
||||
}
|
||||
if req.IsPublic != nil {
|
||||
filters["is_public"] = *req.IsPublic
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
|
||||
files, total, err := p.fileRepo.List(ctx, filters, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list files: %w", err)
|
||||
}
|
||||
|
||||
fileResponses := mappers.FileEntitiesToResponses(files)
|
||||
totalPages := (int(total) + req.Limit - 1) / req.Limit
|
||||
|
||||
response := &models.ListFilesResponse{
|
||||
Files: fileResponses,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) GetFileByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.FileResponse, error) {
|
||||
files, err := p.fileRepo.GetByOrganizationID(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get files by organization: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.FileEntitiesToResponses(files)
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *FileProcessorImpl) GetFileByUserID(ctx context.Context, userID uuid.UUID) ([]*models.FileResponse, error) {
|
||||
files, err := p.fileRepo.GetByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get files by user: %w", err)
|
||||
}
|
||||
|
||||
responses := mappers.FileEntitiesToResponses(files)
|
||||
return responses, nil
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type InventoryProcessor interface {
|
||||
CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error)
|
||||
UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error)
|
||||
DeleteInventory(ctx context.Context, id uuid.UUID) error
|
||||
GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error)
|
||||
ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error)
|
||||
AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error)
|
||||
GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error)
|
||||
GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error)
|
||||
}
|
||||
|
||||
type InventoryRepository interface {
|
||||
Create(ctx context.Context, inventory *entities.Inventory) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
|
||||
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Inventory, error)
|
||||
GetByProductAndOutlet(ctx context.Context, productID, outletID uuid.UUID) (*entities.Inventory, error)
|
||||
GetByOutlet(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
|
||||
GetByProduct(ctx context.Context, productID uuid.UUID) ([]*entities.Inventory, error)
|
||||
GetLowStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
|
||||
GetZeroStock(ctx context.Context, outletID uuid.UUID) ([]*entities.Inventory, error)
|
||||
Update(ctx context.Context, inventory *entities.Inventory) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Inventory, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
AdjustQuantity(ctx context.Context, productID, outletID uuid.UUID, delta int) (*entities.Inventory, error)
|
||||
SetQuantity(ctx context.Context, productID, outletID uuid.UUID, quantity int) (*entities.Inventory, error)
|
||||
UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int) error
|
||||
BulkCreate(ctx context.Context, inventoryItems []*entities.Inventory) error
|
||||
BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error
|
||||
GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error)
|
||||
}
|
||||
|
||||
type InventoryProcessorImpl struct {
|
||||
inventoryRepo InventoryRepository
|
||||
productRepo ProductRepository
|
||||
outletRepo OutletRepository
|
||||
}
|
||||
|
||||
func NewInventoryProcessorImpl(
|
||||
inventoryRepo InventoryRepository,
|
||||
productRepo ProductRepository,
|
||||
outletRepo OutletRepository,
|
||||
) *InventoryProcessorImpl {
|
||||
return &InventoryProcessorImpl{
|
||||
inventoryRepo: inventoryRepo,
|
||||
productRepo: productRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) {
|
||||
_, err := p.productRepo.GetByID(ctx, req.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid product: %w", err)
|
||||
}
|
||||
|
||||
// Validate outlet exists
|
||||
_, err = p.outletRepo.GetByID(ctx, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet: %w", err)
|
||||
}
|
||||
|
||||
// Check if inventory already exists for this product-outlet combination
|
||||
existingInventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, req.ProductID, req.OutletID)
|
||||
if err == nil && existingInventory != nil {
|
||||
return nil, fmt.Errorf("inventory already exists for product %s in outlet %s", req.ProductID, req.OutletID)
|
||||
}
|
||||
|
||||
// Map request to entity
|
||||
inventoryEntity := mappers.CreateInventoryRequestToEntity(req)
|
||||
|
||||
// Create inventory
|
||||
if err := p.inventoryRepo.Create(ctx, inventoryEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create inventory: %w", err)
|
||||
}
|
||||
|
||||
// Get inventory with relations for response
|
||||
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, inventoryEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created inventory: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) {
|
||||
// Get existing inventory
|
||||
existingInventory, err := p.inventoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inventory not found: %w", err)
|
||||
}
|
||||
|
||||
// Apply updates to entity
|
||||
mappers.UpdateInventoryEntityFromRequest(existingInventory, req)
|
||||
|
||||
// Update inventory
|
||||
if err := p.inventoryRepo.Update(ctx, existingInventory); err != nil {
|
||||
return nil, fmt.Errorf("failed to update inventory: %w", err)
|
||||
}
|
||||
|
||||
// Get updated inventory with relations for response
|
||||
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated inventory: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UUID) error {
|
||||
// Check if inventory exists
|
||||
_, err := p.inventoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inventory not found: %w", err)
|
||||
}
|
||||
|
||||
// Delete inventory
|
||||
if err := p.inventoryRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete inventory: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) {
|
||||
inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inventory not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.InventoryEntityToResponse(inventoryEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
inventoryEntities, total, err := p.inventoryRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list inventory: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.InventoryResponse, len(inventoryEntities))
|
||||
for i, entity := range inventoryEntities {
|
||||
response := mappers.InventoryEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, int(total), nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) {
|
||||
// Validate product exists
|
||||
_, err := p.productRepo.GetByID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid product: %w", err)
|
||||
}
|
||||
|
||||
// Validate outlet exists
|
||||
_, err = p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet: %w", err)
|
||||
}
|
||||
|
||||
// Perform quantity adjustment
|
||||
adjustedInventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, req.Delta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err)
|
||||
}
|
||||
|
||||
// Get inventory with relations for response
|
||||
inventoryWithRelations, err := p.inventoryRepo.GetWithRelations(ctx, adjustedInventory.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve adjusted inventory: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.InventoryEntityToResponse(inventoryWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
|
||||
// Validate outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet: %w", err)
|
||||
}
|
||||
|
||||
inventoryEntities, err := p.inventoryRepo.GetLowStock(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get low stock items: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.InventoryResponse, len(inventoryEntities))
|
||||
for i, entity := range inventoryEntities {
|
||||
response := mappers.InventoryEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (p *InventoryProcessorImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) {
|
||||
// Validate outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid outlet: %w", err)
|
||||
}
|
||||
|
||||
inventoryEntities, err := p.inventoryRepo.GetZeroStock(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get zero stock items: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.InventoryResponse, len(inventoryEntities))
|
||||
for i, entity := range inventoryEntities {
|
||||
response := mappers.InventoryEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderProcessor interface {
|
||||
CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error)
|
||||
AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error)
|
||||
UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error)
|
||||
GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error)
|
||||
ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error)
|
||||
VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error
|
||||
RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error
|
||||
CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error)
|
||||
RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error)
|
||||
}
|
||||
|
||||
type OrderRepository interface {
|
||||
Create(ctx context.Context, order *entities.Order) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error)
|
||||
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error)
|
||||
Update(ctx context.Context, order *entities.Order) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error)
|
||||
GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error)
|
||||
ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error)
|
||||
VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error
|
||||
VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error
|
||||
RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error
|
||||
UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error
|
||||
GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type OrderItemRepository interface {
|
||||
Create(ctx context.Context, orderItem *entities.OrderItem) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error)
|
||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error)
|
||||
Update(ctx context.Context, orderItem *entities.OrderItem) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error
|
||||
}
|
||||
|
||||
type PaymentRepository interface {
|
||||
Create(ctx context.Context, payment *entities.Payment) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error)
|
||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error)
|
||||
Update(ctx context.Context, payment *entities.Payment) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error
|
||||
GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error)
|
||||
}
|
||||
|
||||
type PaymentMethodRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error)
|
||||
}
|
||||
|
||||
type ProductVariantRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.ProductVariant, error)
|
||||
}
|
||||
|
||||
type CustomerRepository interface {
|
||||
GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error)
|
||||
}
|
||||
|
||||
type SimplePaymentMethodRepository struct{}
|
||||
|
||||
func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) {
|
||||
// TODO: Implement proper payment method repository
|
||||
// For now, return a mock payment method
|
||||
return &entities.PaymentMethod{
|
||||
ID: id,
|
||||
Name: "Cash",
|
||||
Type: entities.PaymentMethodTypeCash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type OrderProcessorImpl struct {
|
||||
orderRepo OrderRepository
|
||||
orderItemRepo OrderItemRepository
|
||||
paymentRepo PaymentRepository
|
||||
productRepo ProductRepository
|
||||
paymentMethodRepo PaymentMethodRepository
|
||||
inventoryRepo InventoryRepository
|
||||
productVariantRepo ProductVariantRepository
|
||||
outletRepo OutletRepository
|
||||
customerRepo CustomerRepository
|
||||
}
|
||||
|
||||
func NewOrderProcessorImpl(
|
||||
orderRepo OrderRepository,
|
||||
orderItemRepo OrderItemRepository,
|
||||
paymentRepo PaymentRepository,
|
||||
productRepo ProductRepository,
|
||||
paymentMethodRepo PaymentMethodRepository,
|
||||
inventoryRepo InventoryRepository,
|
||||
productVariantRepo ProductVariantRepository,
|
||||
outletRepo OutletRepository,
|
||||
customerRepo CustomerRepository,
|
||||
) *OrderProcessorImpl {
|
||||
return &OrderProcessorImpl{
|
||||
orderRepo: orderRepo,
|
||||
orderItemRepo: orderItemRepo,
|
||||
paymentRepo: paymentRepo,
|
||||
productRepo: productRepo,
|
||||
paymentMethodRepo: paymentMethodRepo,
|
||||
inventoryRepo: inventoryRepo,
|
||||
productVariantRepo: productVariantRepo,
|
||||
outletRepo: outletRepo,
|
||||
customerRepo: customerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) {
|
||||
orderNumber, err := p.orderRepo.GetNextOrderNumber(ctx, organizationID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate order number: %w", err)
|
||||
}
|
||||
|
||||
outlet, err := p.outletRepo.GetByID(ctx, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
var subtotal, totalCost float64
|
||||
var orderItems []*entities.OrderItem
|
||||
|
||||
for _, itemReq := range req.OrderItems {
|
||||
product, err := p.productRepo.GetByID(ctx, itemReq.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
if variant.ProductID != itemReq.ProductID {
|
||||
return nil, fmt.Errorf("product variant does not belong to the specified product")
|
||||
}
|
||||
|
||||
unitPrice += variant.PriceModifier
|
||||
if variant.Cost > 0 {
|
||||
unitCost = variant.Cost
|
||||
}
|
||||
}
|
||||
|
||||
itemTotalPrice := float64(itemReq.Quantity) * unitPrice
|
||||
itemTotalCost := float64(itemReq.Quantity) * unitCost
|
||||
|
||||
subtotal += itemTotalPrice
|
||||
totalCost += itemTotalCost
|
||||
|
||||
orderItem := &entities.OrderItem{
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
Modifiers: entities.Modifiers(itemReq.Modifiers),
|
||||
Notes: itemReq.Notes,
|
||||
Metadata: entities.Metadata(itemReq.Metadata),
|
||||
Status: entities.OrderItemStatusPending,
|
||||
}
|
||||
|
||||
orderItems = append(orderItems, orderItem)
|
||||
}
|
||||
|
||||
taxAmount := subtotal * outlet.TaxRate
|
||||
totalAmount := subtotal + taxAmount
|
||||
|
||||
metadata := entities.Metadata(req.Metadata)
|
||||
if req.CustomerName != nil {
|
||||
if metadata == nil {
|
||||
metadata = make(entities.Metadata)
|
||||
}
|
||||
metadata["customer_name"] = *req.CustomerName
|
||||
}
|
||||
order := &entities.Order{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: req.OutletID,
|
||||
UserID: req.UserID,
|
||||
CustomerID: req.CustomerID,
|
||||
OrderNumber: orderNumber,
|
||||
TableNumber: req.TableNumber,
|
||||
OrderType: entities.OrderType(req.OrderType),
|
||||
Status: entities.OrderStatusPending,
|
||||
Subtotal: subtotal,
|
||||
TaxAmount: taxAmount,
|
||||
DiscountAmount: 0,
|
||||
TotalAmount: totalAmount,
|
||||
TotalCost: totalCost,
|
||||
PaymentStatus: entities.PaymentStatusPending,
|
||||
IsVoid: false,
|
||||
IsRefund: false,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Create(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order: %w", err)
|
||||
}
|
||||
|
||||
for _, orderItem := range orderItems {
|
||||
orderItem.OrderID = order.ID
|
||||
if err := p.orderItemRepo.Create(ctx, orderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, order.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created order: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
|
||||
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot modify voided order")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return nil, fmt.Errorf("cannot modify fully paid order")
|
||||
}
|
||||
|
||||
// Get outlet information for tax rate
|
||||
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
var newSubtotal, newTotalCost float64
|
||||
var addedOrderItems []*entities.OrderItem
|
||||
|
||||
for _, itemReq := range req.OrderItems {
|
||||
product, err := p.productRepo.GetByID(ctx, itemReq.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
// Use product price from database
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
// Handle product variant if specified
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify variant belongs to the product
|
||||
if variant.ProductID != itemReq.ProductID {
|
||||
return nil, fmt.Errorf("product variant does not belong to the specified product")
|
||||
}
|
||||
|
||||
// Apply price modifier
|
||||
unitPrice += variant.PriceModifier
|
||||
// Use variant cost if available, otherwise use product cost
|
||||
if variant.Cost > 0 {
|
||||
unitCost = variant.Cost
|
||||
}
|
||||
}
|
||||
|
||||
itemTotalPrice := float64(itemReq.Quantity) * unitPrice
|
||||
itemTotalCost := float64(itemReq.Quantity) * unitCost
|
||||
|
||||
newSubtotal += itemTotalPrice
|
||||
newTotalCost += itemTotalCost
|
||||
|
||||
orderItem := &entities.OrderItem{
|
||||
OrderID: orderID,
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
Modifiers: entities.Modifiers(itemReq.Modifiers),
|
||||
Notes: itemReq.Notes,
|
||||
Metadata: entities.Metadata(itemReq.Metadata),
|
||||
Status: entities.OrderItemStatusPending,
|
||||
}
|
||||
|
||||
addedOrderItems = append(addedOrderItems, orderItem)
|
||||
}
|
||||
|
||||
order.Subtotal += newSubtotal
|
||||
order.TotalCost += newTotalCost
|
||||
// Recalculate tax amount using outlet's tax rate
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
for k, v := range req.Metadata {
|
||||
order.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
var addedItemResponses []models.OrderItemResponse
|
||||
for _, orderItem := range addedOrderItems {
|
||||
if err := p.orderItemRepo.Create(ctx, orderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
|
||||
itemResponse := models.OrderItemResponse{
|
||||
ID: orderItem.ID,
|
||||
OrderID: orderItem.OrderID,
|
||||
ProductID: orderItem.ProductID,
|
||||
ProductVariantID: orderItem.ProductVariantID,
|
||||
Quantity: orderItem.Quantity,
|
||||
UnitPrice: orderItem.UnitPrice,
|
||||
TotalPrice: orderItem.TotalPrice,
|
||||
UnitCost: orderItem.UnitCost,
|
||||
TotalCost: orderItem.TotalCost,
|
||||
RefundAmount: orderItem.RefundAmount,
|
||||
RefundQuantity: orderItem.RefundQuantity,
|
||||
IsPartiallyRefunded: orderItem.IsPartiallyRefunded,
|
||||
IsFullyRefunded: orderItem.IsFullyRefunded,
|
||||
RefundReason: orderItem.RefundReason,
|
||||
RefundedAt: orderItem.RefundedAt,
|
||||
RefundedBy: orderItem.RefundedBy,
|
||||
Modifiers: []map[string]interface{}(orderItem.Modifiers),
|
||||
Notes: orderItem.Notes,
|
||||
Metadata: map[string]interface{}(orderItem.Metadata),
|
||||
Status: constants.OrderItemStatus(orderItem.Status),
|
||||
CreatedAt: orderItem.CreatedAt,
|
||||
UpdatedAt: orderItem.UpdatedAt,
|
||||
}
|
||||
addedItemResponses = append(addedItemResponses, itemResponse)
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
|
||||
}
|
||||
|
||||
updatedOrderResponse := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
|
||||
return &models.AddToOrderResponse{
|
||||
OrderID: orderID,
|
||||
OrderNumber: order.OrderNumber,
|
||||
AddedItems: addedItemResponses,
|
||||
UpdatedOrder: *updatedOrderResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
|
||||
// Get existing order
|
||||
order, err := p.orderRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if order can be modified
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot modify voided order")
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if req.TableNumber != nil {
|
||||
order.TableNumber = req.TableNumber
|
||||
}
|
||||
if req.Status != nil {
|
||||
order.Status = entities.OrderStatus(*req.Status)
|
||||
}
|
||||
if req.DiscountAmount != nil {
|
||||
order.DiscountAmount = *req.DiscountAmount
|
||||
// Recalculate total amount
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
for k, v := range req.Metadata {
|
||||
order.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Update order
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
// Get updated order with relations
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) {
|
||||
order, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(order)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) {
|
||||
filters := make(map[string]interface{})
|
||||
if req.OrganizationID != nil {
|
||||
filters["organization_id"] = *req.OrganizationID
|
||||
}
|
||||
if req.OutletID != nil {
|
||||
filters["outlet_id"] = *req.OutletID
|
||||
}
|
||||
if req.UserID != nil {
|
||||
filters["user_id"] = *req.UserID
|
||||
}
|
||||
if req.CustomerID != nil {
|
||||
filters["customer_id"] = *req.CustomerID
|
||||
}
|
||||
if req.OrderType != nil {
|
||||
filters["order_type"] = string(*req.OrderType)
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = string(*req.Status)
|
||||
}
|
||||
if req.PaymentStatus != nil {
|
||||
filters["payment_status"] = string(*req.PaymentStatus)
|
||||
}
|
||||
if req.IsVoid != nil {
|
||||
filters["is_void"] = *req.IsVoid
|
||||
}
|
||||
if req.IsRefund != nil {
|
||||
filters["is_refund"] = *req.IsRefund
|
||||
}
|
||||
if req.DateFrom != nil {
|
||||
filters["date_from"] = *req.DateFrom
|
||||
}
|
||||
if req.DateTo != nil {
|
||||
filters["date_to"] = *req.DateTo
|
||||
}
|
||||
if req.Search != "" {
|
||||
filters["search"] = req.Search
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
orders, total, err := p.orderRepo.List(ctx, filters, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list orders: %w", err)
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
orderResponses := make([]models.OrderResponse, len(orders))
|
||||
for i, order := range orders {
|
||||
response := mappers.OrderEntityToResponse(order)
|
||||
if response != nil {
|
||||
orderResponses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(total) / req.Limit
|
||||
if int(total)%req.Limit > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &models.ListOrdersResponse{
|
||||
Orders: orderResponses,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error {
|
||||
if req.OrderID != req.OrderID {
|
||||
return fmt.Errorf("order ID mismatch: path parameter does not match request body")
|
||||
}
|
||||
|
||||
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return fmt.Errorf("order is already voided")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return fmt.Errorf("cannot void fully paid order")
|
||||
}
|
||||
|
||||
if req.Type == "ALL" {
|
||||
// Update order status to cancelled and mark as voided in a single transaction
|
||||
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order: %w", err)
|
||||
}
|
||||
} else if req.Type == "ITEM" {
|
||||
if len(req.Items) == 0 {
|
||||
return fmt.Errorf("items list is required when voiding specific items")
|
||||
}
|
||||
|
||||
var totalVoidedAmount float64
|
||||
var totalVoidedCost float64
|
||||
|
||||
for _, itemVoid := range req.Items {
|
||||
orderItemID := itemVoid.OrderItemID
|
||||
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, orderItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order item not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify the order item belongs to this order
|
||||
if orderItem.OrderID != req.OrderID {
|
||||
return fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
|
||||
// Validate void quantity
|
||||
if itemVoid.Quantity > orderItem.Quantity {
|
||||
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
|
||||
}
|
||||
|
||||
// Calculate voided amounts
|
||||
voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice
|
||||
voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost
|
||||
|
||||
totalVoidedAmount += voidedAmount
|
||||
totalVoidedCost += voidedCost
|
||||
|
||||
// Void the order item
|
||||
if err := p.orderItemRepo.VoidOrderItem(ctx, orderItemID, itemVoid.Quantity, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order item %d: %w", itemVoid.OrderItemID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get outlet information for tax rate
|
||||
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Update order totals
|
||||
order.Subtotal -= totalVoidedAmount
|
||||
order.TotalCost -= totalVoidedCost
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
// Update the order
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order totals: %w", err)
|
||||
}
|
||||
|
||||
// Check if all items are voided, then void the entire order
|
||||
remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remaining order items: %w", err)
|
||||
}
|
||||
|
||||
allItemsVoided := true
|
||||
for _, item := range remainingItems {
|
||||
if item.Quantity > 0 {
|
||||
allItemsVoided = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allItemsVoided {
|
||||
// Update order status to cancelled and mark as voided when all items are voided
|
||||
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order after all items voided: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid void type: must be 'ALL' or 'ITEM'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error {
|
||||
order, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if order can be refunded
|
||||
if order.IsRefund {
|
||||
return fmt.Errorf("order is already refunded")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != entities.PaymentStatusCompleted {
|
||||
return fmt.Errorf("order is not paid, cannot refund")
|
||||
}
|
||||
|
||||
reason := "No reason provided"
|
||||
if req.Reason != nil {
|
||||
reason = *req.Reason
|
||||
}
|
||||
|
||||
// Process refund based on request type
|
||||
if req.RefundAmount != nil {
|
||||
// Full or partial refund by amount
|
||||
if *req.RefundAmount > order.TotalAmount {
|
||||
return fmt.Errorf("refund amount cannot exceed order total")
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order.RefundAmount = *req.RefundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
// Mark order as refunded
|
||||
if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to mark order as refunded: %w", err)
|
||||
}
|
||||
|
||||
} else if len(req.OrderItems) > 0 {
|
||||
// Refund by specific items
|
||||
totalRefundAmount := float64(0)
|
||||
|
||||
for _, itemRefund := range req.OrderItems {
|
||||
// Get order item
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, itemRefund.OrderItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order item not found: %w", err)
|
||||
}
|
||||
|
||||
if orderItem.OrderID != id {
|
||||
return fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
|
||||
// Calculate refund amount for this item
|
||||
refundQuantity := itemRefund.RefundQuantity
|
||||
if refundQuantity == 0 {
|
||||
refundQuantity = orderItem.Quantity
|
||||
}
|
||||
|
||||
if refundQuantity > orderItem.Quantity {
|
||||
return fmt.Errorf("refund quantity cannot exceed original quantity")
|
||||
}
|
||||
|
||||
refundAmount := float64(refundQuantity) * orderItem.UnitPrice
|
||||
if itemRefund.RefundAmount != nil {
|
||||
refundAmount = *itemRefund.RefundAmount
|
||||
}
|
||||
|
||||
// Process item refund
|
||||
itemReason := reason
|
||||
if itemRefund.Reason != nil {
|
||||
itemReason = *itemRefund.Reason
|
||||
}
|
||||
|
||||
if err := p.orderItemRepo.RefundOrderItem(ctx, itemRefund.OrderItemID, refundQuantity, refundAmount, itemReason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to refund order item: %w", err)
|
||||
}
|
||||
|
||||
totalRefundAmount += refundAmount
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order.RefundAmount = totalRefundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
// Mark order as refunded
|
||||
if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to mark order as refunded: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) {
|
||||
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot process payment for voided order")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return nil, fmt.Errorf("order is already fully paid")
|
||||
}
|
||||
|
||||
_, err = p.paymentMethodRepo.GetByID(ctx, req.PaymentMethodID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid: %w", err)
|
||||
}
|
||||
|
||||
remainingAmount := order.TotalAmount - totalPaid
|
||||
if req.Amount > remainingAmount {
|
||||
return nil, fmt.Errorf("payment amount exceeds remaining balance")
|
||||
}
|
||||
|
||||
payment := &entities.Payment{
|
||||
OrderID: req.OrderID,
|
||||
PaymentMethodID: req.PaymentMethodID,
|
||||
Amount: req.Amount,
|
||||
Status: entities.PaymentTransactionStatusCompleted,
|
||||
TransactionID: req.TransactionID,
|
||||
SplitNumber: req.SplitNumber,
|
||||
SplitTotal: req.SplitTotal,
|
||||
SplitDescription: req.SplitDescription,
|
||||
Metadata: entities.Metadata(req.Metadata),
|
||||
}
|
||||
|
||||
if err := p.paymentRepo.Create(ctx, payment); err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment: %w", err)
|
||||
}
|
||||
|
||||
if len(req.PaymentOrderItems) > 0 {
|
||||
for _, itemPayment := range req.PaymentOrderItems {
|
||||
paymentOrderItem := &entities.PaymentOrderItem{
|
||||
PaymentID: payment.ID,
|
||||
OrderItemID: itemPayment.OrderItemID,
|
||||
Amount: itemPayment.Amount,
|
||||
}
|
||||
|
||||
fmt.Println(paymentOrderItem)
|
||||
// TODO: Create payment order item in database
|
||||
// This would require a PaymentOrderItemRepository
|
||||
}
|
||||
}
|
||||
|
||||
// Update order payment status if fully paid
|
||||
newTotalPaid := totalPaid + req.Amount
|
||||
orderJustCompleted := false
|
||||
if newTotalPaid >= order.TotalAmount {
|
||||
if order.PaymentStatus != entities.PaymentStatusCompleted {
|
||||
orderJustCompleted = true
|
||||
}
|
||||
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusCompleted); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
||||
}
|
||||
// Set order status to completed when fully paid
|
||||
if err := p.orderRepo.UpdateStatus(ctx, req.OrderID, entities.OrderStatusCompleted); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order status: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusPartiallyRefunded); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if orderJustCompleted {
|
||||
orderItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
||||
}
|
||||
for _, item := range orderItems {
|
||||
if _, err := p.inventoryRepo.AdjustQuantity(ctx, item.ProductID, order.OutletID, -item.Quantity); err != nil {
|
||||
return nil, fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get payment with relations for response
|
||||
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.PaymentEntityToResponse(paymentWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
|
||||
// Get payment
|
||||
payment, err := p.paymentRepo.GetByID(ctx, paymentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if payment can be refunded
|
||||
if payment.Status != entities.PaymentTransactionStatusCompleted {
|
||||
return fmt.Errorf("payment is not completed, cannot refund")
|
||||
}
|
||||
|
||||
if refundAmount > payment.Amount {
|
||||
return fmt.Errorf("refund amount cannot exceed payment amount")
|
||||
}
|
||||
|
||||
// Process refund
|
||||
if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to refund payment: %w", err)
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order, err := p.orderRepo.GetByID(ctx, payment.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get order: %w", err)
|
||||
}
|
||||
|
||||
order.RefundAmount += refundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
||||
// Get the order
|
||||
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify order belongs to the organization
|
||||
if order.OrganizationID != organizationID {
|
||||
return nil, fmt.Errorf("order does not belong to the organization")
|
||||
}
|
||||
|
||||
// Check if order status is pending (only pending orders can have customer set)
|
||||
if order.Status != entities.OrderStatusPending {
|
||||
return nil, fmt.Errorf("customer can only be set for pending orders")
|
||||
}
|
||||
|
||||
// Verify customer exists and belongs to the organization
|
||||
customer, err := p.customerRepo.GetByIDAndOrganization(ctx, req.CustomerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customer not found or does not belong to the organization: %w", err)
|
||||
}
|
||||
|
||||
// Update order with customer ID
|
||||
order.CustomerID = &req.CustomerID
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order with customer: %w", err)
|
||||
}
|
||||
|
||||
response := &models.SetOrderCustomerResponse{
|
||||
OrderID: orderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Message: fmt.Sprintf("Customer '%s' successfully set for order", customer.Name),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrganizationProcessor interface {
|
||||
CreateOrganization(ctx context.Context, req *models.CreateOrganizationRequest) (*models.CreateOrganizationResponse, error)
|
||||
UpdateOrganization(ctx context.Context, id uuid.UUID, req *models.UpdateOrganizationRequest) (*models.OrganizationResponse, error)
|
||||
DeleteOrganization(ctx context.Context, id uuid.UUID) error
|
||||
GetOrganizationByID(ctx context.Context, id uuid.UUID) (*models.OrganizationResponse, error)
|
||||
ListOrganizations(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.OrganizationResponse, int, error)
|
||||
}
|
||||
|
||||
type OrganizationProcessorImpl struct {
|
||||
organizationRepo OrganizationRepository
|
||||
outletRepo OutletRepository
|
||||
userRepo UserRepository
|
||||
}
|
||||
|
||||
func NewOrganizationProcessorImpl(
|
||||
organizationRepo OrganizationRepository,
|
||||
outletRepo OutletRepository,
|
||||
userRepo UserRepository,
|
||||
) *OrganizationProcessorImpl {
|
||||
return &OrganizationProcessorImpl{
|
||||
organizationRepo: organizationRepo,
|
||||
outletRepo: outletRepo,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrganizationProcessorImpl) CreateOrganization(ctx context.Context, req *models.CreateOrganizationRequest) (*models.CreateOrganizationResponse, error) {
|
||||
if req.OrganizationEmail != nil && *req.OrganizationEmail != "" {
|
||||
existingOrg, err := p.organizationRepo.GetByEmail(ctx, *req.OrganizationEmail)
|
||||
if err == nil && existingOrg != nil {
|
||||
return nil, fmt.Errorf("organization with email %s already exists", *req.OrganizationEmail)
|
||||
}
|
||||
}
|
||||
|
||||
existingUser, err := p.userRepo.GetByEmail(ctx, req.AdminEmail)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, fmt.Errorf("user with email %s already exists", req.AdminEmail)
|
||||
}
|
||||
|
||||
organizationEntity := &entities.Organization{
|
||||
Name: req.OrganizationName,
|
||||
Email: req.OrganizationEmail,
|
||||
PhoneNumber: req.OrganizationPhoneNumber,
|
||||
PlanType: string(req.PlanType),
|
||||
}
|
||||
|
||||
err = p.organizationRepo.Create(ctx, organizationEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create organization: %w", err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.AdminPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
adminUserEntity := &entities.User{
|
||||
OrganizationID: organizationEntity.ID,
|
||||
Name: req.AdminName,
|
||||
Email: req.AdminEmail,
|
||||
PasswordHash: string(passwordHash),
|
||||
Role: entities.RoleAdmin,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err = p.userRepo.Create(ctx, adminUserEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create admin user: %w", err)
|
||||
}
|
||||
|
||||
defaultOutletEntity := &entities.Outlet{
|
||||
OrganizationID: organizationEntity.ID,
|
||||
Name: req.OutletName,
|
||||
Address: req.OutletAddress,
|
||||
Timezone: req.OutletTimezone,
|
||||
Currency: req.OutletCurrency,
|
||||
TaxRate: 0.0,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err = p.outletRepo.Create(ctx, defaultOutletEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create default outlet: %w", err)
|
||||
}
|
||||
|
||||
organizationResponse := mappers.OrganizationEntityToResponse(organizationEntity)
|
||||
adminUserResponse := mappers.UserEntityToResponse(adminUserEntity)
|
||||
outletResponse := mappers.OutletEntityToResponse(defaultOutletEntity)
|
||||
|
||||
return &models.CreateOrganizationResponse{
|
||||
Organization: organizationResponse,
|
||||
AdminUser: adminUserResponse,
|
||||
DefaultOutlet: outletResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrganizationProcessorImpl) UpdateOrganization(ctx context.Context, id uuid.UUID, req *models.UpdateOrganizationRequest) (*models.OrganizationResponse, error) {
|
||||
existingOrg, err := p.organizationRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
if req.Email != nil && (existingOrg.Email == nil || *req.Email != *existingOrg.Email) {
|
||||
existingOrgByEmail, err := p.organizationRepo.GetByEmail(ctx, *req.Email)
|
||||
if err == nil && existingOrgByEmail != nil && existingOrgByEmail.ID != id {
|
||||
return nil, fmt.Errorf("organization with email %s already exists", *req.Email)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
existingOrg.Name = *req.Name
|
||||
}
|
||||
if req.Email != nil {
|
||||
existingOrg.Email = req.Email
|
||||
}
|
||||
if req.PhoneNumber != nil {
|
||||
existingOrg.PhoneNumber = req.PhoneNumber
|
||||
}
|
||||
if req.PlanType != nil {
|
||||
existingOrg.PlanType = string(*req.PlanType)
|
||||
}
|
||||
|
||||
err = p.organizationRepo.Update(ctx, existingOrg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update organization: %w", err)
|
||||
}
|
||||
|
||||
return mappers.OrganizationEntityToResponse(existingOrg), nil
|
||||
}
|
||||
|
||||
func (p *OrganizationProcessorImpl) DeleteOrganization(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := p.organizationRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.organizationRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete organization: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrganizationProcessorImpl) GetOrganizationByID(ctx context.Context, id uuid.UUID) (*models.OrganizationResponse, error) {
|
||||
organization, err := p.organizationRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.OrganizationEntityToResponse(organization), nil
|
||||
}
|
||||
|
||||
func (p *OrganizationProcessorImpl) ListOrganizations(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.OrganizationResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
organizations, totalCount, err := p.organizationRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get organizations: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.OrganizationResponse, len(organizations))
|
||||
for i, org := range organizations {
|
||||
response := mappers.OrganizationEntityToResponse(org)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, int(totalCount), nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrganizationRepository interface {
|
||||
Create(ctx context.Context, org *entities.Organization) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Organization, error)
|
||||
GetWithOutlets(ctx context.Context, id uuid.UUID) (*entities.Organization, error)
|
||||
GetByPlanType(ctx context.Context, planType string) ([]*entities.Organization, error)
|
||||
UpdatePlanType(ctx context.Context, id uuid.UUID, planType string) error
|
||||
Update(ctx context.Context, org *entities.Organization) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Organization, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
GetByEmail(ctx context.Context, email string) (*entities.Organization, error)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/appcontext"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OutletProcessor interface {
|
||||
ListOutletsByOrganization(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]*models.OutletResponse, int64, error)
|
||||
GetOutletByID(ctx context.Context, organizationID uuid.UUID, outletID uuid.UUID) (*models.OutletResponse, error)
|
||||
CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error)
|
||||
UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletRequest) (*models.OutletResponse, error)
|
||||
DeleteOutlet(ctx context.Context, outletID uuid.UUID) error
|
||||
}
|
||||
|
||||
type OutletProcessorImpl struct {
|
||||
outletRepo *repository.OutletRepositoryImpl
|
||||
}
|
||||
|
||||
func NewOutletProcessorImpl(outletRepo *repository.OutletRepositoryImpl) *OutletProcessorImpl {
|
||||
return &OutletProcessorImpl{
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) ListOutletsByOrganization(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]*models.OutletResponse, int64, error) {
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// Get outlets with pagination
|
||||
outlets, total, err := p.outletRepo.GetByOrganizationIDWithPagination(ctx, organizationID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get outlets: %w", err)
|
||||
}
|
||||
|
||||
// Convert to response models
|
||||
responses := make([]*models.OutletResponse, len(outlets))
|
||||
for i, outlet := range outlets {
|
||||
responses[i] = mappers.OutletEntityToResponse(outlet)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) GetOutletByID(ctx context.Context, organizationID, outletID uuid.UUID) (*models.OutletResponse, error) {
|
||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
if outlet.OrganizationID != organizationID {
|
||||
return nil, fmt.Errorf("outlet does not belong to the organization")
|
||||
}
|
||||
|
||||
response := mappers.OutletEntityToResponse(outlet)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) CreateOutlet(ctx context.Context, req *models.CreateOutletRequest) (*models.OutletResponse, error) {
|
||||
// Get organization ID from context
|
||||
contextInfo := appcontext.FromContext(ctx)
|
||||
if contextInfo.OrganizationID == uuid.Nil {
|
||||
return nil, fmt.Errorf("organization ID not found in context")
|
||||
}
|
||||
|
||||
// Set organization ID from context
|
||||
req.OrganizationID = contextInfo.OrganizationID
|
||||
|
||||
// Create outlet entity
|
||||
outlet := &entities.Outlet{
|
||||
OrganizationID: req.OrganizationID,
|
||||
Name: req.Name,
|
||||
Address: &req.Address,
|
||||
Currency: string(req.Currency),
|
||||
TaxRate: req.TaxRate,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
err := p.outletRepo.Create(ctx, outlet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create outlet: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OutletEntityToResponse(outlet)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) UpdateOutlet(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletRequest) (*models.OutletResponse, error) {
|
||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
if outlet.OrganizationID != req.OrganizationID {
|
||||
return nil, fmt.Errorf("outlet does not belong to the organization")
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
outlet.Name = *req.Name
|
||||
}
|
||||
if req.Address != nil {
|
||||
outlet.Address = req.Address
|
||||
}
|
||||
if req.TaxRate != nil {
|
||||
outlet.TaxRate = *req.TaxRate
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
outlet.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
err = p.outletRepo.Update(ctx, outlet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update outlet: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OutletEntityToResponse(outlet)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OutletProcessorImpl) DeleteOutlet(ctx context.Context, outletID uuid.UUID) error {
|
||||
contextInfo := appcontext.FromContext(ctx)
|
||||
if contextInfo.OrganizationID == uuid.Nil {
|
||||
return fmt.Errorf("organization ID not found in context")
|
||||
}
|
||||
|
||||
// Get existing outlet
|
||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
if outlet.OrganizationID != contextInfo.OrganizationID {
|
||||
return fmt.Errorf("outlet does not belong to the organization")
|
||||
}
|
||||
|
||||
err = p.outletRepo.Delete(ctx, outletID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete outlet: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OutletRepository interface {
|
||||
Create(ctx context.Context, outlet *entities.Outlet) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Outlet, error)
|
||||
GetWithOrders(ctx context.Context, id uuid.UUID) (*entities.Outlet, error)
|
||||
GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.Outlet, error)
|
||||
GetByOrganizationIDWithPagination(ctx context.Context, organizationID uuid.UUID, limit, offset int) ([]*entities.Outlet, int64, error)
|
||||
Update(ctx context.Context, outlet *entities.Outlet) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Outlet, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OutletSettingProcessorImpl struct {
|
||||
outletSettingRepo *repository.OutletSettingRepositoryImpl
|
||||
outletRepo *repository.OutletRepositoryImpl
|
||||
}
|
||||
|
||||
func NewOutletSettingProcessorImpl(
|
||||
outletSettingRepo *repository.OutletSettingRepositoryImpl,
|
||||
outletRepo *repository.OutletRepositoryImpl,
|
||||
) *OutletSettingProcessorImpl {
|
||||
return &OutletSettingProcessorImpl{
|
||||
outletSettingRepo: outletSettingRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) CreateSetting(ctx context.Context, req *models.CreateOutletSettingRequest) (*models.OutletSettingResponse, error) {
|
||||
// Check if outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if setting already exists
|
||||
existingSetting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, req.OutletID, req.Key)
|
||||
if err == nil && existingSetting != nil {
|
||||
return nil, fmt.Errorf("setting with key '%s' already exists for this outlet", req.Key)
|
||||
}
|
||||
|
||||
setting := &entities.OutletSetting{
|
||||
OutletID: req.OutletID,
|
||||
Key: req.Key,
|
||||
Value: req.Value,
|
||||
}
|
||||
|
||||
err = p.outletSettingRepo.Create(ctx, setting)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create setting: %w", err)
|
||||
}
|
||||
|
||||
return &models.OutletSettingResponse{
|
||||
ID: setting.ID,
|
||||
OutletID: setting.OutletID,
|
||||
Key: setting.Key,
|
||||
Value: setting.Value,
|
||||
CreatedAt: setting.CreatedAt,
|
||||
UpdatedAt: setting.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) UpdateSetting(ctx context.Context, outletID uuid.UUID, key string, req *models.UpdateOutletSettingRequest) (*models.OutletSettingResponse, error) {
|
||||
// Check if outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Get existing setting
|
||||
setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting not found: %w", err)
|
||||
}
|
||||
|
||||
// Update setting
|
||||
setting.Value = req.Value
|
||||
err = p.outletSettingRepo.Update(ctx, setting)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update setting: %w", err)
|
||||
}
|
||||
|
||||
return &models.OutletSettingResponse{
|
||||
ID: setting.ID,
|
||||
OutletID: setting.OutletID,
|
||||
Key: setting.Key,
|
||||
Value: setting.Value,
|
||||
CreatedAt: setting.CreatedAt,
|
||||
UpdatedAt: setting.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) GetSetting(ctx context.Context, outletID uuid.UUID, key string) (*models.OutletSettingResponse, error) {
|
||||
// Check if outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("setting not found: %w", err)
|
||||
}
|
||||
|
||||
return &models.OutletSettingResponse{
|
||||
ID: setting.ID,
|
||||
OutletID: setting.OutletID,
|
||||
Key: setting.Key,
|
||||
Value: setting.Value,
|
||||
CreatedAt: setting.CreatedAt,
|
||||
UpdatedAt: setting.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) GetPrinterSettings(ctx context.Context, outletID uuid.UUID) (*models.OutletPrinterSettings, error) {
|
||||
// Check if outlet exists
|
||||
outlet, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Get printer settings from database
|
||||
settings, err := p.outletSettingRepo.GetPrinterSettingsByOutletID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get printer settings: %w", err)
|
||||
}
|
||||
|
||||
// Build printer settings with defaults
|
||||
printerSettings := &models.OutletPrinterSettings{
|
||||
OutletName: p.getSettingValue(settings, constants.PRINTER_OUTLET_NAME, outlet.Name),
|
||||
Address: p.getSettingValue(settings, constants.PRINTER_ADDRESS, ""),
|
||||
PhoneNumber: p.getSettingValue(settings, constants.PRINTER_PHONE_NUMBER, ""),
|
||||
PaperSize: p.getSettingValue(settings, constants.PRINTER_PAPER_SIZE, constants.DEFAULT_PAPER_SIZE),
|
||||
Footer: p.getSettingValue(settings, constants.PRINTER_FOOTER, constants.DEFAULT_FOOTER),
|
||||
FooterHashtag: p.getSettingValue(settings, constants.PRINTER_FOOTER_HASHTAG, constants.DEFAULT_FOOTER_HASHTAG),
|
||||
}
|
||||
|
||||
return printerSettings, nil
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) UpdatePrinterSettings(ctx context.Context, outletID uuid.UUID, req *models.UpdateOutletPrinterSettingsRequest) (*models.OutletPrinterSettings, error) {
|
||||
// Check if outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Update each setting if provided
|
||||
if req.OutletName != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_OUTLET_NAME, *req.OutletName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update outlet name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Address != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_ADDRESS, *req.Address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update address: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.PhoneNumber != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_PHONE_NUMBER, *req.PhoneNumber)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update phone number: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.PaperSize != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_PAPER_SIZE, *req.PaperSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update paper size: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Footer != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_FOOTER, *req.Footer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update footer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.FooterHashtag != nil {
|
||||
err = p.upsertSetting(ctx, outletID, constants.PRINTER_FOOTER_HASHTAG, *req.FooterHashtag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update footer hashtag: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Return updated settings
|
||||
return p.GetPrinterSettings(ctx, outletID)
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) DeleteSetting(ctx context.Context, outletID uuid.UUID, key string) error {
|
||||
// Check if outlet exists
|
||||
_, err := p.outletRepo.GetByID(ctx, outletID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.outletSettingRepo.DeleteByOutletIDAndKey(ctx, outletID, key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete setting: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) getSettingValue(settings map[string]string, key, defaultValue string) string {
|
||||
if value, exists := settings[key]; exists {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (p *OutletSettingProcessorImpl) upsertSetting(ctx context.Context, outletID uuid.UUID, key, value string) error {
|
||||
setting, err := p.outletSettingRepo.GetByOutletIDAndKey(ctx, outletID, key)
|
||||
if err != nil {
|
||||
// Setting doesn't exist, create new one
|
||||
setting = &entities.OutletSetting{
|
||||
OutletID: outletID,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
return p.outletSettingRepo.Create(ctx, setting)
|
||||
}
|
||||
|
||||
// Setting exists, update it
|
||||
setting.Value = value
|
||||
return p.outletSettingRepo.Update(ctx, setting)
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PaymentMethodProcessor interface {
|
||||
CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error)
|
||||
GetPaymentMethodByID(ctx context.Context, id uuid.UUID) (*models.PaymentMethodResponse, error)
|
||||
ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error)
|
||||
UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *models.UpdatePaymentMethodRequest) (*models.PaymentMethodResponse, error)
|
||||
DeletePaymentMethod(ctx context.Context, id uuid.UUID) error
|
||||
GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]models.PaymentMethodResponse, error)
|
||||
}
|
||||
|
||||
type PaymentMethodProcessorImpl struct {
|
||||
paymentMethodRepo repository.PaymentMethodRepository
|
||||
}
|
||||
|
||||
func NewPaymentMethodProcessorImpl(paymentMethodRepo repository.PaymentMethodRepository) *PaymentMethodProcessorImpl {
|
||||
return &PaymentMethodProcessorImpl{
|
||||
paymentMethodRepo: paymentMethodRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) CreatePaymentMethod(ctx context.Context, req *models.CreatePaymentMethodRequest) (*models.PaymentMethodResponse, error) {
|
||||
// Check if payment method with same name already exists
|
||||
exists, err := p.paymentMethodRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", req.Name)
|
||||
}
|
||||
|
||||
// Map request to entity
|
||||
paymentMethodEntity := mappers.CreatePaymentMethodRequestToEntity(req)
|
||||
|
||||
// Create payment method
|
||||
if err := p.paymentMethodRepo.Create(ctx, paymentMethodEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment method: %w", err)
|
||||
}
|
||||
|
||||
// Get created payment method
|
||||
createdPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, paymentMethodEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created payment method: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response
|
||||
response := mappers.PaymentMethodEntityToResponse(createdPaymentMethod)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) GetPaymentMethodByID(ctx context.Context, id uuid.UUID) (*models.PaymentMethodResponse, error) {
|
||||
paymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.PaymentMethodEntityToResponse(paymentMethod)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) ListPaymentMethods(ctx context.Context, req *models.ListPaymentMethodsRequest) (*models.ListPaymentMethodsResponse, error) {
|
||||
// Build filters
|
||||
filters := make(map[string]interface{})
|
||||
if req.OrganizationID != nil {
|
||||
filters["organization_id"] = *req.OrganizationID
|
||||
}
|
||||
if req.Type != nil {
|
||||
filters["type"] = string(*req.Type)
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
filters["is_active"] = *req.IsActive
|
||||
}
|
||||
if req.Search != "" {
|
||||
filters["search"] = req.Search
|
||||
}
|
||||
|
||||
// Calculate offset
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
|
||||
// Get payment methods
|
||||
paymentMethods, total, err := p.paymentMethodRepo.List(ctx, filters, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list payment methods: %w", err)
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
paymentMethodResponses := make([]models.PaymentMethodResponse, len(paymentMethods))
|
||||
for i, paymentMethod := range paymentMethods {
|
||||
response := mappers.PaymentMethodEntityToResponse(paymentMethod)
|
||||
if response != nil {
|
||||
paymentMethodResponses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(total) / req.Limit
|
||||
if int(total)%req.Limit > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &models.ListPaymentMethodsResponse{
|
||||
PaymentMethods: paymentMethodResponses,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) UpdatePaymentMethod(ctx context.Context, id uuid.UUID, req *models.UpdatePaymentMethodRequest) (*models.PaymentMethodResponse, error) {
|
||||
// Get existing payment method
|
||||
existingPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
// Check name uniqueness if name is being updated
|
||||
if req.Name != nil && *req.Name != existingPaymentMethod.Name {
|
||||
exists, err := p.paymentMethodRepo.ExistsByName(ctx, existingPaymentMethod.OrganizationID, *req.Name, &id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check payment method name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("payment method with name '%s' already exists for this organization", *req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
mappers.UpdatePaymentMethodEntityFromRequest(existingPaymentMethod, req)
|
||||
|
||||
// Update payment method
|
||||
if err := p.paymentMethodRepo.Update(ctx, existingPaymentMethod); err != nil {
|
||||
return nil, fmt.Errorf("failed to update payment method: %w", err)
|
||||
}
|
||||
|
||||
// Get updated payment method
|
||||
updatedPaymentMethod, err := p.paymentMethodRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated payment method: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.PaymentMethodEntityToResponse(updatedPaymentMethod)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) DeletePaymentMethod(ctx context.Context, id uuid.UUID) error {
|
||||
// Check if payment method exists
|
||||
_, err := p.paymentMethodRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Check if payment method is being used in any payments
|
||||
// For now, allow deletion
|
||||
|
||||
// Delete payment method
|
||||
if err := p.paymentMethodRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete payment method: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PaymentMethodProcessorImpl) GetActivePaymentMethodsByOrganization(ctx context.Context, organizationID uuid.UUID) ([]models.PaymentMethodResponse, error) {
|
||||
paymentMethods, err := p.paymentMethodRepo.GetActiveByOrganizationID(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get active payment methods: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.PaymentMethodResponse, len(paymentMethods))
|
||||
for i, paymentMethod := range paymentMethods {
|
||||
response := mappers.PaymentMethodEntityToResponse(paymentMethod)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductProcessor interface {
|
||||
CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error)
|
||||
UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error)
|
||||
DeleteProduct(ctx context.Context, id uuid.UUID) error
|
||||
GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error)
|
||||
ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error)
|
||||
}
|
||||
|
||||
type ProductRepository interface {
|
||||
Create(ctx context.Context, product *entities.Product) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Product, error)
|
||||
GetWithCategory(ctx context.Context, id uuid.UUID) (*entities.Product, error)
|
||||
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Product, error)
|
||||
GetByOrganization(ctx context.Context, organizationID uuid.UUID) ([]*entities.Product, error)
|
||||
GetByCategory(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error)
|
||||
GetByBusinessType(ctx context.Context, businessType string) ([]*entities.Product, error)
|
||||
GetActiveByCategoryID(ctx context.Context, categoryID uuid.UUID) ([]*entities.Product, error)
|
||||
Update(ctx context.Context, product *entities.Product) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Product, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
GetBySKU(ctx context.Context, organizationID uuid.UUID, sku string) (*entities.Product, error)
|
||||
ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error)
|
||||
GetByName(ctx context.Context, organizationID uuid.UUID, name string) (*entities.Product, error)
|
||||
ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error)
|
||||
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
||||
GetLowCostProducts(ctx context.Context, organizationID uuid.UUID, maxCost float64) ([]*entities.Product, error)
|
||||
}
|
||||
|
||||
type ProductProcessorImpl struct {
|
||||
productRepo ProductRepository
|
||||
categoryRepo CategoryRepository
|
||||
productVariantRepo repository.ProductVariantRepository
|
||||
inventoryRepo InventoryRepository
|
||||
outletRepo OutletRepository
|
||||
}
|
||||
|
||||
func NewProductProcessorImpl(productRepo ProductRepository, categoryRepo CategoryRepository, productVariantRepo repository.ProductVariantRepository, inventoryRepo InventoryRepository, outletRepo OutletRepository) *ProductProcessorImpl {
|
||||
return &ProductProcessorImpl{
|
||||
productRepo: productRepo,
|
||||
categoryRepo: categoryRepo,
|
||||
productVariantRepo: productVariantRepo,
|
||||
inventoryRepo: inventoryRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.CreateProductRequest) (*models.ProductResponse, error) {
|
||||
_, err := p.categoryRepo.GetByID(ctx, req.CategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid category: %w", err)
|
||||
}
|
||||
|
||||
if req.SKU != nil && *req.SKU != "" {
|
||||
exists, err := p.productRepo.ExistsBySKU(ctx, req.OrganizationID, *req.SKU, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU)
|
||||
}
|
||||
}
|
||||
|
||||
exists, err := p.productRepo.ExistsByName(ctx, req.OrganizationID, req.Name, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("product with name '%s' already exists for this organization", req.Name)
|
||||
}
|
||||
|
||||
productEntity := mappers.CreateProductRequestToEntity(req)
|
||||
|
||||
if err := p.productRepo.Create(ctx, productEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create product: %w", err)
|
||||
}
|
||||
|
||||
// Create variants if provided
|
||||
if req.Variants != nil && len(req.Variants) > 0 {
|
||||
for _, variantReq := range req.Variants {
|
||||
// Set the product ID for the variant
|
||||
variantReq.ProductID = productEntity.ID
|
||||
|
||||
// Check variant name uniqueness within the same product
|
||||
exists, err := p.productVariantRepo.ExistsByName(ctx, productEntity.ID, variantReq.Name, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("variant with name '%s' already exists for this product", variantReq.Name)
|
||||
}
|
||||
|
||||
variantEntity := mappers.CreateProductVariantRequestToEntity(&variantReq)
|
||||
if err := p.productVariantRepo.Create(ctx, variantEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create product variant: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create inventory records for all outlets if requested
|
||||
if req.CreateInventory {
|
||||
if err := p.createInventoryForAllOutlets(ctx, productEntity.ID, req.OrganizationID, req.InitialStock, req.ReorderLevel); err != nil {
|
||||
return nil, fmt.Errorf("failed to create inventory records: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
productWithCategory, err := p.productRepo.GetWithCategory(ctx, productEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created product: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.ProductEntityToResponse(productWithCategory)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID, req *models.UpdateProductRequest) (*models.ProductResponse, error) {
|
||||
existingProduct, err := p.productRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
if req.CategoryID != nil {
|
||||
_, err := p.categoryRepo.GetByID(ctx, *req.CategoryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid category: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if req.SKU != nil && *req.SKU != "" {
|
||||
currentSKU := ""
|
||||
if existingProduct.SKU != nil {
|
||||
currentSKU = *existingProduct.SKU
|
||||
}
|
||||
if *req.SKU != currentSKU {
|
||||
exists, err := p.productRepo.ExistsBySKU(ctx, existingProduct.OrganizationID, *req.SKU, &id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check SKU uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("product with SKU '%s' already exists for this organization", *req.SKU)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != nil && *req.Name != existingProduct.Name {
|
||||
exists, err := p.productRepo.ExistsByName(ctx, existingProduct.OrganizationID, *req.Name, &id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check product name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("product with name '%s' already exists for this organization", *req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
mappers.UpdateProductEntityFromRequest(existingProduct, req)
|
||||
|
||||
if err := p.productRepo.Update(ctx, existingProduct); err != nil {
|
||||
return nil, fmt.Errorf("failed to update product: %w", err)
|
||||
}
|
||||
|
||||
// Update reorder level for all existing inventory records if provided
|
||||
if req.ReorderLevel != nil {
|
||||
if err := p.updateReorderLevelForAllOutlets(ctx, id, *req.ReorderLevel); err != nil {
|
||||
return nil, fmt.Errorf("failed to update reorder levels: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
productWithCategory, err := p.productRepo.GetWithCategory(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated product: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.ProductEntityToResponse(productWithCategory)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductProcessorImpl) DeleteProduct(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := p.productRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
productWithRelations, err := p.productRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check product relations: %w", err)
|
||||
}
|
||||
|
||||
if len(productWithRelations.Inventory) > 0 {
|
||||
return fmt.Errorf("cannot delete product: it has inventory records associated with it")
|
||||
}
|
||||
|
||||
if len(productWithRelations.OrderItems) > 0 {
|
||||
return fmt.Errorf("cannot delete product: it has order items associated with it")
|
||||
}
|
||||
|
||||
if err := p.productRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete product: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID) (*models.ProductResponse, error) {
|
||||
productEntity, err := p.productRepo.GetWithCategory(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.ProductEntityToResponse(productEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ProductResponse, int, error) {
|
||||
offset := (page - 1) * limit
|
||||
|
||||
productEntities, total, err := p.productRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to list products: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.ProductResponse, len(productEntities))
|
||||
for i, entity := range productEntities {
|
||||
response := mappers.ProductEntityToResponse(entity)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, int(total), nil
|
||||
}
|
||||
|
||||
// Helper methods for inventory management
|
||||
|
||||
// createInventoryForAllOutlets creates inventory records for all outlets of an organization
|
||||
func (p *ProductProcessorImpl) createInventoryForAllOutlets(ctx context.Context, productID, organizationID uuid.UUID, initialStock, reorderLevel *int) error {
|
||||
// Get all outlets for the organization
|
||||
outlets, err := p.outletRepo.GetByOrganizationID(ctx, organizationID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get outlets for organization: %w", err)
|
||||
}
|
||||
|
||||
if len(outlets) == 0 {
|
||||
return fmt.Errorf("no outlets found for organization")
|
||||
}
|
||||
|
||||
// Prepare inventory items for bulk creation
|
||||
var inventoryItems []*entities.Inventory
|
||||
for _, outlet := range outlets {
|
||||
quantity := 0
|
||||
if initialStock != nil {
|
||||
quantity = *initialStock
|
||||
}
|
||||
|
||||
reorderLevelValue := 0
|
||||
if reorderLevel != nil {
|
||||
reorderLevelValue = *reorderLevel
|
||||
}
|
||||
|
||||
inventoryItem := &entities.Inventory{
|
||||
OutletID: outlet.ID,
|
||||
ProductID: productID,
|
||||
Quantity: quantity,
|
||||
ReorderLevel: reorderLevelValue,
|
||||
}
|
||||
inventoryItems = append(inventoryItems, inventoryItem)
|
||||
}
|
||||
|
||||
// Bulk create inventory records
|
||||
if err := p.inventoryRepo.BulkCreate(ctx, inventoryItems); err != nil {
|
||||
return fmt.Errorf("failed to bulk create inventory records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateReorderLevelForAllOutlets updates the reorder level for all inventory records of a product
|
||||
func (p *ProductProcessorImpl) updateReorderLevelForAllOutlets(ctx context.Context, productID uuid.UUID, reorderLevel int) error {
|
||||
// Get all inventory records for the product
|
||||
inventoryRecords, err := p.inventoryRepo.GetByProduct(ctx, productID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get inventory records for product: %w", err)
|
||||
}
|
||||
|
||||
// Update reorder level for each inventory record
|
||||
for _, inventory := range inventoryRecords {
|
||||
inventory.ReorderLevel = reorderLevel
|
||||
if err := p.inventoryRepo.Update(ctx, inventory); err != nil {
|
||||
return fmt.Errorf("failed to update inventory reorder level: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// Mock repositories for testing
|
||||
type MockProductRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockCategoryRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockProductVariantRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockInventoryRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockOutletRepository struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Test helper functions
|
||||
func TestCreateProductWithInventory(t *testing.T) {
|
||||
// This is a basic test structure - in a real implementation,
|
||||
// you would use a proper testing framework with database mocks
|
||||
|
||||
t.Run("should create product with inventory when create_inventory is true", func(t *testing.T) {
|
||||
// Arrange
|
||||
productRepo := &MockProductRepository{}
|
||||
categoryRepo := &MockCategoryRepository{}
|
||||
productVariantRepo := &MockProductVariantRepository{}
|
||||
inventoryRepo := &MockInventoryRepository{}
|
||||
outletRepo := &MockOutletRepository{}
|
||||
|
||||
processor := NewProductProcessorImpl(
|
||||
productRepo,
|
||||
categoryRepo,
|
||||
productVariantRepo,
|
||||
inventoryRepo,
|
||||
outletRepo,
|
||||
)
|
||||
|
||||
req := &models.CreateProductRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
CategoryID: uuid.New(),
|
||||
Name: "Test Product",
|
||||
Price: 10.0,
|
||||
Cost: 5.0,
|
||||
InitialStock: &[]int{100}[0],
|
||||
ReorderLevel: &[]int{20}[0],
|
||||
CreateInventory: true,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
||||
|
||||
// Mock outlets
|
||||
outlets := []*models.Outlet{
|
||||
{ID: uuid.New()},
|
||||
{ID: uuid.New()},
|
||||
}
|
||||
outletRepo.On("GetByOrganizationID", mock.Anything, req.OrganizationID).Return(outlets, nil)
|
||||
|
||||
// Mock inventory creation
|
||||
inventoryRepo.On("BulkCreate", mock.Anything, mock.Anything).Return(nil)
|
||||
|
||||
// Act
|
||||
result, err := processor.CreateProduct(context.Background(), req)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Verify that inventory was created
|
||||
inventoryRepo.AssertCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
||||
outletRepo.AssertCalled(t, "GetByOrganizationID", mock.Anything, req.OrganizationID)
|
||||
})
|
||||
|
||||
t.Run("should not create inventory when create_inventory is false", func(t *testing.T) {
|
||||
// Arrange
|
||||
productRepo := &MockProductRepository{}
|
||||
categoryRepo := &MockCategoryRepository{}
|
||||
productVariantRepo := &MockProductVariantRepository{}
|
||||
inventoryRepo := &MockInventoryRepository{}
|
||||
outletRepo := &MockOutletRepository{}
|
||||
|
||||
processor := NewProductProcessorImpl(
|
||||
productRepo,
|
||||
categoryRepo,
|
||||
productVariantRepo,
|
||||
inventoryRepo,
|
||||
outletRepo,
|
||||
)
|
||||
|
||||
req := &models.CreateProductRequest{
|
||||
OrganizationID: uuid.New(),
|
||||
CategoryID: uuid.New(),
|
||||
Name: "Test Product",
|
||||
Price: 10.0,
|
||||
Cost: 5.0,
|
||||
CreateInventory: false,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
categoryRepo.On("GetByID", mock.Anything, req.CategoryID).Return(&models.Category{}, nil)
|
||||
productRepo.On("ExistsBySKU", mock.Anything, req.OrganizationID, mock.Anything, mock.Anything).Return(false, nil)
|
||||
productRepo.On("ExistsByName", mock.Anything, req.OrganizationID, req.Name, mock.Anything).Return(false, nil)
|
||||
productRepo.On("Create", mock.Anything, mock.Anything).Return(nil)
|
||||
productRepo.On("GetWithCategory", mock.Anything, mock.Anything).Return(&models.Product{}, nil)
|
||||
|
||||
// Act
|
||||
result, err := processor.CreateProduct(context.Background(), req)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Verify that inventory was NOT created
|
||||
inventoryRepo.AssertNotCalled(t, "BulkCreate", mock.Anything, mock.Anything)
|
||||
outletRepo.AssertNotCalled(t, "GetByOrganizationID", mock.Anything, mock.Anything)
|
||||
})
|
||||
}
|
||||
|
||||
// Mock implementations (simplified for testing)
|
||||
func (m *MockProductRepository) Create(ctx context.Context, product *models.Product) error {
|
||||
args := m.Called(ctx, product)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Product), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) GetWithCategory(ctx context.Context, id uuid.UUID) (*models.Product, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Product), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) ExistsBySKU(ctx context.Context, organizationID uuid.UUID, sku string, excludeID *uuid.UUID) (bool, error) {
|
||||
args := m.Called(ctx, organizationID, sku, excludeID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockProductRepository) ExistsByName(ctx context.Context, organizationID uuid.UUID, name string, excludeID *uuid.UUID) (bool, error) {
|
||||
args := m.Called(ctx, organizationID, name, excludeID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockCategoryRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.Category, error) {
|
||||
args := m.Called(ctx, id)
|
||||
return args.Get(0).(*models.Category), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockInventoryRepository) BulkCreate(ctx context.Context, inventoryItems []*models.Inventory) error {
|
||||
args := m.Called(ctx, inventoryItems)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *MockOutletRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*models.Outlet, error) {
|
||||
args := m.Called(ctx, organizationID)
|
||||
return args.Get(0).([]*models.Outlet), args.Error(1)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProductVariantProcessor interface {
|
||||
CreateProductVariant(ctx context.Context, req *models.CreateProductVariantRequest) (*models.ProductVariantResponse, error)
|
||||
UpdateProductVariant(ctx context.Context, id uuid.UUID, req *models.UpdateProductVariantRequest) (*models.ProductVariantResponse, error)
|
||||
DeleteProductVariant(ctx context.Context, id uuid.UUID) error
|
||||
GetProductVariantByID(ctx context.Context, id uuid.UUID) (*models.ProductVariantResponse, error)
|
||||
GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) ([]models.ProductVariantResponse, error)
|
||||
}
|
||||
|
||||
type ProductVariantProcessorImpl struct {
|
||||
productVariantRepo repository.ProductVariantRepository
|
||||
productRepo ProductRepository
|
||||
}
|
||||
|
||||
func NewProductVariantProcessorImpl(
|
||||
productVariantRepo repository.ProductVariantRepository,
|
||||
productRepo ProductRepository,
|
||||
) *ProductVariantProcessorImpl {
|
||||
return &ProductVariantProcessorImpl{
|
||||
productVariantRepo: productVariantRepo,
|
||||
productRepo: productRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProductVariantProcessorImpl) CreateProductVariant(ctx context.Context, req *models.CreateProductVariantRequest) (*models.ProductVariantResponse, error) {
|
||||
// Validate product exists
|
||||
_, err := p.productRepo.GetByID(ctx, req.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid product: %w", err)
|
||||
}
|
||||
|
||||
// Check name uniqueness within the same product
|
||||
exists, err := p.productVariantRepo.ExistsByName(ctx, req.ProductID, req.Name, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("variant with name '%s' already exists for this product", req.Name)
|
||||
}
|
||||
|
||||
// Map request to entity
|
||||
variantEntity := mappers.CreateProductVariantRequestToEntity(req)
|
||||
|
||||
// Create variant
|
||||
if err := p.productVariantRepo.Create(ctx, variantEntity); err != nil {
|
||||
return nil, fmt.Errorf("failed to create product variant: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.ProductVariantEntityToResponse(variantEntity)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductVariantProcessorImpl) UpdateProductVariant(ctx context.Context, id uuid.UUID, req *models.UpdateProductVariantRequest) (*models.ProductVariantResponse, error) {
|
||||
// Get existing variant
|
||||
existingVariant, err := p.productVariantRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
// Check name uniqueness if being updated
|
||||
if req.Name != nil && *req.Name != existingVariant.Name {
|
||||
exists, err := p.productVariantRepo.ExistsByName(ctx, existingVariant.ProductID, *req.Name, &id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check variant name uniqueness: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return nil, fmt.Errorf("variant with name '%s' already exists for this product", *req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply updates to entity
|
||||
mappers.UpdateProductVariantEntityFromRequest(existingVariant, req)
|
||||
|
||||
// Update variant
|
||||
if err := p.productVariantRepo.Update(ctx, existingVariant); err != nil {
|
||||
return nil, fmt.Errorf("failed to update product variant: %w", err)
|
||||
}
|
||||
|
||||
// Map entity to response model
|
||||
response := mappers.ProductVariantEntityToResponse(existingVariant)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductVariantProcessorImpl) DeleteProductVariant(ctx context.Context, id uuid.UUID) error {
|
||||
// Check if variant exists
|
||||
_, err := p.productVariantRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
// TODO: Check if variant is used in any order items before deletion
|
||||
// This would require checking the order_items table
|
||||
|
||||
// Delete variant
|
||||
if err := p.productVariantRepo.Delete(ctx, id); err != nil {
|
||||
return fmt.Errorf("failed to delete product variant: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProductVariantProcessorImpl) GetProductVariantByID(ctx context.Context, id uuid.UUID) (*models.ProductVariantResponse, error) {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.ProductVariantEntityToResponse(variant)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *ProductVariantProcessorImpl) GetProductVariantsByProductID(ctx context.Context, productID uuid.UUID) ([]models.ProductVariantResponse, error) {
|
||||
variants, err := p.productVariantRepo.GetByProductID(ctx, productID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get product variants: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.ProductVariantResponse, len(variants))
|
||||
for i, variant := range variants {
|
||||
response := mappers.ProductVariantEntityToResponse(variant)
|
||||
responses[i] = *response
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserProcessorImpl struct {
|
||||
userRepo UserRepository
|
||||
organizationRepo OrganizationRepository
|
||||
outletRepo OutletRepository
|
||||
}
|
||||
|
||||
func NewUserProcessor(
|
||||
userRepo UserRepository,
|
||||
organizationRepo OrganizationRepository,
|
||||
outletRepo OutletRepository,
|
||||
) *UserProcessorImpl {
|
||||
return &UserProcessorImpl{
|
||||
userRepo: userRepo,
|
||||
organizationRepo: organizationRepo,
|
||||
outletRepo: outletRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) CreateUser(ctx context.Context, req *models.CreateUserRequest) (*models.UserResponse, error) {
|
||||
_, err := p.organizationRepo.GetByID(ctx, req.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
if req.OutletID != nil {
|
||||
_, err := p.outletRepo.GetByID(ctx, *req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
existingUser, err := p.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, fmt.Errorf("user with email %s already exists", req.Email)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
userEntity := mappers.UserCreateRequestToEntity(req, string(passwordHash))
|
||||
|
||||
err = p.userRepo.Create(ctx, userEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
return mappers.UserEntityToResponse(userEntity), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) UpdateUser(ctx context.Context, id uuid.UUID, req *models.UpdateUserRequest) (*models.UserResponse, error) {
|
||||
existingUser, err := p.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
if req.Email != nil && *req.Email != existingUser.Email {
|
||||
existingUserByEmail, err := p.userRepo.GetByEmail(ctx, *req.Email)
|
||||
if err == nil && existingUserByEmail != nil && existingUserByEmail.ID != id {
|
||||
return nil, fmt.Errorf("user with email %s already exists", *req.Email)
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
existingUser.Name = *req.Name
|
||||
}
|
||||
if req.Email != nil {
|
||||
existingUser.Email = *req.Email
|
||||
}
|
||||
if req.Role != nil {
|
||||
existingUser.Role = entities.UserRole(*req.Role)
|
||||
}
|
||||
if req.OutletID != nil {
|
||||
existingUser.OutletID = req.OutletID
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
existingUser.IsActive = *req.IsActive
|
||||
}
|
||||
if req.Permissions != nil {
|
||||
existingUser.Permissions = entities.Permissions(*req.Permissions)
|
||||
}
|
||||
|
||||
err = p.userRepo.Update(ctx, existingUser)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
return mappers.UserEntityToResponse(existingUser), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := p.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.userRepo.Delete(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserByID(ctx context.Context, id uuid.UUID) (*models.UserResponse, error) {
|
||||
user, err := p.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.UserEntityToResponse(user), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserByEmail(ctx context.Context, email string) (*models.UserResponse, error) {
|
||||
user, err := p.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return mappers.UserEntityToResponse(user), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) ListUsers(ctx context.Context, organizationID uuid.UUID, page, limit int) ([]models.UserResponse, int, error) {
|
||||
_, err := p.organizationRepo.GetByID(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("organization not found: %w", err)
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
filters := map[string]interface{}{
|
||||
"organization_id": organizationID,
|
||||
}
|
||||
|
||||
users, totalCount, err := p.userRepo.List(ctx, filters, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
responses := make([]models.UserResponse, len(users))
|
||||
for i, user := range users {
|
||||
response := mappers.UserEntityToResponse(user)
|
||||
if response != nil {
|
||||
responses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
return responses, int(totalCount), nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) GetUserEntityByEmail(ctx context.Context, email string) (*entities.User, error) {
|
||||
user, err := p.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) ChangePassword(ctx context.Context, userID uuid.UUID, req *models.ChangePasswordRequest) error {
|
||||
user, err := p.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.CurrentPassword))
|
||||
if err != nil {
|
||||
return fmt.Errorf("current password is incorrect")
|
||||
}
|
||||
|
||||
newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash new password: %w", err)
|
||||
}
|
||||
|
||||
err = p.userRepo.UpdatePassword(ctx, userID, string(newPasswordHash))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) ActivateUser(ctx context.Context, userID uuid.UUID) error {
|
||||
_, err := p.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.userRepo.UpdateActiveStatus(ctx, userID, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to activate user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *UserProcessorImpl) DeactivateUser(ctx context.Context, userID uuid.UUID) error {
|
||||
_, err := p.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found: %w", err)
|
||||
}
|
||||
|
||||
err = p.userRepo.UpdateActiveStatus(ctx, userID, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to deactivate user: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"apskel-pos-be/internal/entities"
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(ctx context.Context, user *entities.User) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*entities.User, error)
|
||||
GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||
GetByRole(ctx context.Context, role entities.UserRole) ([]*entities.User, error)
|
||||
GetActiveUsers(ctx context.Context, organizationID uuid.UUID) ([]*entities.User, error)
|
||||
Update(ctx context.Context, user *entities.User) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
UpdatePassword(ctx context.Context, id uuid.UUID, passwordHash string) error
|
||||
UpdateActiveStatus(ctx context.Context, id uuid.UUID, isActive bool) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.User, int64, error)
|
||||
Count(ctx context.Context, filters map[string]interface{}) (int64, error)
|
||||
}
|
||||
Reference in New Issue
Block a user