Compare commits

..
Author SHA1 Message Date
Efril 84222fc7f4 update 2026-05-28 15:30:18 +07:00
Efril 23ac572e3f add print_to_checker at product outlet 2026-05-28 13:49:57 +07:00
Efril 957c1ae53d update order response 2026-05-25 20:28:24 +07:00
40 changed files with 254 additions and 1476 deletions
-10
View File
@@ -135,8 +135,6 @@ func (a *App) Initialize(cfg *config.Config) error {
services.productOutletPriceService,
validators.productOutletPriceValidator,
selfOrderHandler,
services.expenseService,
validators.expenseValidator,
)
return nil
@@ -238,7 +236,6 @@ type repositories struct {
notificationReceiverRepo *repository.NotificationReceiverRepositoryImpl
notificationDeliveryRepo *repository.NotificationDeliveryRepositoryImpl
productOutletPriceRepo *repository.ProductOutletPriceRepositoryImpl
expenseRepo *repository.ExpenseRepositoryImpl
}
func (a *App) initRepositories() *repositories {
@@ -291,7 +288,6 @@ func (a *App) initRepositories() *repositories {
notificationReceiverRepo: repository.NewNotificationReceiverRepository(a.db),
notificationDeliveryRepo: repository.NewNotificationDeliveryRepository(a.db),
productOutletPriceRepo: repository.NewProductOutletPriceRepositoryImpl(a.db),
expenseRepo: repository.NewExpenseRepositoryImpl(a.db),
}
}
@@ -337,7 +333,6 @@ type processors struct {
userDeviceProcessor *processor.UserDeviceProcessorImpl
notificationProcessor *processor.NotificationProcessorImpl
productOutletPriceProcessor processor.ProductOutletPriceProcessor
expenseProcessor *processor.ExpenseProcessorImpl
}
func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processors {
@@ -388,7 +383,6 @@ func (a *App) initProcessors(cfg *config.Config, repos *repositories) *processor
userDeviceProcessor: processor.NewUserDeviceProcessorImpl(repos.userDeviceRepo),
notificationProcessor: buildNotificationProcessor(cfg, repos),
productOutletPriceProcessor: processor.NewProductOutletPriceProcessorImpl(repos.productOutletPriceRepo, repos.productRepo, repos.outletRepo),
expenseProcessor: processor.NewExpenseProcessorImpl(repos.expenseRepo),
}
}
@@ -428,7 +422,6 @@ type services struct {
userDeviceService service.UserDeviceService
notificationService service.NotificationService
productOutletPriceService service.ProductOutletPriceService
expenseService *service.ExpenseServiceImpl
}
func (a *App) initServices(processors *processors, repos *repositories, cfg *config.Config) *services {
@@ -506,7 +499,6 @@ func (a *App) initServices(processors *processors, repos *repositories, cfg *con
userDeviceService: userDeviceService,
notificationService: notificationService,
productOutletPriceService: service.NewProductOutletPriceService(processors.productOutletPriceProcessor),
expenseService: service.NewExpenseService(processors.expenseProcessor),
}
}
@@ -549,7 +541,6 @@ type validators struct {
userDeviceValidator *validator.UserDeviceValidatorImpl
notificationValidator *validator.NotificationValidatorImpl
productOutletPriceValidator *validator.ProductOutletPriceValidatorImpl
expenseValidator *validator.ExpenseValidatorImpl
}
func (a *App) initValidators() *validators {
@@ -580,7 +571,6 @@ func (a *App) initValidators() *validators {
userDeviceValidator: validator.NewUserDeviceValidator(),
notificationValidator: validator.NewNotificationValidator(),
productOutletPriceValidator: validator.NewProductOutletPriceValidator(),
expenseValidator: validator.NewExpenseValidator(),
}
}
-1
View File
@@ -60,7 +60,6 @@ const (
NotificationServiceEntity = "notification_service"
NotificationHandlerEntity = "notification_handler"
ProductOutletPriceServiceEntity = "product_outlet_price_service"
ExpenseServiceEntity = "expense_service"
)
var HttpErrorMap = map[string]int{
-83
View File
@@ -1,83 +0,0 @@
package contract
import (
"time"
"github.com/google/uuid"
)
type CreateExpenseRequest struct {
Receiver string `json:"receiver" validate:"required"`
TransactionDate string `json:"transaction_date" validate:"required"`
CodeNumber string `json:"code_number" validate:"required"`
OutletID string `json:"outlet_id" validate:"required"`
Description *string `json:"description,omitempty"`
Tax float64 `json:"tax"`
Total float64 `json:"total" validate:"required"`
Items []CreateExpenseItemRequest `json:"items" validate:"required"`
}
type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id" validate:"required"`
Description *string `json:"description,omitempty"`
Amount float64 `json:"amount" validate:"required"`
}
type UpdateExpenseRequest struct {
Receiver *string `json:"receiver,omitempty"`
TransactionDate *string `json:"transaction_date,omitempty"`
CodeNumber *string `json:"code_number,omitempty"`
OutletID *string `json:"outlet_id,omitempty"`
Description *string `json:"description,omitempty"`
Tax *float64 `json:"tax,omitempty"`
Total *float64 `json:"total,omitempty"`
Reserved1 *string `json:"reserved1,omitempty"`
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
}
type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
Description *string `json:"description,omitempty"`
Amount *float64 `json:"amount,omitempty"`
}
type ExpenseResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id"`
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Reserved1 *string `json:"reserved1,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Items []ExpenseItemResponse `json:"items,omitempty"`
}
type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ListExpenseRequest struct {
Page int `json:"page" validate:"min=1"`
Limit int `json:"limit" validate:"min=1,max=100"`
Search string `json:"search,omitempty"`
}
type ListExpenseResponse struct {
Expenses []ExpenseResponse `json:"expenses"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
+3
View File
@@ -98,6 +98,8 @@ type OrderItemResponse struct {
ProductName string `json:"product_name"`
ProductVariantID *uuid.UUID `json:"product_variant_id"`
ProductVariantName *string `json:"product_variant_name,omitempty"`
CategoryID *uuid.UUID `json:"category_id,omitempty"`
CategoryName *string `json:"category_name,omitempty"`
Quantity int `json:"quantity"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
@@ -108,6 +110,7 @@ type OrderItemResponse struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PrinterType string `json:"printer_type"`
PrintToChecker bool `json:"print_to_checker"`
PaidQuantity int `json:"paid_quantity"`
}
+3
View File
@@ -17,6 +17,7 @@ type CreateProductRequest struct {
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
Variants []CreateProductVariantRequest `json:"variants,omitempty"`
@@ -36,6 +37,7 @@ type UpdateProductRequest struct {
BusinessType *string `json:"business_type,omitempty"`
ImageURL *string `json:"image_url,omitempty" validate:"omitempty,max=500"`
PrinterType *string `json:"printer_type,omitempty" validate:"omitempty,max=50"`
PrintToChecker *bool `json:"print_to_checker,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
IsActive *bool `json:"is_active,omitempty"`
ReorderLevel *int `json:"reorder_level,omitempty" validate:"omitempty,min=0"`
@@ -71,6 +73,7 @@ type ProductResponse struct {
BusinessType string `json:"business_type"`
ImageURL *string `json:"image_url"`
PrinterType string `json:"printer_type"`
PrintToChecker bool `json:"print_to_checker"`
Metadata map[string]interface{} `json:"metadata"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
@@ -10,10 +10,12 @@ type CreateProductOutletPriceRequest struct {
ProductID uuid.UUID `json:"product_id" validate:"required"`
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker bool `json:"print_to_checker"`
}
type UpdateProductOutletPriceRequest struct {
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker *bool `json:"print_to_checker"`
}
type ProductOutletPriceResponse struct {
@@ -22,6 +24,7 @@ type ProductOutletPriceResponse struct {
OutletID uuid.UUID `json:"outlet_id"`
OutletName string `json:"outlet_name,omitempty"`
Price float64 `json:"price"`
PrintToChecker bool `json:"print_to_checker"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
@@ -39,4 +42,5 @@ type BulkCreateProductOutletPriceRequest struct {
type CreateProductOutletPricePerOutletRequest struct {
OutletID uuid.UUID `json:"outlet_id" validate:"required"`
Price float64 `json:"price" validate:"required,min=0"`
PrintToChecker bool `json:"print_to_checker"`
}
-1
View File
@@ -42,7 +42,6 @@ func GetAllEntities() []interface{} {
&NotificationReceiver{},
&NotificationDelivery{},
&ProductOutletPrice{},
&Expense{},
}
}
-39
View File
@@ -1,39 +0,0 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Expense struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
OrganizationID uuid.UUID `gorm:"type:uuid;not null;index" json:"organization_id"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
Receiver string `gorm:"not null;size:255" json:"receiver"`
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
Description *string `gorm:"type:text" json:"description"`
Tax float64 `gorm:"type:decimal(15,2);not null;default:0" json:"tax"`
Total float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total"`
Reserved1 *string `gorm:"type:text" json:"reserved1"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Organization *Organization `gorm:"foreignKey:OrganizationID" json:"organization,omitempty"`
Outlet *Outlet `gorm:"foreignKey:OutletID" json:"outlet,omitempty"`
Items []ExpenseItem `gorm:"foreignKey:ExpenseID" json:"items,omitempty"`
}
func (e *Expense) BeforeCreate(tx *gorm.DB) error {
if e.ID == uuid.Nil {
e.ID = uuid.New()
}
return nil
}
func (Expense) TableName() string {
return "expenses"
}
-33
View File
@@ -1,33 +0,0 @@
package entities
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ExpenseItem struct {
ID uuid.UUID `gorm:"type:uuid;primary_key;default:gen_random_uuid()" json:"id"`
ExpenseID uuid.UUID `gorm:"type:uuid;not null;index" json:"expense_id"`
ChartOfAccountID uuid.UUID `gorm:"type:uuid;not null;index" json:"chart_of_account_id"`
Description *string `gorm:"type:text" json:"description"`
Amount float64 `gorm:"type:decimal(15,2);not null;default:0" json:"amount"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
Expense *Expense `gorm:"foreignKey:ExpenseID" json:"expense,omitempty"`
ChartOfAccount *ChartOfAccount `gorm:"foreignKey:ChartOfAccountID" json:"chart_of_account,omitempty"`
}
func (e *ExpenseItem) BeforeCreate(tx *gorm.DB) error {
if e.ID == uuid.Nil {
e.ID = uuid.New()
}
return nil
}
func (ExpenseItem) TableName() string {
return "expense_items"
}
+1
View File
@@ -33,6 +33,7 @@ type Product struct {
ProductRecipes []ProductRecipe `gorm:"foreignKey:ProductID" json:"product_recipes,omitempty"`
Inventory []Inventory `gorm:"foreignKey:ProductID" json:"inventory,omitempty"`
OrderItems []OrderItem `gorm:"foreignKey:ProductID" json:"order_items,omitempty"`
ProductOutletPrices []ProductOutletPrice `gorm:"foreignKey:ProductID" json:"product_outlet_prices,omitempty"`
}
func (p *Product) BeforeCreate(tx *gorm.DB) error {
@@ -12,6 +12,7 @@ type ProductOutletPrice struct {
ProductID uuid.UUID `gorm:"type:uuid;not null;index" json:"product_id"`
OutletID uuid.UUID `gorm:"type:uuid;not null;index" json:"outlet_id"`
Price float64 `gorm:"type:decimal(10,2);not null" json:"price"`
PrintToChecker bool `gorm:"not null;default:true" json:"print_to_checker"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
-181
View File
@@ -1,181 +0,0 @@
package handler
import (
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/util"
"strconv"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/service"
"apskel-pos-be/internal/validator"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type ExpenseHandler struct {
expenseService service.ExpenseService
expenseValidator validator.ExpenseValidator
}
func NewExpenseHandler(
expenseService service.ExpenseService,
expenseValidator validator.ExpenseValidator,
) *ExpenseHandler {
return &ExpenseHandler{
expenseService: expenseService,
expenseValidator: expenseValidator,
}
}
func (h *ExpenseHandler) CreateExpense(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
var req contract.CreateExpenseRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::CreateExpense -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
return
}
validationError, validationErrorCode := h.expenseValidator.ValidateCreateExpenseRequest(&req)
if validationError != nil {
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::CreateExpense")
return
}
expenseResponse := h.expenseService.CreateExpense(ctx, contextInfo, &req)
if expenseResponse.HasErrors() {
errorResp := expenseResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::CreateExpense -> Failed to create expense from service")
}
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::CreateExpense")
}
func (h *ExpenseHandler) UpdateExpense(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
expenseIDStr := c.Param("id")
expenseID, err := uuid.Parse(expenseIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> Invalid expense ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
return
}
var req contract.UpdateExpenseRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::UpdateExpense -> request binding failed")
validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
return
}
validationError, validationErrorCode := h.expenseValidator.ValidateUpdateExpenseRequest(&req)
if validationError != nil {
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::UpdateExpense")
return
}
expenseResponse := h.expenseService.UpdateExpense(ctx, contextInfo, expenseID, &req)
if expenseResponse.HasErrors() {
errorResp := expenseResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::UpdateExpense -> Failed to update expense from service")
}
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::UpdateExpense")
}
func (h *ExpenseHandler) DeleteExpense(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
expenseIDStr := c.Param("id")
expenseID, err := uuid.Parse(expenseIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::DeleteExpense -> Invalid expense ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::DeleteExpense")
return
}
expenseResponse := h.expenseService.DeleteExpense(ctx, contextInfo, expenseID)
if expenseResponse.HasErrors() {
errorResp := expenseResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::DeleteExpense -> Failed to delete expense from service")
}
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::DeleteExpense")
}
func (h *ExpenseHandler) GetExpense(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
expenseIDStr := c.Param("id")
expenseID, err := uuid.Parse(expenseIDStr)
if err != nil {
logger.FromContext(ctx).WithError(err).Error("ExpenseHandler::GetExpense -> Invalid expense ID")
validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid expense ID")
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::GetExpense")
return
}
expenseResponse := h.expenseService.GetExpenseByID(ctx, contextInfo, expenseID)
if expenseResponse.HasErrors() {
errorResp := expenseResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::GetExpense -> Failed to get expense from service")
}
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::GetExpense")
}
func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
ctx := c.Request.Context()
contextInfo := appcontext.FromGinContext(ctx)
req := &contract.ListExpenseRequest{
Page: 1,
Limit: 10,
}
if pageStr := c.Query("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil {
req.Page = page
}
}
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil {
req.Limit = limit
}
}
if search := c.Query("search"); search != "" {
req.Search = search
}
validationError, validationErrorCode := h.expenseValidator.ValidateListExpenseRequest(req)
if validationError != nil {
validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error())
util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "ExpenseHandler::ListExpenses")
return
}
expenseResponse := h.expenseService.ListExpenses(ctx, contextInfo, req)
if expenseResponse.HasErrors() {
errorResp := expenseResponse.GetErrors()[0]
logger.FromContext(ctx).WithError(errorResp).Error("ExpenseHandler::ListExpenses -> Failed to list expenses from service")
}
util.HandleResponse(c.Writer, c.Request, expenseResponse, "ExpenseHandler::ListExpenses")
}
-124
View File
@@ -1,124 +0,0 @@
package mappers
import (
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
)
func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
if entity == nil {
return nil
}
return &models.Expense{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
Receiver: entity.Receiver,
TransactionDate: entity.TransactionDate,
CodeNumber: entity.CodeNumber,
Description: entity.Description,
Tax: entity.Tax,
Total: entity.Total,
Reserved1: entity.Reserved1,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
}
func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
if model == nil {
return nil
}
return &entities.Expense{
ID: model.ID,
OrganizationID: model.OrganizationID,
OutletID: model.OutletID,
Receiver: model.Receiver,
TransactionDate: model.TransactionDate,
CodeNumber: model.CodeNumber,
Description: model.Description,
Tax: model.Tax,
Total: model.Total,
Reserved1: model.Reserved1,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
}
func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
if entity == nil {
return nil
}
resp := &models.ExpenseResponse{
ID: entity.ID,
OrganizationID: entity.OrganizationID,
OutletID: entity.OutletID,
Receiver: entity.Receiver,
TransactionDate: entity.TransactionDate,
CodeNumber: entity.CodeNumber,
Description: entity.Description,
Tax: entity.Tax,
Total: entity.Total,
Reserved1: entity.Reserved1,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
if entity.Items != nil {
resp.Items = ExpenseItemEntitiesToResponses(entity.Items)
}
return resp
}
func ExpenseEntitiesToResponses(entities []*entities.Expense) []*models.ExpenseResponse {
if entities == nil {
return nil
}
responses := make([]*models.ExpenseResponse, len(entities))
for i, entity := range entities {
responses[i] = ExpenseEntityToResponse(entity)
}
return responses
}
func ExpenseItemEntityToResponse(entity *entities.ExpenseItem) *models.ExpenseItemResponse {
if entity == nil {
return nil
}
response := &models.ExpenseItemResponse{
ID: entity.ID,
ExpenseID: entity.ExpenseID,
ChartOfAccountID: entity.ChartOfAccountID,
Description: entity.Description,
Amount: entity.Amount,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
if entity.ChartOfAccount != nil {
response.ChartOfAccountName = entity.ChartOfAccount.Name
}
return response
}
func ExpenseItemEntitiesToResponses(entities []entities.ExpenseItem) []models.ExpenseItemResponse {
if entities == nil {
return nil
}
responses := make([]models.ExpenseItemResponse, len(entities))
for i, entity := range entities {
response := ExpenseItemEntityToResponse(&entity)
if response != nil {
responses[i] = *response
}
}
return responses
}
+22 -4
View File
@@ -82,7 +82,7 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
}
for i, item := range order.OrderItems {
resp := OrderItemEntityToResponse(&item)
resp := OrderItemEntityToResponse(&item, order.OutletID)
if resp != nil {
resp.PaidQuantity = paidQtyByOrderItem[item.ID]
response.OrderItems[i] = *resp
@@ -101,11 +101,20 @@ func OrderEntityToResponse(order *entities.Order) *models.OrderResponse {
return response
}
func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemResponse {
func OrderItemEntityToResponse(item *entities.OrderItem, outletID uuid.UUID) *models.OrderItemResponse {
if item == nil {
return nil
}
// Resolve print_to_checker from preloaded outlet prices
printToChecker := true // default
for _, op := range item.Product.ProductOutletPrices {
if op.OutletID == outletID {
printToChecker = op.PrintToChecker
break
}
}
response := &models.OrderItemResponse{
ID: item.ID,
OrderID: item.OrderID,
@@ -130,10 +139,19 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
PrinterType: item.Product.PrinterType,
PrintToChecker: printToChecker,
}
if item.Product.ID != uuid.Nil {
response.ProductName = item.Product.Name
if item.Product.CategoryID != uuid.Nil {
categoryID := item.Product.CategoryID
response.CategoryID = &categoryID
}
if item.Product.Category.ID != uuid.Nil {
categoryName := item.Product.Category.Name
response.CategoryName = &categoryName
}
}
if item.ProductVariant != nil {
@@ -316,14 +334,14 @@ func OrderEntitiesToResponses(orders []*entities.Order) []models.OrderResponse {
return responses
}
func OrderItemEntitiesToResponses(items []*entities.OrderItem) []models.OrderItemResponse {
func OrderItemEntitiesToResponses(items []*entities.OrderItem, outletID uuid.UUID) []models.OrderItemResponse {
if items == nil {
return nil
}
responses := make([]models.OrderItemResponse, len(items))
for i, item := range items {
response := OrderItemEntityToResponse(item)
response := OrderItemEntityToResponse(item, outletID)
if response != nil {
responses[i] = *response
}
+3 -3
View File
@@ -45,7 +45,7 @@ func TestOrderItemEntityToResponse_WithProductNames(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
@@ -89,7 +89,7 @@ func TestOrderItemEntityToResponse_WithoutProductVariant(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
@@ -129,7 +129,7 @@ func TestOrderItemEntityToResponse_WithoutProductPreload(t *testing.T) {
}
// Act
result := OrderItemEntityToResponse(orderItem)
result := OrderItemEntityToResponse(orderItem, uuid.Nil)
// Assert
assert.NotNil(t, result)
@@ -15,6 +15,7 @@ func ProductOutletPriceEntityToModel(entity *entities.ProductOutletPrice) *model
ProductID: entity.ProductID,
OutletID: entity.OutletID,
Price: entity.Price,
PrintToChecker: entity.PrintToChecker,
CreatedAt: entity.CreatedAt,
UpdatedAt: entity.UpdatedAt,
}
@@ -30,6 +31,7 @@ func ProductOutletPriceModelToEntity(model *models.ProductOutletPrice) *entities
ProductID: model.ProductID,
OutletID: model.OutletID,
Price: model.Price,
PrintToChecker: model.PrintToChecker,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}
-108
View File
@@ -1,108 +0,0 @@
package models
import (
"time"
"github.com/google/uuid"
)
type Expense struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id"`
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Reserved1 *string `json:"reserved1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ExpenseItem struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ExpenseResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
OutletID uuid.UUID `json:"outlet_id"`
Receiver string `json:"receiver"`
TransactionDate time.Time `json:"transaction_date"`
CodeNumber string `json:"code_number"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Reserved1 *string `json:"reserved1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Items []ExpenseItemResponse `json:"items,omitempty"`
}
type ExpenseItemResponse struct {
ID uuid.UUID `json:"id"`
ExpenseID uuid.UUID `json:"expense_id"`
ChartOfAccountID uuid.UUID `json:"chart_of_account_id"`
ChartOfAccountName string `json:"chart_of_account_name,omitempty"`
Description *string `json:"description"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateExpenseRequest struct {
Receiver string `json:"receiver"`
TransactionDate string `json:"transaction_date"`
CodeNumber string `json:"code_number"`
OutletID string `json:"outlet_id"`
Description *string `json:"description"`
Tax float64 `json:"tax"`
Total float64 `json:"total"`
Items []CreateExpenseItemRequest `json:"items"`
}
type CreateExpenseItemRequest struct {
ChartOfAccountID string `json:"chart_of_account_id"`
Description *string `json:"description,omitempty"`
Amount float64 `json:"amount"`
}
type UpdateExpenseRequest struct {
Receiver *string `json:"receiver,omitempty"`
TransactionDate *string `json:"transaction_date,omitempty"`
CodeNumber *string `json:"code_number,omitempty"`
OutletID *string `json:"outlet_id,omitempty"`
Description *string `json:"description,omitempty"`
Tax *float64 `json:"tax,omitempty"`
Total *float64 `json:"total,omitempty"`
Reserved1 *string `json:"reserved1,omitempty"`
Items []UpdateExpenseItemRequest `json:"items,omitempty"`
}
type UpdateExpenseItemRequest struct {
ChartOfAccountID *string `json:"chart_of_account_id,omitempty"`
Description *string `json:"description,omitempty"`
Amount *float64 `json:"amount,omitempty"`
}
type ListExpenseRequest struct {
Page int `json:"page"`
Limit int `json:"limit"`
Search string `json:"search,omitempty"`
}
type ListExpenseResponse struct {
Expenses []*ExpenseResponse `json:"expenses"`
TotalCount int `json:"total_count"`
Page int `json:"page"`
Limit int `json:"limit"`
TotalPages int `json:"total_pages"`
}
+3
View File
@@ -188,6 +188,8 @@ type OrderItemResponse struct {
ProductName string
ProductVariantID *uuid.UUID
ProductVariantName *string
CategoryID *uuid.UUID
CategoryName *string
Quantity int
UnitPrice float64
TotalPrice float64
@@ -207,6 +209,7 @@ type OrderItemResponse struct {
CreatedAt time.Time
UpdatedAt time.Time
PrinterType string
PrintToChecker bool
PaidQuantity int
}
+4
View File
@@ -50,6 +50,7 @@ type CreateProductRequest struct {
BusinessType constants.BusinessType `validate:"required"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
PrintToChecker *bool `validate:"omitempty"`
UnitID *uuid.UUID `validate:"omitempty"`
HasIngredients bool `validate:"omitempty"`
Metadata map[string]interface{}
@@ -70,6 +71,7 @@ type UpdateProductRequest struct {
Cost *float64 `validate:"omitempty,min=0"`
ImageURL *string `validate:"omitempty,max=500"`
PrinterType *string `validate:"omitempty,max=50"`
PrintToChecker *bool `validate:"omitempty"`
UnitID *uuid.UUID `validate:"omitempty"`
HasIngredients *bool `validate:"omitempty"`
Metadata map[string]interface{}
@@ -108,6 +110,7 @@ type ProductResponse struct {
BusinessType constants.BusinessType
ImageURL *string
PrinterType string
PrintToChecker bool
UnitID *uuid.UUID
HasIngredients bool
Metadata map[string]interface{}
@@ -121,6 +124,7 @@ type OutletPrice struct {
OutletID uuid.UUID
OutletName string
Price float64
PrintToChecker bool
}
type ProductVariantResponse struct {
+3
View File
@@ -11,6 +11,7 @@ type ProductOutletPrice struct {
ProductID uuid.UUID
OutletID uuid.UUID
Price float64
PrintToChecker bool
CreatedAt time.Time
UpdatedAt time.Time
}
@@ -19,10 +20,12 @@ type CreateProductOutletPriceRequest struct {
ProductID uuid.UUID `validate:"required"`
OutletID uuid.UUID `validate:"required"`
Price float64 `validate:"required,min=0"`
PrintToChecker bool
}
type UpdateProductOutletPriceRequest struct {
Price *float64 `validate:"required,min=0"`
PrintToChecker *bool
}
type ProductOutletPriceResponse struct {
-207
View File
@@ -1,207 +0,0 @@
package processor
import (
"context"
"fmt"
"time"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type ExpenseProcessor interface {
CreateExpense(ctx context.Context, organizationID uuid.UUID, req *models.CreateExpenseRequest) (*models.ExpenseResponse, error)
UpdateExpense(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateExpenseRequest) (*models.ExpenseResponse, error)
DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error
GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error)
ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error)
}
type ExpenseProcessorImpl struct {
expenseRepo ExpenseRepository
}
func NewExpenseProcessorImpl(expenseRepo ExpenseRepository) *ExpenseProcessorImpl {
return &ExpenseProcessorImpl{
expenseRepo: expenseRepo,
}
}
func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID uuid.UUID, req *models.CreateExpenseRequest) (*models.ExpenseResponse, error) {
outletID, err := uuid.Parse(req.OutletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet_id: %w", err)
}
transactionDate, err := time.Parse("2006-01-02", req.TransactionDate)
if err != nil {
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
}
expenseEntity := &entities.Expense{
OrganizationID: organizationID,
OutletID: outletID,
Receiver: req.Receiver,
TransactionDate: transactionDate,
CodeNumber: req.CodeNumber,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
}
err = p.expenseRepo.Create(ctx, expenseEntity)
if err != nil {
return nil, fmt.Errorf("failed to create expense: %w", err)
}
for _, itemReq := range req.Items {
chartOfAccountID, err := uuid.Parse(itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID,
Description: itemReq.Description,
Amount: itemReq.Amount,
}
err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err)
}
}
created, err := p.expenseRepo.GetByID(ctx, expenseEntity.ID)
if err != nil {
return mappers.ExpenseEntityToResponse(expenseEntity), nil
}
return mappers.ExpenseEntityToResponse(created), nil
}
func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateExpenseRequest) (*models.ExpenseResponse, error) {
expenseEntity, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("expense not found: %w", err)
}
if req.Receiver != nil {
expenseEntity.Receiver = *req.Receiver
}
if req.TransactionDate != nil {
parsedDate, err := time.Parse("2006-01-02", *req.TransactionDate)
if err != nil {
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
}
expenseEntity.TransactionDate = parsedDate
}
if req.CodeNumber != nil {
expenseEntity.CodeNumber = *req.CodeNumber
}
if req.OutletID != nil {
outletID, err := uuid.Parse(*req.OutletID)
if err != nil {
return nil, fmt.Errorf("invalid outlet_id: %w", err)
}
expenseEntity.OutletID = outletID
}
if req.Description != nil {
expenseEntity.Description = req.Description
}
if req.Tax != nil {
expenseEntity.Tax = *req.Tax
}
if req.Total != nil {
expenseEntity.Total = *req.Total
}
if req.Reserved1 != nil {
expenseEntity.Reserved1 = req.Reserved1
}
if req.Items != nil {
err = p.expenseRepo.DeleteItemsByExpenseID(ctx, expenseEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err)
}
for _, itemReq := range req.Items {
chartOfAccountID := uuid.Nil
if itemReq.ChartOfAccountID != nil {
chartOfAccountID, err = uuid.Parse(*itemReq.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("invalid chart_of_account_id for item: %w", err)
}
}
amount := 0.0
if itemReq.Amount != nil {
amount = *itemReq.Amount
}
itemEntity := &entities.ExpenseItem{
ExpenseID: expenseEntity.ID,
ChartOfAccountID: chartOfAccountID,
Description: itemReq.Description,
Amount: amount,
}
err = p.expenseRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create expense item: %w", err)
}
}
}
err = p.expenseRepo.Update(ctx, expenseEntity)
if err != nil {
return nil, fmt.Errorf("failed to update expense: %w", err)
}
updated, err := p.expenseRepo.GetByID(ctx, id)
if err != nil {
return mappers.ExpenseEntityToResponse(expenseEntity), nil
}
return mappers.ExpenseEntityToResponse(updated), nil
}
func (p *ExpenseProcessorImpl) DeleteExpense(ctx context.Context, id, organizationID uuid.UUID) error {
_, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return fmt.Errorf("expense not found: %w", err)
}
err = p.expenseRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete expense: %w", err)
}
return nil
}
func (p *ExpenseProcessorImpl) GetExpenseByID(ctx context.Context, id, organizationID uuid.UUID) (*models.ExpenseResponse, error) {
expenseEntity, err := p.expenseRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("expense not found: %w", err)
}
return mappers.ExpenseEntityToResponse(expenseEntity), nil
}
func (p *ExpenseProcessorImpl) ListExpenses(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.ExpenseResponse, int, error) {
offset := (page - 1) * limit
expenseEntities, total, err := p.expenseRepo.List(ctx, organizationID, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list expenses: %w", err)
}
expenseResponses := mappers.ExpenseEntitiesToResponses(expenseEntities)
totalPages := int((total + int64(limit) - 1) / int64(limit))
return expenseResponses, totalPages, nil
}
-19
View File
@@ -1,19 +0,0 @@
package processor
import (
"apskel-pos-be/internal/entities"
"context"
"github.com/google/uuid"
)
type ExpenseRepository interface {
Create(ctx context.Context, expense *entities.Expense) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.Expense, error)
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Expense, error)
Update(ctx context.Context, expense *entities.Expense) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error)
CreateItem(ctx context.Context, item *entities.ExpenseItem) error
DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error
}
+3 -25
View File
@@ -1,7 +1,6 @@
package processor
import (
"apskel-pos-be/internal/constants"
"context"
"errors"
"fmt"
@@ -388,31 +387,10 @@ func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID,
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,
itemResponse := mappers.OrderItemEntityToResponse(orderItem, order.OutletID)
if itemResponse != nil {
addedItemResponses = append(addedItemResponses, *itemResponse)
}
addedItemResponses = append(addedItemResponses, itemResponse)
}
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
@@ -49,6 +49,7 @@ func (p *ProductOutletPriceProcessorImpl) Upsert(ctx context.Context, req *model
ProductID: req.ProductID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: req.PrintToChecker,
}
if err := p.repo.Upsert(ctx, entity); err != nil {
+59 -3
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/logger"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
@@ -125,10 +126,15 @@ func (p *ProductProcessorImpl) CreateProduct(ctx context.Context, req *models.Cr
// Upsert outlet-specific price if outlet context is present
if req.OutletID != uuid.Nil {
printToChecker := true // default
if req.PrintToChecker != nil {
printToChecker = *req.PrintToChecker
}
outletPriceEntity := &entities.ProductOutletPrice{
ProductID: productEntity.ID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: printToChecker,
}
if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil {
return nil, fmt.Errorf("failed to assign outlet price: %w", err)
@@ -196,16 +202,39 @@ func (p *ProductProcessorImpl) UpdateProduct(ctx context.Context, id uuid.UUID,
}
}
// Upsert outlet-specific price if outlet context is present
if req.OutletID != uuid.Nil && req.Price != nil {
// Upsert outlet-specific price if outlet context is present and price or print_to_checker is provided
if req.OutletID != uuid.Nil && (req.Price != nil || req.PrintToChecker != nil) {
// Fetch existing outlet price to use as fallback for fields not provided
existing, _ := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, req.OutletID)
price := float64(0)
if existing != nil {
price = existing.Price
}
if req.Price != nil {
price = *req.Price
}
printToChecker := true // default
if existing != nil {
printToChecker = existing.PrintToChecker
}
if req.PrintToChecker != nil {
printToChecker = *req.PrintToChecker
}
outletPriceEntity := &entities.ProductOutletPrice{
ProductID: id,
OutletID: req.OutletID,
Price: *req.Price,
Price: price,
PrintToChecker: printToChecker,
}
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> upserting outlet price: productID=%s outletID=%s price=%f printToChecker=%v", id, req.OutletID, price, printToChecker)
if err := p.outletPriceRepo.Upsert(ctx, outletPriceEntity); err != nil {
return nil, fmt.Errorf("failed to assign outlet price: %w", err)
}
} else {
logger.FromContext(ctx).Infof("ProductProcessor::UpdateProduct -> skipping outlet price upsert: outletID=%s price=%v printToChecker=%v", req.OutletID, req.Price, req.PrintToChecker)
}
productWithCategory, err := p.productRepo.GetWithCategory(ctx, id)
@@ -256,6 +285,7 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
outletPrice, err := p.outletPriceRepo.GetByProductAndOutlet(ctx, id, outletID)
if err == nil {
response.OutletPrice = &outletPrice.Price
response.PrintToChecker = outletPrice.PrintToChecker
}
} else {
// No outlet context — return all outlet prices for this product
@@ -267,6 +297,7 @@ func (p *ProductProcessorImpl) GetProductByID(ctx context.Context, id uuid.UUID,
OutletID: op.OutletID,
OutletName: op.Outlet.Name,
Price: op.Price,
PrintToChecker: op.PrintToChecker,
}
}
response.OutletPrices = prices
@@ -303,12 +334,37 @@ func (p *ProductProcessorImpl) ListProducts(ctx context.Context, filters map[str
}
responses := make([]models.ProductResponse, len(productEntities))
if outletID != uuid.Nil && len(productEntities) > 0 {
// Bulk-fetch outlet prices to populate OutletPrice and PrintToChecker per product
productIDs := make([]uuid.UUID, len(productEntities))
for i, e := range productEntities {
productIDs[i] = e.ID
}
outletPrices, opErr := p.outletPriceRepo.GetByProductsAndOutlet(ctx, productIDs, outletID)
priceMap := make(map[uuid.UUID]*entities.ProductOutletPrice)
if opErr == nil {
for _, op := range outletPrices {
priceMap[op.ProductID] = op
}
}
for i, entity := range productEntities {
response := mappers.ProductEntityToResponse(entity)
if response != nil {
if op, ok := priceMap[entity.ID]; ok {
response.OutletPrice = &op.Price
response.PrintToChecker = op.PrintToChecker
}
responses[i] = *response
}
}
} else {
for i, entity := range productEntities {
response := mappers.ProductEntityToResponse(entity)
if response != nil {
responses[i] = *response
}
}
}
return responses, int(total), nil
}
-101
View File
@@ -1,101 +0,0 @@
package repository
import (
"context"
"strings"
"github.com/google/uuid"
"apskel-pos-be/internal/entities"
"gorm.io/gorm"
)
type ExpenseRepositoryImpl struct {
db *gorm.DB
}
func NewExpenseRepositoryImpl(db *gorm.DB) *ExpenseRepositoryImpl {
return &ExpenseRepositoryImpl{
db: db,
}
}
func (r *ExpenseRepositoryImpl) Create(ctx context.Context, expense *entities.Expense) error {
return r.db.WithContext(ctx).Create(expense).Error
}
func (r *ExpenseRepositoryImpl) GetByID(ctx context.Context, id uuid.UUID) (*entities.Expense, error) {
var expense entities.Expense
err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount").
First(&expense, "id = ?", id).Error
if err != nil {
return nil, err
}
return &expense, nil
}
func (r *ExpenseRepositoryImpl) GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Expense, error) {
var expense entities.Expense
err := r.db.WithContext(ctx).
Preload("Items.ChartOfAccount").
Where("id = ? AND organization_id = ?", id, organizationID).
First(&expense).Error
if err != nil {
return nil, err
}
return &expense, nil
}
func (r *ExpenseRepositoryImpl) Update(ctx context.Context, expense *entities.Expense) error {
return r.db.WithContext(ctx).Save(expense).Error
}
func (r *ExpenseRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.Expense{}, "id = ?", id).Error
}
func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Expense, int64, error) {
var expenses []*entities.Expense
var total int64
query := r.db.WithContext(ctx).Model(&entities.Expense{}).Where("organization_id = ?", organizationID)
for key, value := range filters {
switch key {
case "search":
if searchStr, ok := value.(string); ok && searchStr != "" {
searchPattern := "%" + strings.ToLower(searchStr) + "%"
query = query.Where("LOWER(receiver) LIKE ? OR LOWER(code_number) LIKE ? OR LOWER(description) LIKE ?",
searchPattern, searchPattern, searchPattern)
}
case "outlet_id":
if outletID, ok := value.(uuid.UUID); ok {
query = query.Where("outlet_id = ?", outletID)
}
default:
query = query.Where(key+" = ?", value)
}
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.
Preload("Items.ChartOfAccount").
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&expenses).Error
return expenses, total, err
}
func (r *ExpenseRepositoryImpl) CreateItem(ctx context.Context, item *entities.ExpenseItem) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *ExpenseRepositoryImpl) DeleteItemsByExpenseID(ctx context.Context, expenseID uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&entities.ExpenseItem{}, "expense_id = ?", expenseID).Error
}
+6
View File
@@ -60,6 +60,8 @@ func (r *OrderRepositoryImpl) GetWithRelations(ctx context.Context, id uuid.UUID
Preload("User").
Preload("OrderItems").
Preload("OrderItems.Product").
Preload("OrderItems.Product.Category").
Preload("OrderItems.Product.ProductOutletPrices").
Preload("OrderItems.ProductVariant").
Preload("Payments").
Preload("Payments.PaymentMethod").
@@ -139,6 +141,8 @@ func (r *OrderRepositoryImpl) List(ctx context.Context, filters map[string]inter
Preload("User").
Preload("OrderItems").
Preload("OrderItems.Product").
Preload("OrderItems.Product.Category").
Preload("OrderItems.Product.ProductOutletPrices").
Preload("OrderItems.ProductVariant").
Preload("Payments").
Preload("Payments.PaymentMethod").
@@ -155,6 +159,8 @@ func (r *OrderRepositoryImpl) ListBySessionID(ctx context.Context, sessionID str
Preload("User").
Preload("OrderItems").
Preload("OrderItems.Product").
Preload("OrderItems.Product.Category").
Preload("OrderItems.Product.ProductOutletPrices").
Preload("OrderItems.ProductVariant").
Preload("Payments").
Preload("Payments.PaymentMethod").
@@ -7,7 +7,6 @@ import (
"github.com/google/uuid"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type ProductOutletPriceRepository interface {
@@ -53,10 +52,18 @@ func (r *ProductOutletPriceRepositoryImpl) GetByOutlet(ctx context.Context, outl
}
func (r *ProductOutletPriceRepositoryImpl) Upsert(ctx context.Context, price *entities.ProductOutletPrice) error {
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "product_id"}, {Name: "outlet_id"}},
DoUpdates: clause.AssignmentColumns([]string{"price", "updated_at"}),
}).Create(price).Error
if price.ID == uuid.Nil {
price.ID = uuid.New()
}
return r.db.WithContext(ctx).Exec(`
INSERT INTO product_outlet_prices (id, product_id, outlet_id, price, print_to_checker, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, NOW(), NOW())
ON CONFLICT (product_id, outlet_id)
DO UPDATE SET
price = EXCLUDED.price,
print_to_checker = EXCLUDED.print_to_checker,
updated_at = NOW()
`, price.ID, price.ProductID, price.OutletID, price.Price, price.PrintToChecker).Error
}
func (r *ProductOutletPriceRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error {
+1 -13
View File
@@ -50,12 +50,11 @@ type Router struct {
notificationHandler *handler.NotificationHandler
selfOrderHandler *handler.SelfOrderHandler
productOutletPriceHandler *handler.ProductOutletPriceHandler
expenseHandler *handler.ExpenseHandler
authMiddleware *middleware.AuthMiddleware
customerAuthMiddleware *middleware.CustomerAuthMiddleware
}
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler, expenseService *service.ExpenseServiceImpl, expenseValidator *validator.ExpenseValidatorImpl) *Router {
func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authService service.AuthService, authMiddleware *middleware.AuthMiddleware, userService *service.UserServiceImpl, userValidator *validator.UserValidatorImpl, organizationService service.OrganizationService, organizationValidator validator.OrganizationValidator, outletService service.OutletService, outletValidator validator.OutletValidator, outletSettingService service.OutletSettingService, categoryService service.CategoryService, categoryValidator validator.CategoryValidator, productService service.ProductService, productValidator validator.ProductValidator, productVariantService service.ProductVariantService, productVariantValidator validator.ProductVariantValidator, inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, orderService service.OrderService, orderValidator validator.OrderValidator, fileService service.FileService, fileValidator validator.FileValidator, customerService service.CustomerService, customerValidator validator.CustomerValidator, paymentMethodService service.PaymentMethodService, paymentMethodValidator validator.PaymentMethodValidator, analyticsService *service.AnalyticsServiceImpl, reportService service.ReportService, tableService *service.TableServiceImpl, tableValidator *validator.TableValidator, unitService handler.UnitService, ingredientService handler.IngredientService, productRecipeService service.ProductRecipeService, vendorService service.VendorService, vendorValidator validator.VendorValidator, purchaseOrderService service.PurchaseOrderService, purchaseOrderValidator validator.PurchaseOrderValidator, unitConverterService service.IngredientUnitConverterService, unitConverterValidator validator.IngredientUnitConverterValidator, chartOfAccountTypeService service.ChartOfAccountTypeService, chartOfAccountTypeValidator validator.ChartOfAccountTypeValidator, chartOfAccountService service.ChartOfAccountService, chartOfAccountValidator validator.ChartOfAccountValidator, accountService service.AccountService, accountValidator validator.AccountValidator, orderIngredientTransactionService service.OrderIngredientTransactionService, orderIngredientTransactionValidator validator.OrderIngredientTransactionValidator, gamificationService service.GamificationService, gamificationValidator validator.GamificationValidator, rewardService service.RewardService, rewardValidator validator.RewardValidator, campaignService service.CampaignService, campaignValidator validator.CampaignValidator, customerAuthService service.CustomerAuthService, customerAuthValidator validator.CustomerAuthValidator, customerPointsService service.CustomerPointsService, spinGameService service.SpinGameService, customerAuthMiddleware *middleware.CustomerAuthMiddleware, userDeviceService service.UserDeviceService, userDeviceValidator validator.UserDeviceValidator, notificationService service.NotificationService, notificationValidator validator.NotificationValidator, productOutletPriceService service.ProductOutletPriceService, productOutletPriceValidator validator.ProductOutletPriceValidator, selfOrderHandler *handler.SelfOrderHandler) *Router {
return &Router{
config: cfg,
@@ -98,7 +97,6 @@ func NewRouter(cfg *config.Config, healthHandler *handler.HealthHandler, authSer
notificationHandler: handler.NewNotificationHandler(notificationService, notificationValidator),
selfOrderHandler: selfOrderHandler,
productOutletPriceHandler: handler.NewProductOutletPriceHandler(productOutletPriceService, productOutletPriceValidator),
expenseHandler: handler.NewExpenseHandler(expenseService, expenseValidator),
}
}
@@ -446,16 +444,6 @@ func (r *Router) addAppRoutes(rg *gin.Engine) {
accounts.GET("/:id/balance", r.accountHandler.GetAccountBalance)
}
expenses := protected.Group("/expenses")
expenses.Use(r.authMiddleware.RequireAdminOrManager())
{
expenses.POST("", r.expenseHandler.CreateExpense)
expenses.GET("", r.expenseHandler.ListExpenses)
expenses.GET("/:id", r.expenseHandler.GetExpense)
expenses.PUT("/:id", r.expenseHandler.UpdateExpense)
expenses.DELETE("/:id", r.expenseHandler.DeleteExpense)
}
orderIngredientTransactions := protected.Group("/order-ingredient-transactions")
orderIngredientTransactions.Use(r.authMiddleware.RequireAdminOrManager())
{
-107
View File
@@ -1,107 +0,0 @@
package service
import (
"apskel-pos-be/internal/appcontext"
"context"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/processor"
"apskel-pos-be/internal/transformer"
"github.com/google/uuid"
)
type ExpenseService interface {
CreateExpense(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateExpenseRequest) *contract.Response
UpdateExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateExpenseRequest) *contract.Response
DeleteExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
GetExpenseByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response
ListExpenses(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListExpenseRequest) *contract.Response
}
type ExpenseServiceImpl struct {
expenseProcessor processor.ExpenseProcessor
}
func NewExpenseService(expenseProcessor processor.ExpenseProcessor) *ExpenseServiceImpl {
return &ExpenseServiceImpl{
expenseProcessor: expenseProcessor,
}
}
func (s *ExpenseServiceImpl) CreateExpense(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateExpenseRequest) *contract.Response {
modelReq := transformer.CreateExpenseRequestToModel(req)
expenseResponse, err := s.expenseProcessor.CreateExpense(ctx, apctx.OrganizationID, modelReq)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.ExpenseModelResponseToResponse(expenseResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *ExpenseServiceImpl) UpdateExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID, req *contract.UpdateExpenseRequest) *contract.Response {
modelReq := transformer.UpdateExpenseRequestToModel(req)
expenseResponse, err := s.expenseProcessor.UpdateExpense(ctx, id, apctx.OrganizationID, modelReq)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.ExpenseModelResponseToResponse(expenseResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *ExpenseServiceImpl) DeleteExpense(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
err := s.expenseProcessor.DeleteExpense(ctx, id, apctx.OrganizationID)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
return contract.BuildSuccessResponse(map[string]interface{}{
"message": "Expense deleted successfully",
})
}
func (s *ExpenseServiceImpl) GetExpenseByID(ctx context.Context, apctx *appcontext.ContextInfo, id uuid.UUID) *contract.Response {
expenseResponse, err := s.expenseProcessor.GetExpenseByID(ctx, id, apctx.OrganizationID)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponse := transformer.ExpenseModelResponseToResponse(expenseResponse)
return contract.BuildSuccessResponse(contractResponse)
}
func (s *ExpenseServiceImpl) ListExpenses(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.ListExpenseRequest) *contract.Response {
modelReq := transformer.ListExpenseRequestToModel(req)
filters := make(map[string]interface{})
if modelReq.Search != "" {
filters["search"] = modelReq.Search
}
expenses, totalPages, err := s.expenseProcessor.ListExpenses(ctx, apctx.OrganizationID, filters, modelReq.Page, modelReq.Limit)
if err != nil {
errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.ExpenseServiceEntity, err.Error())
return contract.BuildErrorResponse([]*contract.ResponseError{errorResp})
}
contractResponses := transformer.ExpenseModelResponsesToResponses(expenses)
response := contract.ListExpenseResponse{
Expenses: contractResponses,
TotalCount: len(contractResponses),
Page: modelReq.Page,
Limit: modelReq.Limit,
TotalPages: totalPages,
}
return contract.BuildSuccessResponse(response)
}
@@ -108,6 +108,7 @@ func (s *ProductOutletPriceServiceImpl) BulkUpsert(ctx context.Context, req *con
ProductID: req.ProductID,
OutletID: p.OutletID,
Price: p.Price,
PrintToChecker: p.PrintToChecker,
}
}
-126
View File
@@ -1,126 +0,0 @@
package transformer
import (
"apskel-pos-be/internal/contract"
"apskel-pos-be/internal/models"
)
func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.CreateExpenseRequest {
items := make([]models.CreateExpenseItemRequest, len(req.Items))
for i, item := range req.Items {
items[i] = CreateExpenseItemRequestToModel(&item)
}
return &models.CreateExpenseRequest{
Receiver: req.Receiver,
TransactionDate: req.TransactionDate,
CodeNumber: req.CodeNumber,
OutletID: req.OutletID,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
Items: items,
}
}
func CreateExpenseItemRequestToModel(req *contract.CreateExpenseItemRequest) models.CreateExpenseItemRequest {
return models.CreateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID,
Description: req.Description,
Amount: req.Amount,
}
}
func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.UpdateExpenseRequest {
modelReq := &models.UpdateExpenseRequest{
Receiver: req.Receiver,
TransactionDate: req.TransactionDate,
CodeNumber: req.CodeNumber,
OutletID: req.OutletID,
Description: req.Description,
Tax: req.Tax,
Total: req.Total,
Reserved1: req.Reserved1,
}
if req.Items != nil {
items := make([]models.UpdateExpenseItemRequest, len(req.Items))
for i, item := range req.Items {
items[i] = UpdateExpenseItemRequestToModel(&item)
}
modelReq.Items = items
}
return modelReq
}
func UpdateExpenseItemRequestToModel(req *contract.UpdateExpenseItemRequest) models.UpdateExpenseItemRequest {
return models.UpdateExpenseItemRequest{
ChartOfAccountID: req.ChartOfAccountID,
Description: req.Description,
Amount: req.Amount,
}
}
func ListExpenseRequestToModel(req *contract.ListExpenseRequest) *models.ListExpenseRequest {
return &models.ListExpenseRequest{
Page: req.Page,
Limit: req.Limit,
Search: req.Search,
}
}
func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.ExpenseResponse {
if expense == nil {
return nil
}
items := make([]contract.ExpenseItemResponse, len(expense.Items))
for i, item := range expense.Items {
items[i] = ExpenseItemModelResponseToResponse(&item)
}
return &contract.ExpenseResponse{
ID: expense.ID,
OrganizationID: expense.OrganizationID,
OutletID: expense.OutletID,
Receiver: expense.Receiver,
TransactionDate: expense.TransactionDate,
CodeNumber: expense.CodeNumber,
Description: expense.Description,
Tax: expense.Tax,
Total: expense.Total,
Reserved1: expense.Reserved1,
CreatedAt: expense.CreatedAt,
UpdatedAt: expense.UpdatedAt,
Items: items,
}
}
func ExpenseItemModelResponseToResponse(item *models.ExpenseItemResponse) contract.ExpenseItemResponse {
return contract.ExpenseItemResponse{
ID: item.ID,
ExpenseID: item.ExpenseID,
ChartOfAccountID: item.ChartOfAccountID,
ChartOfAccountName: item.ChartOfAccountName,
Description: item.Description,
Amount: item.Amount,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
func ExpenseModelResponsesToResponses(expenses []*models.ExpenseResponse) []contract.ExpenseResponse {
if expenses == nil {
return nil
}
responses := make([]contract.ExpenseResponse, len(expenses))
for i, expense := range expenses {
response := ExpenseModelResponseToResponse(expense)
if response != nil {
responses[i] = *response
}
}
return responses
}
@@ -100,6 +100,8 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
ProductName: item.ProductName,
ProductVariantID: item.ProductVariantID,
ProductVariantName: item.ProductVariantName,
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
@@ -110,6 +112,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
PrinterType: item.PrinterType,
PrintToChecker: item.PrintToChecker,
PaidQuantity: item.PaidQuantity,
}
}
@@ -168,6 +171,8 @@ func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToO
ProductName: item.ProductName,
ProductVariantID: item.ProductVariantID,
ProductVariantName: item.ProductVariantName,
CategoryID: item.CategoryID,
CategoryName: item.CategoryName,
Quantity: item.Quantity,
UnitPrice: item.UnitPrice,
TotalPrice: item.TotalPrice,
@@ -177,6 +182,7 @@ func AddToOrderModelToContract(resp *models.AddToOrderResponse) *contract.AddToO
Status: string(item.Status),
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
PrintToChecker: item.PrintToChecker,
}
}
return &contract.AddToOrderResponse{
@@ -14,6 +14,7 @@ func CreateProductOutletPriceRequestToModel(req *contract.CreateProductOutletPri
ProductID: req.ProductID,
OutletID: req.OutletID,
Price: req.Price,
PrintToChecker: req.PrintToChecker,
}
}
@@ -24,6 +25,7 @@ func UpdateProductOutletPriceRequestToModel(req *contract.UpdateProductOutletPri
return &models.UpdateProductOutletPriceRequest{
Price: &req.Price,
PrintToChecker: req.PrintToChecker,
}
}
@@ -37,6 +39,7 @@ func ProductOutletPriceModelToResponse(m *models.ProductOutletPrice) *contract.P
ProductID: m.ProductID,
OutletID: m.OutletID,
Price: m.Price,
PrintToChecker: m.PrintToChecker,
CreatedAt: m.CreatedAt,
UpdatedAt: m.UpdatedAt,
}
@@ -57,6 +57,7 @@ func CreateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Cr
BusinessType: businessType,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
PrintToChecker: req.PrintToChecker,
Metadata: metadata,
Variants: variants,
}
@@ -84,6 +85,7 @@ func UpdateProductRequestToModel(apctx *appcontext.ContextInfo, req *contract.Up
Cost: req.Cost,
ImageURL: req.ImageURL,
PrinterType: req.PrinterType,
PrintToChecker: req.PrintToChecker,
Metadata: metadata,
IsActive: req.IsActive,
}
@@ -122,6 +124,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
OutletID: op.OutletID,
OutletName: op.OutletName,
Price: op.Price,
PrintToChecker: op.PrintToChecker,
}
}
}
@@ -141,6 +144,7 @@ func ProductModelResponseToResponse(prod *models.ProductResponse) *contract.Prod
BusinessType: string(prod.BusinessType),
ImageURL: prod.ImageURL,
PrinterType: prod.PrinterType,
PrintToChecker: prod.PrintToChecker,
Metadata: prod.Metadata,
IsActive: prod.IsActive,
CreatedAt: prod.CreatedAt,
-141
View File
@@ -1,141 +0,0 @@
package validator
import (
"errors"
"fmt"
"strings"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"github.com/google/uuid"
)
type ExpenseValidator interface {
ValidateCreateExpenseRequest(req *contract.CreateExpenseRequest) (error, string)
ValidateUpdateExpenseRequest(req *contract.UpdateExpenseRequest) (error, string)
ValidateListExpenseRequest(req *contract.ListExpenseRequest) (error, string)
}
type ExpenseValidatorImpl struct{}
func NewExpenseValidator() *ExpenseValidatorImpl {
return &ExpenseValidatorImpl{}
}
func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.CreateExpenseRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.Receiver) == "" {
return errors.New("receiver is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.TransactionDate) == "" {
return errors.New("transaction_date is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.CodeNumber) == "" {
return errors.New("code_number is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.OutletID) == "" {
return errors.New("outlet_id is required"), constants.MissingFieldErrorCode
}
if _, err := uuid.Parse(req.OutletID); err != nil {
return errors.New("outlet_id must be a valid UUID"), constants.MalformedFieldErrorCode
}
if req.Total <= 0 {
return errors.New("total must be greater than 0"), constants.MalformedFieldErrorCode
}
if req.Tax < 0 {
return errors.New("tax cannot be negative"), constants.MalformedFieldErrorCode
}
if len(req.Items) == 0 {
return errors.New("at least one item is required"), constants.MissingFieldErrorCode
}
for i, item := range req.Items {
if strings.TrimSpace(item.ChartOfAccountID) == "" {
return fmt.Errorf("item %d: chart_of_account_id is required", i), constants.MissingFieldErrorCode
}
if _, err := uuid.Parse(item.ChartOfAccountID); err != nil {
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
}
if item.Amount <= 0 {
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
}
}
return nil, ""
}
func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.UpdateExpenseRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if req.Receiver != nil && strings.TrimSpace(*req.Receiver) == "" {
return errors.New("receiver cannot be empty"), constants.MalformedFieldErrorCode
}
if req.CodeNumber != nil && strings.TrimSpace(*req.CodeNumber) == "" {
return errors.New("code_number cannot be empty"), constants.MalformedFieldErrorCode
}
if req.OutletID != nil {
if strings.TrimSpace(*req.OutletID) == "" {
return errors.New("outlet_id cannot be empty"), constants.MalformedFieldErrorCode
}
if _, err := uuid.Parse(*req.OutletID); err != nil {
return errors.New("outlet_id must be a valid UUID"), constants.MalformedFieldErrorCode
}
}
if req.Total != nil && *req.Total <= 0 {
return errors.New("total must be greater than 0"), constants.MalformedFieldErrorCode
}
if req.Tax != nil && *req.Tax < 0 {
return errors.New("tax cannot be negative"), constants.MalformedFieldErrorCode
}
if req.Items != nil {
for i, item := range req.Items {
if item.ChartOfAccountID != nil {
if strings.TrimSpace(*item.ChartOfAccountID) == "" {
return fmt.Errorf("item %d: chart_of_account_id cannot be empty", i), constants.MalformedFieldErrorCode
}
if _, err := uuid.Parse(*item.ChartOfAccountID); err != nil {
return fmt.Errorf("item %d: chart_of_account_id must be a valid UUID", i), constants.MalformedFieldErrorCode
}
}
if item.Amount != nil && *item.Amount <= 0 {
return fmt.Errorf("item %d: amount must be greater than 0", i), constants.MalformedFieldErrorCode
}
}
}
return nil, ""
}
func (v *ExpenseValidatorImpl) ValidateListExpenseRequest(req *contract.ListExpenseRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if req.Page < 1 {
return errors.New("page must be at least 1"), constants.MalformedFieldErrorCode
}
if req.Limit < 1 || req.Limit > 100 {
return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode
}
return nil, ""
}
@@ -0,0 +1 @@
ALTER TABLE product_outlet_prices DROP COLUMN IF EXISTS print_to_checker;
@@ -0,0 +1 @@
ALTER TABLE product_outlet_prices ADD COLUMN print_to_checker BOOLEAN NOT NULL DEFAULT TRUE;
@@ -1,2 +0,0 @@
DROP TABLE IF EXISTS expense_items;
DROP TABLE IF EXISTS expenses;
@@ -1,33 +0,0 @@
CREATE TABLE expenses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
outlet_id UUID NOT NULL REFERENCES outlets(id) ON DELETE CASCADE,
receiver VARCHAR(255) NOT NULL,
transaction_date DATE NOT NULL,
code_number VARCHAR(50) NOT NULL,
description TEXT,
tax DECIMAL(15,2) NOT NULL DEFAULT 0,
total DECIMAL(15,2) NOT NULL DEFAULT 0,
reserved1 TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_expenses_organization_id ON expenses(organization_id);
CREATE INDEX idx_expenses_outlet_id ON expenses(outlet_id);
CREATE INDEX idx_expenses_transaction_date ON expenses(transaction_date);
CREATE INDEX idx_expenses_code_number ON expenses(code_number);
CREATE INDEX idx_expenses_created_at ON expenses(created_at);
CREATE TABLE expense_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
expense_id UUID NOT NULL REFERENCES expenses(id) ON DELETE CASCADE,
chart_of_account_id UUID NOT NULL REFERENCES chart_of_accounts(id) ON DELETE RESTRICT,
description TEXT,
amount DECIMAL(15,2) NOT NULL DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_expense_items_expense_id ON expense_items(expense_id);
CREATE INDEX idx_expense_items_chart_of_account_id ON expense_items(chart_of_account_id);