Add coa purchase and vendors

This commit is contained in:
Aditya Siregar
2025-09-12 01:12:11 +07:00
parent c107733add
commit efe09c21e4
86 changed files with 8517 additions and 212 deletions
+207
View File
@@ -0,0 +1,207 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type AccountProcessor interface {
CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error)
GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error)
UpdateAccount(ctx context.Context, id uuid.UUID, req *models.UpdateAccountRequest) (*models.AccountResponse, error)
DeleteAccount(ctx context.Context, id uuid.UUID) error
ListAccounts(ctx context.Context, req *models.ListAccountsRequest) ([]models.AccountResponse, int, error)
GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.AccountResponse, error)
GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]models.AccountResponse, error)
UpdateAccountBalance(ctx context.Context, id uuid.UUID, amount float64) error
GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error)
}
type AccountProcessorImpl struct {
accountRepo AccountRepository
chartOfAccountRepo ChartOfAccountRepository
}
func NewAccountProcessorImpl(accountRepo AccountRepository, chartOfAccountRepo ChartOfAccountRepository) *AccountProcessorImpl {
return &AccountProcessorImpl{
accountRepo: accountRepo,
chartOfAccountRepo: chartOfAccountRepo,
}
}
func (p *AccountProcessorImpl) CreateAccount(ctx context.Context, req *models.CreateAccountRequest) (*models.AccountResponse, error) {
// Get organization and outlet from context
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var outletID *uuid.UUID
if appCtx.OutletID != uuid.Nil {
outletID = &appCtx.OutletID
}
// Check if account number already exists for this organization/outlet
existing, err := p.accountRepo.GetByNumber(ctx, organizationID, req.Number, outletID)
if err == nil && existing != nil {
return nil, fmt.Errorf("account with number %s already exists", req.Number)
}
// Validate chart of account exists
_, err = p.chartOfAccountRepo.GetByID(ctx, req.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("chart of account not found: %w", err)
}
entity := mappers.AccountCreateRequestToEntity(req, organizationID, outletID)
err = p.accountRepo.Create(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to create account: %w", err)
}
return mappers.AccountEntityToResponse(entity), nil
}
func (p *AccountProcessorImpl) GetAccountByID(ctx context.Context, id uuid.UUID) (*models.AccountResponse, error) {
entity, err := p.accountRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("account not found: %w", err)
}
return mappers.AccountEntityToResponse(entity), nil
}
func (p *AccountProcessorImpl) UpdateAccount(ctx context.Context, id uuid.UUID, req *models.UpdateAccountRequest) (*models.AccountResponse, error) {
entity, err := p.accountRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("account not found: %w", err)
}
// Check if new number already exists (if number is being updated)
if req.Number != nil && *req.Number != entity.Number {
existing, err := p.accountRepo.GetByNumber(ctx, entity.OrganizationID, *req.Number, entity.OutletID)
if err == nil && existing != nil {
return nil, fmt.Errorf("account with number %s already exists", *req.Number)
}
}
// Validate chart of account exists if provided
if req.ChartOfAccountID != nil {
_, err = p.chartOfAccountRepo.GetByID(ctx, *req.ChartOfAccountID)
if err != nil {
return nil, fmt.Errorf("chart of account not found: %w", err)
}
}
mappers.AccountUpdateRequestToEntity(entity, req)
err = p.accountRepo.Update(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to update account: %w", err)
}
return mappers.AccountEntityToResponse(entity), nil
}
func (p *AccountProcessorImpl) DeleteAccount(ctx context.Context, id uuid.UUID) error {
entity, err := p.accountRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("account not found: %w", err)
}
// Prevent deletion of system accounts
if entity.IsSystem {
return fmt.Errorf("cannot delete system account")
}
err = p.accountRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete account: %w", err)
}
return nil
}
func (p *AccountProcessorImpl) ListAccounts(ctx context.Context, req *models.ListAccountsRequest) ([]models.AccountResponse, int, error) {
// Get organization and outlet from context
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var outletID *uuid.UUID
if appCtx.OutletID != uuid.Nil {
outletID = &appCtx.OutletID
}
filterEntity := &entities.Account{
OrganizationID: organizationID,
OutletID: outletID,
ChartOfAccountID: *req.ChartOfAccountID,
AccountType: entities.AccountType(*req.AccountType),
}
entities, total, err := p.accountRepo.List(ctx, filterEntity)
if err != nil {
return nil, 0, fmt.Errorf("failed to list accounts: %w", err)
}
responses := make([]models.AccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.AccountEntityToResponse(entity)
}
return responses, total, nil
}
func (p *AccountProcessorImpl) GetAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.AccountResponse, error) {
entities, err := p.accountRepo.GetByOrganization(ctx, organizationID, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get accounts by organization: %w", err)
}
responses := make([]models.AccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.AccountEntityToResponse(entity)
}
return responses, nil
}
func (p *AccountProcessorImpl) GetAccountsByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]models.AccountResponse, error) {
entities, err := p.accountRepo.GetByChartOfAccount(ctx, chartOfAccountID)
if err != nil {
return nil, fmt.Errorf("failed to get accounts by chart of account: %w", err)
}
responses := make([]models.AccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.AccountEntityToResponse(entity)
}
return responses, nil
}
func (p *AccountProcessorImpl) UpdateAccountBalance(ctx context.Context, id uuid.UUID, amount float64) error {
_, err := p.accountRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("account not found: %w", err)
}
err = p.accountRepo.UpdateBalance(ctx, id, amount)
if err != nil {
return fmt.Errorf("failed to update account balance: %w", err)
}
return nil
}
func (p *AccountProcessorImpl) GetAccountBalance(ctx context.Context, id uuid.UUID) (float64, error) {
balance, err := p.accountRepo.GetBalance(ctx, id)
if err != nil {
return 0, fmt.Errorf("failed to get account balance: %w", err)
}
return balance, nil
}
@@ -0,0 +1,206 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/appcontext"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type ChartOfAccountProcessor interface {
CreateChartOfAccount(ctx context.Context, req *models.CreateChartOfAccountRequest) (*models.ChartOfAccountResponse, error)
GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountResponse, error)
UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountRequest) (*models.ChartOfAccountResponse, error)
DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error
ListChartOfAccounts(ctx context.Context, req *models.ListChartOfAccountsRequest) ([]models.ChartOfAccountResponse, int, error)
GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error)
GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error)
CreateDefaultChartOfAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) error
}
type ChartOfAccountProcessorImpl struct {
chartOfAccountRepo ChartOfAccountRepository
chartOfAccountTypeRepo ChartOfAccountTypeRepository
}
func NewChartOfAccountProcessorImpl(chartOfAccountRepo ChartOfAccountRepository, chartOfAccountTypeRepo ChartOfAccountTypeRepository) *ChartOfAccountProcessorImpl {
return &ChartOfAccountProcessorImpl{
chartOfAccountRepo: chartOfAccountRepo,
chartOfAccountTypeRepo: chartOfAccountTypeRepo,
}
}
func (p *ChartOfAccountProcessorImpl) CreateChartOfAccount(ctx context.Context, req *models.CreateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) {
// Get organization and outlet from context
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var outletID *uuid.UUID
if appCtx.OutletID != uuid.Nil {
outletID = &appCtx.OutletID
}
// Check if code already exists for this organization/outlet
existing, err := p.chartOfAccountRepo.GetByCode(ctx, organizationID, req.Code, outletID)
if err == nil && existing != nil {
return nil, fmt.Errorf("chart of account with code %s already exists", req.Code)
}
// Validate parent exists if provided
if req.ParentID != nil {
_, err := p.chartOfAccountRepo.GetByID(ctx, *req.ParentID)
if err != nil {
return nil, fmt.Errorf("parent chart of account not found: %w", err)
}
}
// Validate chart of account type exists
_, err = p.chartOfAccountTypeRepo.GetByID(ctx, req.ChartOfAccountTypeID)
if err != nil {
return nil, fmt.Errorf("chart of account type not found: %w", err)
}
entity := mappers.ChartOfAccountCreateRequestToEntity(req, organizationID, outletID)
err = p.chartOfAccountRepo.Create(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to create chart of account: %w", err)
}
return mappers.ChartOfAccountEntityToResponse(entity), nil
}
func (p *ChartOfAccountProcessorImpl) GetChartOfAccountByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountResponse, error) {
entity, err := p.chartOfAccountRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("chart of account not found: %w", err)
}
return mappers.ChartOfAccountEntityToResponse(entity), nil
}
func (p *ChartOfAccountProcessorImpl) UpdateChartOfAccount(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountRequest) (*models.ChartOfAccountResponse, error) {
entity, err := p.chartOfAccountRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("chart of account not found: %w", err)
}
// Check if new code already exists (if code is being updated)
if req.Code != nil && *req.Code != entity.Code {
existing, err := p.chartOfAccountRepo.GetByCode(ctx, entity.OrganizationID, *req.Code, entity.OutletID)
if err == nil && existing != nil {
return nil, fmt.Errorf("chart of account with code %s already exists", *req.Code)
}
}
// Validate parent exists if provided
if req.ParentID != nil {
_, err := p.chartOfAccountRepo.GetByID(ctx, *req.ParentID)
if err != nil {
return nil, fmt.Errorf("parent chart of account not found: %w", err)
}
}
// Validate chart of account type exists if provided
if req.ChartOfAccountTypeID != nil {
_, err = p.chartOfAccountTypeRepo.GetByID(ctx, *req.ChartOfAccountTypeID)
if err != nil {
return nil, fmt.Errorf("chart of account type not found: %w", err)
}
}
mappers.ChartOfAccountUpdateRequestToEntity(entity, req)
err = p.chartOfAccountRepo.Update(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to update chart of account: %w", err)
}
return mappers.ChartOfAccountEntityToResponse(entity), nil
}
func (p *ChartOfAccountProcessorImpl) DeleteChartOfAccount(ctx context.Context, id uuid.UUID) error {
entity, err := p.chartOfAccountRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("chart of account not found: %w", err)
}
// Prevent deletion of system accounts
if entity.IsSystem {
return fmt.Errorf("cannot delete system chart of account")
}
err = p.chartOfAccountRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete chart of account: %w", err)
}
return nil
}
func (p *ChartOfAccountProcessorImpl) ListChartOfAccounts(ctx context.Context, req *models.ListChartOfAccountsRequest) ([]models.ChartOfAccountResponse, int, error) {
// Get organization and outlet from context
appCtx := appcontext.FromGinContext(ctx)
organizationID := appCtx.OrganizationID
var outletID *uuid.UUID
if appCtx.OutletID != uuid.Nil {
outletID = &appCtx.OutletID
}
filterEntity := &entities.ChartOfAccount{
OrganizationID: organizationID,
OutletID: outletID,
ChartOfAccountTypeID: *req.ChartOfAccountTypeID,
ParentID: req.ParentID,
}
entities, total, err := p.chartOfAccountRepo.List(ctx, filterEntity)
if err != nil {
return nil, 0, fmt.Errorf("failed to list chart of accounts: %w", err)
}
responses := make([]models.ChartOfAccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.ChartOfAccountEntityToResponse(entity)
}
return responses, total, nil
}
func (p *ChartOfAccountProcessorImpl) GetChartOfAccountsByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) {
entities, err := p.chartOfAccountRepo.GetByOrganization(ctx, organizationID, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get chart of accounts by organization: %w", err)
}
responses := make([]models.ChartOfAccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.ChartOfAccountEntityToResponse(entity)
}
return responses, nil
}
func (p *ChartOfAccountProcessorImpl) GetChartOfAccountsByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]models.ChartOfAccountResponse, error) {
entities, err := p.chartOfAccountRepo.GetByType(ctx, organizationID, chartOfAccountTypeID, outletID)
if err != nil {
return nil, fmt.Errorf("failed to get chart of accounts by type: %w", err)
}
responses := make([]models.ChartOfAccountResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.ChartOfAccountEntityToResponse(entity)
}
return responses, nil
}
func (p *ChartOfAccountProcessorImpl) CreateDefaultChartOfAccounts(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) error {
// This method will be implemented to create default chart of accounts
// based on the JSON template when a new organization/outlet is created
// For now, we'll return nil as this is a placeholder
return nil
}
@@ -0,0 +1,106 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type ChartOfAccountTypeProcessor interface {
CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error)
GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error)
UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error)
DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error
ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ChartOfAccountTypeResponse, int, error)
}
type ChartOfAccountTypeProcessorImpl struct {
chartOfAccountTypeRepo ChartOfAccountTypeRepository
}
func NewChartOfAccountTypeProcessorImpl(chartOfAccountTypeRepo ChartOfAccountTypeRepository) *ChartOfAccountTypeProcessorImpl {
return &ChartOfAccountTypeProcessorImpl{
chartOfAccountTypeRepo: chartOfAccountTypeRepo,
}
}
func (p *ChartOfAccountTypeProcessorImpl) CreateChartOfAccountType(ctx context.Context, req *models.CreateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) {
// Check if code already exists
existing, err := p.chartOfAccountTypeRepo.GetByCode(ctx, req.Code)
if err == nil && existing != nil {
return nil, fmt.Errorf("chart of account type with code %s already exists", req.Code)
}
entity := mappers.ChartOfAccountTypeCreateRequestToEntity(req)
err = p.chartOfAccountTypeRepo.Create(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to create chart of account type: %w", err)
}
return mappers.ChartOfAccountTypeEntityToResponse(entity), nil
}
func (p *ChartOfAccountTypeProcessorImpl) GetChartOfAccountTypeByID(ctx context.Context, id uuid.UUID) (*models.ChartOfAccountTypeResponse, error) {
entity, err := p.chartOfAccountTypeRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("chart of account type not found: %w", err)
}
return mappers.ChartOfAccountTypeEntityToResponse(entity), nil
}
func (p *ChartOfAccountTypeProcessorImpl) UpdateChartOfAccountType(ctx context.Context, id uuid.UUID, req *models.UpdateChartOfAccountTypeRequest) (*models.ChartOfAccountTypeResponse, error) {
entity, err := p.chartOfAccountTypeRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("chart of account type not found: %w", err)
}
// Check if new code already exists (if code is being updated)
if req.Code != nil && *req.Code != entity.Code {
existing, err := p.chartOfAccountTypeRepo.GetByCode(ctx, *req.Code)
if err == nil && existing != nil {
return nil, fmt.Errorf("chart of account type with code %s already exists", *req.Code)
}
}
mappers.ChartOfAccountTypeUpdateRequestToEntity(entity, req)
err = p.chartOfAccountTypeRepo.Update(ctx, entity)
if err != nil {
return nil, fmt.Errorf("failed to update chart of account type: %w", err)
}
return mappers.ChartOfAccountTypeEntityToResponse(entity), nil
}
func (p *ChartOfAccountTypeProcessorImpl) DeleteChartOfAccountType(ctx context.Context, id uuid.UUID) error {
_, err := p.chartOfAccountTypeRepo.GetByID(ctx, id)
if err != nil {
return fmt.Errorf("chart of account type not found: %w", err)
}
err = p.chartOfAccountTypeRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete chart of account type: %w", err)
}
return nil
}
func (p *ChartOfAccountTypeProcessorImpl) ListChartOfAccountTypes(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.ChartOfAccountTypeResponse, int, error) {
entities, total, err := p.chartOfAccountTypeRepo.List(ctx, filters, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to list chart of account types: %w", err)
}
responses := make([]models.ChartOfAccountTypeResponse, len(entities))
for i, entity := range entities {
responses[i] = *mappers.ChartOfAccountTypeEntityToResponse(entity)
}
return responses, total, nil
}
@@ -0,0 +1,259 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type IngredientUnitConverterRepository interface {
Create(ctx context.Context, converter *entities.IngredientUnitConverter) error
GetByID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error)
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error)
Update(ctx context.Context, converter *entities.IngredientUnitConverter) error
Delete(ctx context.Context, id, organizationID uuid.UUID) error
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*entities.IngredientUnitConverter, int, error)
GetByIngredientAndUnits(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID) (*entities.IngredientUnitConverter, error)
GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error)
GetActiveConverters(ctx context.Context, organizationID uuid.UUID) ([]*entities.IngredientUnitConverter, error)
ConvertQuantity(ctx context.Context, ingredientID, fromUnitID, toUnitID, organizationID uuid.UUID, quantity float64) (float64, error)
}
type IngredientUnitConverterProcessor interface {
CreateIngredientUnitConverter(ctx context.Context, organizationID, userID uuid.UUID, req *models.CreateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error)
UpdateIngredientUnitConverter(ctx context.Context, id, organizationID, userID uuid.UUID, req *models.UpdateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error)
DeleteIngredientUnitConverter(ctx context.Context, id, organizationID uuid.UUID) error
GetIngredientUnitConverterByID(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientUnitConverterResponse, error)
ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error)
GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error)
ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error)
}
type IngredientUnitConverterProcessorImpl struct {
converterRepo IngredientUnitConverterRepository
ingredientRepo IngredientRepository
unitRepo UnitRepository
}
func NewIngredientUnitConverterProcessorImpl(
converterRepo IngredientUnitConverterRepository,
ingredientRepo IngredientRepository,
unitRepo UnitRepository,
) *IngredientUnitConverterProcessorImpl {
return &IngredientUnitConverterProcessorImpl{
converterRepo: converterRepo,
ingredientRepo: ingredientRepo,
unitRepo: unitRepo,
}
}
func (p *IngredientUnitConverterProcessorImpl) CreateIngredientUnitConverter(ctx context.Context, organizationID, userID uuid.UUID, req *models.CreateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) {
// Validate ingredient exists
_, err := p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient not found: %w", err)
}
// Validate units exist
_, err = p.unitRepo.GetByID(ctx, req.FromUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("from unit not found: %w", err)
}
_, err = p.unitRepo.GetByID(ctx, req.ToUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("to unit not found: %w", err)
}
// Check if converter already exists
existingConverter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID)
if err == nil && existingConverter != nil {
return nil, fmt.Errorf("converter already exists for this ingredient and unit combination")
}
// Set default values
isActive := true
if req.IsActive != nil {
isActive = *req.IsActive
}
// Create entity
converter := &entities.IngredientUnitConverter{
OrganizationID: organizationID,
IngredientID: req.IngredientID,
FromUnitID: req.FromUnitID,
ToUnitID: req.ToUnitID,
ConversionFactor: req.ConversionFactor,
IsActive: isActive,
CreatedBy: userID,
UpdatedBy: userID,
}
err = p.converterRepo.Create(ctx, converter)
if err != nil {
return nil, fmt.Errorf("failed to create ingredient unit converter: %w", err)
}
// Get the created converter with relationships
createdConverter, err := p.converterRepo.GetByID(ctx, converter.ID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get created converter: %w", err)
}
return mappers.IngredientUnitConverterEntityToResponse(createdConverter), nil
}
func (p *IngredientUnitConverterProcessorImpl) UpdateIngredientUnitConverter(ctx context.Context, id, organizationID, userID uuid.UUID, req *models.UpdateIngredientUnitConverterRequest) (*models.IngredientUnitConverterResponse, error) {
// Get existing converter
converter, err := p.converterRepo.GetByID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient unit converter not found: %w", err)
}
// Update fields if provided
if req.FromUnitID != nil {
// Validate new unit exists
_, err = p.unitRepo.GetByID(ctx, *req.FromUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("from unit not found: %w", err)
}
converter.FromUnitID = *req.FromUnitID
}
if req.ToUnitID != nil {
// Validate new unit exists
_, err = p.unitRepo.GetByID(ctx, *req.ToUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("to unit not found: %w", err)
}
converter.ToUnitID = *req.ToUnitID
}
if req.ConversionFactor != nil {
converter.ConversionFactor = *req.ConversionFactor
}
if req.IsActive != nil {
converter.IsActive = *req.IsActive
}
converter.UpdatedBy = userID
err = p.converterRepo.Update(ctx, converter)
if err != nil {
return nil, fmt.Errorf("failed to update ingredient unit converter: %w", err)
}
// Get the updated converter with relationships
updatedConverter, err := p.converterRepo.GetByID(ctx, converter.ID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get updated converter: %w", err)
}
return mappers.IngredientUnitConverterEntityToResponse(updatedConverter), nil
}
func (p *IngredientUnitConverterProcessorImpl) DeleteIngredientUnitConverter(ctx context.Context, id, organizationID uuid.UUID) error {
// Check if converter exists
_, err := p.converterRepo.GetByID(ctx, id, organizationID)
if err != nil {
return fmt.Errorf("ingredient unit converter not found: %w", err)
}
err = p.converterRepo.Delete(ctx, id, organizationID)
if err != nil {
return fmt.Errorf("failed to delete ingredient unit converter: %w", err)
}
return nil
}
func (p *IngredientUnitConverterProcessorImpl) GetIngredientUnitConverterByID(ctx context.Context, id, organizationID uuid.UUID) (*models.IngredientUnitConverterResponse, error) {
converter, err := p.converterRepo.GetByID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient unit converter not found: %w", err)
}
return mappers.IngredientUnitConverterEntityToResponse(converter), nil
}
func (p *IngredientUnitConverterProcessorImpl) ListIngredientUnitConverters(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.IngredientUnitConverterResponse, int, error) {
converters, total, err := p.converterRepo.List(ctx, organizationID, filters, page, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to list ingredient unit converters: %w", err)
}
responses := make([]*models.IngredientUnitConverterResponse, len(converters))
for i, converter := range converters {
responses[i] = mappers.IngredientUnitConverterEntityToResponse(converter)
}
return responses, total, nil
}
func (p *IngredientUnitConverterProcessorImpl) GetConvertersForIngredient(ctx context.Context, ingredientID, organizationID uuid.UUID) ([]*models.IngredientUnitConverterResponse, error) {
converters, err := p.converterRepo.GetConvertersForIngredient(ctx, ingredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get converters for ingredient: %w", err)
}
responses := make([]*models.IngredientUnitConverterResponse, len(converters))
for i, converter := range converters {
responses[i] = mappers.IngredientUnitConverterEntityToResponse(converter)
}
return responses, nil
}
func (p *IngredientUnitConverterProcessorImpl) ConvertUnit(ctx context.Context, organizationID uuid.UUID, req *models.ConvertUnitRequest) (*models.ConvertUnitResponse, error) {
// Get ingredient and units for response
ingredient, err := p.ingredientRepo.GetByID(ctx, req.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient not found: %w", err)
}
fromUnit, err := p.unitRepo.GetByID(ctx, req.FromUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("from unit not found: %w", err)
}
toUnit, err := p.unitRepo.GetByID(ctx, req.ToUnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("to unit not found: %w", err)
}
// Convert quantity
convertedQuantity, err := p.converterRepo.ConvertQuantity(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID, req.Quantity)
if err != nil {
return nil, fmt.Errorf("failed to convert quantity: %w", err)
}
// Get conversion factor for response
converter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.FromUnitID, req.ToUnitID, organizationID)
var conversionFactor float64
if err == nil {
conversionFactor = converter.ConversionFactor
} else {
// Try reverse converter
reverseConverter, err := p.converterRepo.GetByIngredientAndUnits(ctx, req.IngredientID, req.ToUnitID, req.FromUnitID, organizationID)
if err == nil {
conversionFactor = 1.0 / reverseConverter.ConversionFactor
}
}
response := &models.ConvertUnitResponse{
FromQuantity: req.Quantity,
FromUnit: mappers.MapUnitEntityToResponse(fromUnit),
ToQuantity: convertedQuantity,
ToUnit: mappers.MapUnitEntityToResponse(toUnit),
ConversionFactor: conversionFactor,
Ingredient: mappers.MapIngredientEntityToResponse(ingredient),
}
return response, nil
}
@@ -0,0 +1,441 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type PurchaseOrderProcessor interface {
CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error
GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error)
ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error)
GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error)
GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error)
UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error)
}
type PurchaseOrderProcessorImpl struct {
purchaseOrderRepo PurchaseOrderRepository
vendorRepo VendorRepository
ingredientRepo IngredientRepository
unitRepo UnitRepository
fileRepo FileRepository
inventoryMovementService InventoryMovementService
unitConverterRepo IngredientUnitConverterRepository
}
func NewPurchaseOrderProcessorImpl(
purchaseOrderRepo PurchaseOrderRepository,
vendorRepo VendorRepository,
ingredientRepo IngredientRepository,
unitRepo UnitRepository,
fileRepo FileRepository,
inventoryMovementService InventoryMovementService,
unitConverterRepo IngredientUnitConverterRepository,
) *PurchaseOrderProcessorImpl {
return &PurchaseOrderProcessorImpl{
purchaseOrderRepo: purchaseOrderRepo,
vendorRepo: vendorRepo,
ingredientRepo: ingredientRepo,
unitRepo: unitRepo,
fileRepo: fileRepo,
inventoryMovementService: inventoryMovementService,
unitConverterRepo: unitConverterRepo,
}
}
func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
// Check if vendor exists and belongs to organization
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, req.VendorID, organizationID)
if err != nil {
return nil, fmt.Errorf("vendor not found: %w", err)
}
// Check if PO number already exists in organization
existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, req.PONumber, organizationID)
if err == nil && existingPO != nil {
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
}
// Validate ingredients and units exist
for i, item := range req.Items {
_, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
}
_, err = p.unitRepo.GetByID(ctx, item.UnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
}
}
// Calculate total amount
totalAmount := 0.0
for _, item := range req.Items {
totalAmount += item.Amount
}
// Create purchase order entity
poEntity := &entities.PurchaseOrder{
OrganizationID: organizationID,
VendorID: req.VendorID,
PONumber: req.PONumber,
TransactionDate: req.TransactionDate,
DueDate: req.DueDate,
Reference: req.Reference,
Status: "draft", // Default status
Message: req.Message,
TotalAmount: totalAmount,
}
if req.Status != nil {
poEntity.Status = *req.Status
}
// Create purchase order
err = p.purchaseOrderRepo.Create(ctx, poEntity)
if err != nil {
return nil, fmt.Errorf("failed to create purchase order: %w", err)
}
// Create purchase order items
for _, itemReq := range req.Items {
itemEntity := &entities.PurchaseOrderItem{
PurchaseOrderID: poEntity.ID,
IngredientID: itemReq.IngredientID,
Description: itemReq.Description,
Quantity: itemReq.Quantity,
UnitID: itemReq.UnitID,
Amount: itemReq.Amount,
}
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
}
}
// Create attachments if provided
for _, fileID := range req.AttachmentFileIDs {
attachmentEntity := &entities.PurchaseOrderAttachment{
PurchaseOrderID: poEntity.ID,
FileID: fileID,
}
err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity)
if err != nil {
return nil, fmt.Errorf("failed to create purchase order attachment: %w", err)
}
}
// Get the created purchase order with all relations
createdPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to get created purchase order: %w", err)
}
return mappers.PurchaseOrderEntityToResponse(createdPO), nil
}
func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
// Get existing purchase order
poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("purchase order not found: %w", err)
}
// Check if vendor exists and belongs to organization (if vendor is being updated)
if req.VendorID != nil {
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID)
if err != nil {
return nil, fmt.Errorf("vendor not found: %w", err)
}
poEntity.VendorID = *req.VendorID
}
// Check if PO number already exists (if PO number is being updated)
if req.PONumber != nil && *req.PONumber != poEntity.PONumber {
existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, *req.PONumber, organizationID)
if err == nil && existingPO != nil {
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", *req.PONumber)
}
poEntity.PONumber = *req.PONumber
}
// Update other fields
if req.TransactionDate != nil {
poEntity.TransactionDate = *req.TransactionDate
}
if req.DueDate != nil {
poEntity.DueDate = *req.DueDate
}
if req.Reference != nil {
poEntity.Reference = req.Reference
}
if req.Status != nil {
poEntity.Status = *req.Status
}
if req.Message != nil {
poEntity.Message = req.Message
}
// Update items if provided
if req.Items != nil {
// Delete existing items
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to delete existing items: %w", err)
}
// Create new items
totalAmount := 0.0
for _, itemReq := range req.Items {
// Validate ingredients and units exist
if itemReq.IngredientID != nil {
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("ingredient not found: %w", err)
}
}
if itemReq.UnitID != nil {
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
if err != nil {
return nil, fmt.Errorf("unit not found: %w", err)
}
}
// Use existing values if not provided
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
unitID := poEntity.Items[0].UnitID
quantity := poEntity.Items[0].Quantity
amount := poEntity.Items[0].Amount
description := poEntity.Items[0].Description
if itemReq.IngredientID != nil {
ingredientID = *itemReq.IngredientID
}
if itemReq.UnitID != nil {
unitID = *itemReq.UnitID
}
if itemReq.Quantity != nil {
quantity = *itemReq.Quantity
}
if itemReq.Amount != nil {
amount = *itemReq.Amount
}
if itemReq.Description != nil {
description = itemReq.Description
}
itemEntity := &entities.PurchaseOrderItem{
PurchaseOrderID: poEntity.ID,
IngredientID: ingredientID,
Description: description,
Quantity: quantity,
UnitID: unitID,
Amount: amount,
}
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
if err != nil {
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
}
totalAmount += amount
}
poEntity.TotalAmount = totalAmount
}
// Update attachments if provided
if req.AttachmentFileIDs != nil {
// Delete existing attachments
err = p.purchaseOrderRepo.DeleteAttachmentsByPurchaseOrderID(ctx, poEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to delete existing attachments: %w", err)
}
// Create new attachments
for _, fileID := range req.AttachmentFileIDs {
attachmentEntity := &entities.PurchaseOrderAttachment{
PurchaseOrderID: poEntity.ID,
FileID: fileID,
}
err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity)
if err != nil {
return nil, fmt.Errorf("failed to create purchase order attachment: %w", err)
}
}
}
// Update purchase order
err = p.purchaseOrderRepo.Update(ctx, poEntity)
if err != nil {
return nil, fmt.Errorf("failed to update purchase order: %w", err)
}
// Get the updated purchase order with all relations
updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID)
if err != nil {
return nil, fmt.Errorf("failed to get updated purchase order: %w", err)
}
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
}
func (p *PurchaseOrderProcessorImpl) DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error {
_, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return fmt.Errorf("purchase order not found: %w", err)
}
err = p.purchaseOrderRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete purchase order: %w", err)
}
return nil
}
func (p *PurchaseOrderProcessorImpl) GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error) {
poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("purchase order not found: %w", err)
}
return mappers.PurchaseOrderEntityToResponse(poEntity), nil
}
func (p *PurchaseOrderProcessorImpl) ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error) {
offset := (page - 1) * limit
poEntities, total, err := p.purchaseOrderRepo.List(ctx, organizationID, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list purchase orders: %w", err)
}
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
for i, poEntity := range poEntities {
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
return poResponses, totalPages, nil
}
func (p *PurchaseOrderProcessorImpl) GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error) {
poEntities, err := p.purchaseOrderRepo.GetByStatus(ctx, organizationID, status)
if err != nil {
return nil, fmt.Errorf("failed to get purchase orders by status: %w", err)
}
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
for i, poEntity := range poEntities {
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
}
return poResponses, nil
}
func (p *PurchaseOrderProcessorImpl) GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error) {
poEntities, err := p.purchaseOrderRepo.GetOverdue(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get overdue purchase orders: %w", err)
}
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
for i, poEntity := range poEntities {
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
}
return poResponses, nil
}
func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error) {
// Get the purchase order with items to check current status
po, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("purchase order not found: %w", err)
}
// Check if status is changing to "received" and current status is not "received"
if status == "received" && po.Status != "received" {
// Get purchase order with items for inventory update
poWithItems, err := p.purchaseOrderRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get purchase order with items: %w", err)
}
// Update inventory for each item
for _, item := range poWithItems.Items {
// Get ingredient to find its base unit
ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get ingredient %s: %w", item.IngredientID, err)
}
// Convert quantity to ingredient's base unit if needed
quantityToAdd := item.Quantity
if item.UnitID != ingredient.UnitID {
// Convert from purchase unit to ingredient's base unit
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, item.IngredientID, item.UnitID, ingredient.UnitID, organizationID, item.Quantity)
if err != nil {
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", item.IngredientID, item.UnitID, ingredient.UnitID, err)
}
quantityToAdd = convertedQuantity
}
// Calculate unit cost in ingredient's base unit
unitCost := 0.0
if quantityToAdd > 0 {
unitCost = item.Amount / quantityToAdd
}
// Create inventory movement for ingredient purchase
reason := fmt.Sprintf("Purchase order %s received", po.PONumber)
referenceType := entities.InventoryMovementReferenceTypePurchaseOrder
referenceID := &id
err = p.inventoryMovementService.CreateIngredientMovement(
ctx,
item.IngredientID,
organizationID,
outletID,
userID,
entities.InventoryMovementTypePurchase,
quantityToAdd,
unitCost,
reason,
&referenceType,
referenceID,
)
if err != nil {
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
}
}
}
// Update the purchase order status
err = p.purchaseOrderRepo.UpdateStatus(ctx, id, status)
if err != nil {
return nil, fmt.Errorf("failed to update purchase order status: %w", err)
}
// Get the updated purchase order
updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to get updated purchase order: %w", err)
}
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
}
@@ -0,0 +1,32 @@
package processor
import (
"apskel-pos-be/internal/entities"
"context"
"github.com/google/uuid"
)
type PurchaseOrderRepository interface {
Create(ctx context.Context, po *entities.PurchaseOrder) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.PurchaseOrder, error)
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.PurchaseOrder, error)
Update(ctx context.Context, po *entities.PurchaseOrder) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.PurchaseOrder, int64, error)
Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error)
GetByPONumber(ctx context.Context, poNumber string, organizationID uuid.UUID) (*entities.PurchaseOrder, error)
GetByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*entities.PurchaseOrder, error)
GetOverdue(ctx context.Context, organizationID uuid.UUID) ([]*entities.PurchaseOrder, error)
UpdateStatus(ctx context.Context, id uuid.UUID, status string) error
UpdateTotalAmount(ctx context.Context, id uuid.UUID, totalAmount float64) error
CreateItem(ctx context.Context, item *entities.PurchaseOrderItem) error
UpdateItem(ctx context.Context, item *entities.PurchaseOrderItem) error
DeleteItem(ctx context.Context, id uuid.UUID) error
DeleteItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error
GetItemsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderItem, error)
CreateAttachment(ctx context.Context, attachment *entities.PurchaseOrderAttachment) error
DeleteAttachment(ctx context.Context, id uuid.UUID) error
DeleteAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) error
GetAttachmentsByPurchaseOrderID(ctx context.Context, purchaseOrderID uuid.UUID) ([]*entities.PurchaseOrderAttachment, error)
}
@@ -0,0 +1,44 @@
package processor
import (
"context"
"apskel-pos-be/internal/entities"
"github.com/google/uuid"
)
// Repository interfaces for processors
type ChartOfAccountTypeRepository interface {
Create(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccountType, error)
GetByCode(ctx context.Context, code string) (*entities.ChartOfAccountType, error)
Update(ctx context.Context, chartOfAccountType *entities.ChartOfAccountType) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, filters map[string]interface{}, page, limit int) ([]*entities.ChartOfAccountType, int, error)
GetActive(ctx context.Context) ([]*entities.ChartOfAccountType, error)
}
type ChartOfAccountRepository interface {
Create(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.ChartOfAccount, error)
Update(ctx context.Context, chartOfAccount *entities.ChartOfAccount) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, req *entities.ChartOfAccount) ([]*entities.ChartOfAccount, int, error)
GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error)
GetByType(ctx context.Context, organizationID uuid.UUID, chartOfAccountTypeID uuid.UUID, outletID *uuid.UUID) ([]*entities.ChartOfAccount, error)
GetByCode(ctx context.Context, organizationID uuid.UUID, code string, outletID *uuid.UUID) (*entities.ChartOfAccount, error)
}
type AccountRepository interface {
Create(ctx context.Context, account *entities.Account) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.Account, error)
Update(ctx context.Context, account *entities.Account) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, req *entities.Account) ([]*entities.Account, int, error)
GetByOrganization(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) ([]*entities.Account, error)
GetByChartOfAccount(ctx context.Context, chartOfAccountID uuid.UUID) ([]*entities.Account, error)
GetByNumber(ctx context.Context, organizationID uuid.UUID, number string, outletID *uuid.UUID) (*entities.Account, error)
UpdateBalance(ctx context.Context, id uuid.UUID, amount float64) error
GetBalance(ctx context.Context, id uuid.UUID) (float64, error)
}
+177
View File
@@ -0,0 +1,177 @@
package processor
import (
"context"
"fmt"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/mappers"
"apskel-pos-be/internal/models"
"github.com/google/uuid"
)
type VendorProcessor interface {
CreateVendor(ctx context.Context, organizationID uuid.UUID, req *models.CreateVendorRequest) (*models.VendorResponse, error)
UpdateVendor(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateVendorRequest) (*models.VendorResponse, error)
DeleteVendor(ctx context.Context, id, organizationID uuid.UUID) error
GetVendorByID(ctx context.Context, id, organizationID uuid.UUID) (*models.VendorResponse, error)
ListVendors(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.VendorResponse, int, error)
GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*models.VendorResponse, error)
}
type VendorProcessorImpl struct {
vendorRepo VendorRepository
}
func NewVendorProcessorImpl(vendorRepo VendorRepository) *VendorProcessorImpl {
return &VendorProcessorImpl{
vendorRepo: vendorRepo,
}
}
func (p *VendorProcessorImpl) CreateVendor(ctx context.Context, organizationID uuid.UUID, req *models.CreateVendorRequest) (*models.VendorResponse, error) {
// Check if vendor with same name already exists in organization
if req.Name != "" {
existingVendor, err := p.vendorRepo.GetByName(ctx, req.Name, organizationID)
if err == nil && existingVendor != nil {
return nil, fmt.Errorf("vendor with name %s already exists in this organization", req.Name)
}
}
// Check if vendor with same email already exists in organization
if req.Email != nil && *req.Email != "" {
existingVendor, err := p.vendorRepo.GetByEmail(ctx, *req.Email, organizationID)
if err == nil && existingVendor != nil {
return nil, fmt.Errorf("vendor with email %s already exists in this organization", *req.Email)
}
}
vendorEntity := &entities.Vendor{
OrganizationID: organizationID,
Name: req.Name,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
Address: req.Address,
ContactPerson: req.ContactPerson,
TaxNumber: req.TaxNumber,
PaymentTerms: req.PaymentTerms,
Notes: req.Notes,
IsActive: true, // Default to active
}
if req.IsActive != nil {
vendorEntity.IsActive = *req.IsActive
}
err := p.vendorRepo.Create(ctx, vendorEntity)
if err != nil {
return nil, fmt.Errorf("failed to create vendor: %w", err)
}
return mappers.VendorEntityToResponse(vendorEntity), nil
}
func (p *VendorProcessorImpl) UpdateVendor(ctx context.Context, id, organizationID uuid.UUID, req *models.UpdateVendorRequest) (*models.VendorResponse, error) {
vendorEntity, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("vendor not found: %w", err)
}
// Check if vendor with same name already exists (excluding current vendor)
if req.Name != nil && *req.Name != "" && *req.Name != vendorEntity.Name {
existingVendor, err := p.vendorRepo.GetByName(ctx, *req.Name, organizationID)
if err == nil && existingVendor != nil && existingVendor.ID != id {
return nil, fmt.Errorf("vendor with name %s already exists in this organization", *req.Name)
}
}
// Check if vendor with same email already exists (excluding current vendor)
if req.Email != nil && *req.Email != "" && (vendorEntity.Email == nil || *req.Email != *vendorEntity.Email) {
existingVendor, err := p.vendorRepo.GetByEmail(ctx, *req.Email, organizationID)
if err == nil && existingVendor != nil && existingVendor.ID != id {
return nil, fmt.Errorf("vendor with email %s already exists in this organization", *req.Email)
}
}
// Update fields
if req.Name != nil {
vendorEntity.Name = *req.Name
}
if req.Email != nil {
vendorEntity.Email = req.Email
}
if req.PhoneNumber != nil {
vendorEntity.PhoneNumber = req.PhoneNumber
}
if req.Address != nil {
vendorEntity.Address = req.Address
}
if req.ContactPerson != nil {
vendorEntity.ContactPerson = req.ContactPerson
}
if req.TaxNumber != nil {
vendorEntity.TaxNumber = req.TaxNumber
}
if req.PaymentTerms != nil {
vendorEntity.PaymentTerms = req.PaymentTerms
}
if req.Notes != nil {
vendorEntity.Notes = req.Notes
}
if req.IsActive != nil {
vendorEntity.IsActive = *req.IsActive
}
err = p.vendorRepo.Update(ctx, vendorEntity)
if err != nil {
return nil, fmt.Errorf("failed to update vendor: %w", err)
}
return mappers.VendorEntityToResponse(vendorEntity), nil
}
func (p *VendorProcessorImpl) DeleteVendor(ctx context.Context, id, organizationID uuid.UUID) error {
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return fmt.Errorf("vendor not found: %w", err)
}
err = p.vendorRepo.Delete(ctx, id)
if err != nil {
return fmt.Errorf("failed to delete vendor: %w", err)
}
return nil
}
func (p *VendorProcessorImpl) GetVendorByID(ctx context.Context, id, organizationID uuid.UUID) (*models.VendorResponse, error) {
vendorEntity, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
if err != nil {
return nil, fmt.Errorf("vendor not found: %w", err)
}
return mappers.VendorEntityToResponse(vendorEntity), nil
}
func (p *VendorProcessorImpl) ListVendors(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.VendorResponse, int, error) {
offset := (page - 1) * limit
vendorEntities, total, err := p.vendorRepo.List(ctx, organizationID, filters, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to list vendors: %w", err)
}
vendorResponses := mappers.VendorEntitiesToResponses(vendorEntities)
totalPages := int((total + int64(limit) - 1) / int64(limit))
return vendorResponses, totalPages, nil
}
func (p *VendorProcessorImpl) GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*models.VendorResponse, error) {
vendorEntities, err := p.vendorRepo.GetActiveVendors(ctx, organizationID)
if err != nil {
return nil, fmt.Errorf("failed to get active vendors: %w", err)
}
return mappers.VendorEntitiesToResponses(vendorEntities), nil
}
+21
View File
@@ -0,0 +1,21 @@
package processor
import (
"apskel-pos-be/internal/entities"
"context"
"github.com/google/uuid"
)
type VendorRepository interface {
Create(ctx context.Context, vendor *entities.Vendor) error
GetByID(ctx context.Context, id uuid.UUID) (*entities.Vendor, error)
GetByIDAndOrganizationID(ctx context.Context, id, organizationID uuid.UUID) (*entities.Vendor, error)
Update(ctx context.Context, vendor *entities.Vendor) error
Delete(ctx context.Context, id uuid.UUID) error
List(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, limit, offset int) ([]*entities.Vendor, int64, error)
Count(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}) (int64, error)
GetByEmail(ctx context.Context, email string, organizationID uuid.UUID) (*entities.Vendor, error)
GetByName(ctx context.Context, name string, organizationID uuid.UUID) (*entities.Vendor, error)
GetActiveVendors(ctx context.Context, organizationID uuid.UUID) ([]*entities.Vendor, error)
}