feature/expense #13
@ -11,6 +11,7 @@ type CreateExpenseRequest struct {
|
|||||||
TransactionDate string `json:"transaction_date" validate:"required"`
|
TransactionDate string `json:"transaction_date" validate:"required"`
|
||||||
CodeNumber string `json:"code_number" validate:"required"`
|
CodeNumber string `json:"code_number" validate:"required"`
|
||||||
OutletID string `json:"outlet_id" validate:"required"`
|
OutletID string `json:"outlet_id" validate:"required"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
Total float64 `json:"total" validate:"required"`
|
Total float64 `json:"total" validate:"required"`
|
||||||
@ -29,6 +30,7 @@ type UpdateExpenseRequest struct {
|
|||||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
CodeNumber *string `json:"code_number,omitempty"`
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
OutletID *string `json:"outlet_id,omitempty"`
|
OutletID *string `json:"outlet_id,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Tax *float64 `json:"tax,omitempty"`
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
Total *float64 `json:"total,omitempty"`
|
Total *float64 `json:"total,omitempty"`
|
||||||
@ -50,6 +52,7 @@ type ExpenseResponse struct {
|
|||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
Total float64 `json:"total"`
|
Total float64 `json:"total"`
|
||||||
@ -76,6 +79,7 @@ type ListExpenseRequest struct {
|
|||||||
Limit int `json:"limit" validate:"min=1,max=100"`
|
Limit int `json:"limit" validate:"min=1,max=100"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
OutletID string `json:"outlet_id,omitempty"`
|
OutletID string `json:"outlet_id,omitempty"`
|
||||||
|
Status string `json:"status,omitempty" validate:"omitempty,oneof=draft sent approved cancel"`
|
||||||
StartDate string `json:"start_date,omitempty"`
|
StartDate string `json:"start_date,omitempty"`
|
||||||
EndDate string `json:"end_date,omitempty"`
|
EndDate string `json:"end_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ type Expense struct {
|
|||||||
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
Receiver string `gorm:"not null;size:255" json:"receiver"`
|
||||||
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
TransactionDate time.Time `gorm:"type:date;not null" json:"transaction_date"`
|
||||||
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
CodeNumber string `gorm:"not null;size:50" json:"code_number"`
|
||||||
|
Status string `gorm:"not null;size:20;default:'draft'" json:"status"`
|
||||||
Description *string `gorm:"type:text" json:"description"`
|
Description *string `gorm:"type:text" json:"description"`
|
||||||
Tax float64 `gorm:"type:decimal(15,2);not null;default:0" json:"tax"`
|
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"`
|
Total float64 `gorm:"type:decimal(15,2);not null;default:0" json:"total"`
|
||||||
|
|||||||
@ -164,6 +164,10 @@ func (h *ExpenseHandler) ListExpenses(c *gin.Context) {
|
|||||||
req.Search = search
|
req.Search = search
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if status := c.Query("status"); status != "" {
|
||||||
|
req.Status = status
|
||||||
|
}
|
||||||
|
|
||||||
// Prioritize outlet_id from context (e.g. outlet-scoped user),
|
// Prioritize outlet_id from context (e.g. outlet-scoped user),
|
||||||
// fall back to query param if context has no outlet.
|
// fall back to query param if context has no outlet.
|
||||||
if contextInfo.OutletID != uuid.Nil {
|
if contextInfo.OutletID != uuid.Nil {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ func ExpenseEntityToModel(entity *entities.Expense) *models.Expense {
|
|||||||
Receiver: entity.Receiver,
|
Receiver: entity.Receiver,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
CodeNumber: entity.CodeNumber,
|
CodeNumber: entity.CodeNumber,
|
||||||
|
Status: entity.Status,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
Tax: entity.Tax,
|
Tax: entity.Tax,
|
||||||
Total: entity.Total,
|
Total: entity.Total,
|
||||||
@ -38,6 +39,7 @@ func ExpenseModelToEntity(model *models.Expense) *entities.Expense {
|
|||||||
Receiver: model.Receiver,
|
Receiver: model.Receiver,
|
||||||
TransactionDate: model.TransactionDate,
|
TransactionDate: model.TransactionDate,
|
||||||
CodeNumber: model.CodeNumber,
|
CodeNumber: model.CodeNumber,
|
||||||
|
Status: model.Status,
|
||||||
Description: model.Description,
|
Description: model.Description,
|
||||||
Tax: model.Tax,
|
Tax: model.Tax,
|
||||||
Total: model.Total,
|
Total: model.Total,
|
||||||
@ -59,6 +61,7 @@ func ExpenseEntityToResponse(entity *entities.Expense) *models.ExpenseResponse {
|
|||||||
Receiver: entity.Receiver,
|
Receiver: entity.Receiver,
|
||||||
TransactionDate: entity.TransactionDate,
|
TransactionDate: entity.TransactionDate,
|
||||||
CodeNumber: entity.CodeNumber,
|
CodeNumber: entity.CodeNumber,
|
||||||
|
Status: entity.Status,
|
||||||
Description: entity.Description,
|
Description: entity.Description,
|
||||||
Tax: entity.Tax,
|
Tax: entity.Tax,
|
||||||
Total: entity.Total,
|
Total: entity.Total,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type Expense struct {
|
|||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
Total float64 `json:"total"`
|
Total float64 `json:"total"`
|
||||||
@ -39,6 +40,7 @@ type ExpenseResponse struct {
|
|||||||
Receiver string `json:"receiver"`
|
Receiver string `json:"receiver"`
|
||||||
TransactionDate time.Time `json:"transaction_date"`
|
TransactionDate time.Time `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
|
Status string `json:"status"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
Total float64 `json:"total"`
|
Total float64 `json:"total"`
|
||||||
@ -65,6 +67,7 @@ type CreateExpenseRequest struct {
|
|||||||
TransactionDate string `json:"transaction_date"`
|
TransactionDate string `json:"transaction_date"`
|
||||||
CodeNumber string `json:"code_number"`
|
CodeNumber string `json:"code_number"`
|
||||||
OutletID string `json:"outlet_id"`
|
OutletID string `json:"outlet_id"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
Tax float64 `json:"tax"`
|
Tax float64 `json:"tax"`
|
||||||
Total float64 `json:"total"`
|
Total float64 `json:"total"`
|
||||||
@ -83,6 +86,7 @@ type UpdateExpenseRequest struct {
|
|||||||
TransactionDate *string `json:"transaction_date,omitempty"`
|
TransactionDate *string `json:"transaction_date,omitempty"`
|
||||||
CodeNumber *string `json:"code_number,omitempty"`
|
CodeNumber *string `json:"code_number,omitempty"`
|
||||||
OutletID *string `json:"outlet_id,omitempty"`
|
OutletID *string `json:"outlet_id,omitempty"`
|
||||||
|
Status *string `json:"status,omitempty"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
Tax *float64 `json:"tax,omitempty"`
|
Tax *float64 `json:"tax,omitempty"`
|
||||||
Total *float64 `json:"total,omitempty"`
|
Total *float64 `json:"total,omitempty"`
|
||||||
@ -102,6 +106,7 @@ type ListExpenseRequest struct {
|
|||||||
Limit int `json:"limit"`
|
Limit int `json:"limit"`
|
||||||
Search string `json:"search,omitempty"`
|
Search string `json:"search,omitempty"`
|
||||||
OutletID string `json:"outlet_id,omitempty"`
|
OutletID string `json:"outlet_id,omitempty"`
|
||||||
|
Status string `json:"status,omitempty"`
|
||||||
StartDate string `json:"start_date,omitempty"`
|
StartDate string `json:"start_date,omitempty"`
|
||||||
EndDate string `json:"end_date,omitempty"`
|
EndDate string `json:"end_date,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"apskel-pos-be/internal/constants"
|
||||||
"apskel-pos-be/internal/entities"
|
"apskel-pos-be/internal/entities"
|
||||||
"apskel-pos-be/internal/mappers"
|
"apskel-pos-be/internal/mappers"
|
||||||
"apskel-pos-be/internal/models"
|
"apskel-pos-be/internal/models"
|
||||||
@ -41,12 +42,18 @@ func (p *ExpenseProcessorImpl) CreateExpense(ctx context.Context, organizationID
|
|||||||
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
|
return nil, fmt.Errorf("invalid transaction_date format, expected YYYY-MM-DD: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status := string(constants.ExpenseStatusDraft)
|
||||||
|
if req.Status != nil {
|
||||||
|
status = *req.Status
|
||||||
|
}
|
||||||
|
|
||||||
expenseEntity := &entities.Expense{
|
expenseEntity := &entities.Expense{
|
||||||
OrganizationID: organizationID,
|
OrganizationID: organizationID,
|
||||||
OutletID: outletID,
|
OutletID: outletID,
|
||||||
Receiver: req.Receiver,
|
Receiver: req.Receiver,
|
||||||
TransactionDate: transactionDate,
|
TransactionDate: transactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
|
Status: status,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Tax: req.Tax,
|
Tax: req.Tax,
|
||||||
Total: req.Total,
|
Total: req.Total,
|
||||||
@ -104,6 +111,9 @@ func (p *ExpenseProcessorImpl) UpdateExpense(ctx context.Context, id, organizati
|
|||||||
if req.CodeNumber != nil {
|
if req.CodeNumber != nil {
|
||||||
expenseEntity.CodeNumber = *req.CodeNumber
|
expenseEntity.CodeNumber = *req.CodeNumber
|
||||||
}
|
}
|
||||||
|
if req.Status != nil {
|
||||||
|
expenseEntity.Status = *req.Status
|
||||||
|
}
|
||||||
if req.OutletID != nil {
|
if req.OutletID != nil {
|
||||||
outletID, err := uuid.Parse(*req.OutletID)
|
outletID, err := uuid.Parse(*req.OutletID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -512,6 +512,7 @@ func (r *AnalyticsRepositoryImpl) getExpenseByCategory(ctx context.Context, orga
|
|||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
Joins("LEFT JOIN chart_of_accounts parent_coa ON coa.parent_id = parent_coa.id").
|
||||||
Where("e.organization_id = ?", organizationID).
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
@ -535,6 +536,7 @@ func (r *AnalyticsRepositoryImpl) getOperationalExpenseItems(ctx context.Context
|
|||||||
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
Joins("JOIN expenses e ON ei.expense_id = e.id").
|
||||||
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
Joins("JOIN chart_of_accounts coa ON ei.chart_of_account_id = coa.id").
|
||||||
Where("e.organization_id = ?", organizationID).
|
Where("e.organization_id = ?", organizationID).
|
||||||
|
Where("e.status = ?", "approved").
|
||||||
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
Where("e.transaction_date >= ? AND e.transaction_date <= ?", dateFrom, dateTo)
|
||||||
|
|
||||||
if outletID != nil {
|
if outletID != nil {
|
||||||
|
|||||||
@ -84,6 +84,10 @@ func (r *ExpenseRepositoryImpl) List(ctx context.Context, organizationID uuid.UU
|
|||||||
if outletID, ok := value.(uuid.UUID); ok {
|
if outletID, ok := value.(uuid.UUID); ok {
|
||||||
query = query.Where("outlet_id = ?", outletID)
|
query = query.Where("outlet_id = ?", outletID)
|
||||||
}
|
}
|
||||||
|
case "status":
|
||||||
|
if status, ok := value.(string); ok && status != "" {
|
||||||
|
query = query.Where("status = ?", status)
|
||||||
|
}
|
||||||
case "start_date":
|
case "start_date":
|
||||||
if startDate, ok := value.(time.Time); ok {
|
if startDate, ok := value.(time.Time); ok {
|
||||||
query = query.Where("transaction_date >= ?", startDate)
|
query = query.Where("transaction_date >= ?", startDate)
|
||||||
|
|||||||
@ -87,6 +87,9 @@ func (s *ExpenseServiceImpl) ListExpenses(ctx context.Context, apctx *appcontext
|
|||||||
if modelReq.Search != "" {
|
if modelReq.Search != "" {
|
||||||
filters["search"] = modelReq.Search
|
filters["search"] = modelReq.Search
|
||||||
}
|
}
|
||||||
|
if modelReq.Status != "" {
|
||||||
|
filters["status"] = modelReq.Status
|
||||||
|
}
|
||||||
if modelReq.OutletID != "" {
|
if modelReq.OutletID != "" {
|
||||||
outletID, err := uuid.Parse(modelReq.OutletID)
|
outletID, err := uuid.Parse(modelReq.OutletID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ func CreateExpenseRequestToModel(req *contract.CreateExpenseRequest) *models.Cre
|
|||||||
TransactionDate: req.TransactionDate,
|
TransactionDate: req.TransactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
|
Status: req.Status,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Tax: req.Tax,
|
Tax: req.Tax,
|
||||||
Total: req.Total,
|
Total: req.Total,
|
||||||
@ -38,6 +39,7 @@ func UpdateExpenseRequestToModel(req *contract.UpdateExpenseRequest) *models.Upd
|
|||||||
TransactionDate: req.TransactionDate,
|
TransactionDate: req.TransactionDate,
|
||||||
CodeNumber: req.CodeNumber,
|
CodeNumber: req.CodeNumber,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
|
Status: req.Status,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
Tax: req.Tax,
|
Tax: req.Tax,
|
||||||
Total: req.Total,
|
Total: req.Total,
|
||||||
@ -70,6 +72,7 @@ func ListExpenseRequestToModel(req *contract.ListExpenseRequest) *models.ListExp
|
|||||||
Limit: req.Limit,
|
Limit: req.Limit,
|
||||||
Search: req.Search,
|
Search: req.Search,
|
||||||
OutletID: req.OutletID,
|
OutletID: req.OutletID,
|
||||||
|
Status: req.Status,
|
||||||
StartDate: req.StartDate,
|
StartDate: req.StartDate,
|
||||||
EndDate: req.EndDate,
|
EndDate: req.EndDate,
|
||||||
}
|
}
|
||||||
@ -92,6 +95,7 @@ func ExpenseModelResponseToResponse(expense *models.ExpenseResponse) *contract.E
|
|||||||
Receiver: expense.Receiver,
|
Receiver: expense.Receiver,
|
||||||
TransactionDate: expense.TransactionDate,
|
TransactionDate: expense.TransactionDate,
|
||||||
CodeNumber: expense.CodeNumber,
|
CodeNumber: expense.CodeNumber,
|
||||||
|
Status: expense.Status,
|
||||||
Description: expense.Description,
|
Description: expense.Description,
|
||||||
Tax: expense.Tax,
|
Tax: expense.Tax,
|
||||||
Total: expense.Total,
|
Total: expense.Total,
|
||||||
|
|||||||
@ -48,6 +48,10 @@ func (v *ExpenseValidatorImpl) ValidateCreateExpenseRequest(req *contract.Create
|
|||||||
return errors.New("outlet_id must be a valid UUID"), constants.MalformedFieldErrorCode
|
return errors.New("outlet_id must be a valid UUID"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != nil && !constants.IsValidExpenseStatus(constants.ExpenseStatus(*req.Status)) {
|
||||||
|
return errors.New("status must be one of: draft, sent, approved, cancel"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
if req.Total <= 0 {
|
if req.Total <= 0 {
|
||||||
return errors.New("total must be greater than 0"), constants.MalformedFieldErrorCode
|
return errors.New("total must be greater than 0"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
@ -91,6 +95,10 @@ func (v *ExpenseValidatorImpl) ValidateUpdateExpenseRequest(req *contract.Update
|
|||||||
return errors.New("code_number cannot be empty"), constants.MalformedFieldErrorCode
|
return errors.New("code_number cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != nil && !constants.IsValidExpenseStatus(constants.ExpenseStatus(*req.Status)) {
|
||||||
|
return errors.New("status must be one of: draft, sent, approved, cancel"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
if req.OutletID != nil {
|
if req.OutletID != nil {
|
||||||
if strings.TrimSpace(*req.OutletID) == "" {
|
if strings.TrimSpace(*req.OutletID) == "" {
|
||||||
return errors.New("outlet_id cannot be empty"), constants.MalformedFieldErrorCode
|
return errors.New("outlet_id cannot be empty"), constants.MalformedFieldErrorCode
|
||||||
@ -143,5 +151,9 @@ func (v *ExpenseValidatorImpl) ValidateListExpenseRequest(req *contract.ListExpe
|
|||||||
return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode
|
return errors.New("limit must be between 1 and 100"), constants.MalformedFieldErrorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.Status != "" && !constants.IsValidExpenseStatus(constants.ExpenseStatus(req.Status)) {
|
||||||
|
return errors.New("status must be one of: draft, sent, approved, cancel"), constants.MalformedFieldErrorCode
|
||||||
|
}
|
||||||
|
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
|
|||||||
3
migrations/000073_add_status_to_expenses.down.sql
Normal file
3
migrations/000073_add_status_to_expenses.down.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_expenses_status;
|
||||||
|
ALTER TABLE expenses DROP CONSTRAINT IF EXISTS expenses_status_check;
|
||||||
|
ALTER TABLE expenses DROP COLUMN IF EXISTS status;
|
||||||
10
migrations/000073_add_status_to_expenses.up.sql
Normal file
10
migrations/000073_add_status_to_expenses.up.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE expenses ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'draft';
|
||||||
|
|
||||||
|
UPDATE expenses
|
||||||
|
SET status = 'approved'
|
||||||
|
WHERE status = 'draft';
|
||||||
|
|
||||||
|
ALTER TABLE expenses DROP CONSTRAINT IF EXISTS expenses_status_check;
|
||||||
|
ALTER TABLE expenses ADD CONSTRAINT expenses_status_check CHECK (status IN ('draft', 'sent', 'approved', 'cancel'));
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_expenses_status ON expenses(status);
|
||||||
Loading…
x
Reference in New Issue
Block a user