This commit is contained in:
aditya.siregar
2025-07-18 20:10:29 +07:00
parent 1bceae010b
commit 4f5950543e
511 changed files with 24132 additions and 34137 deletions
+347
View File
@@ -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
}
+152
View File
@@ -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
}
+195
View File
@@ -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
}
+245
View File
@@ -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
}
+884
View File
@@ -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)
}
+149
View File
@@ -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
}
+20
View File
@@ -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
}
+307
View File
@@ -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
}
+226
View File
@@ -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
}
+22
View File
@@ -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)
}