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
+98
View File
@@ -0,0 +1,98 @@
package validator
import (
"apskel-pos-be/internal/models"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type AccountValidator interface {
ValidateCreateAccount(req *models.CreateAccountRequest) error
ValidateUpdateAccount(req *models.UpdateAccountRequest) error
}
type AccountValidatorImpl struct {
validator *validator.Validate
}
func NewAccountValidator() AccountValidator {
return &AccountValidatorImpl{
validator: validator.New(),
}
}
func (v *AccountValidatorImpl) ValidateCreateAccount(req *models.CreateAccountRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if strings.TrimSpace(req.Number) == "" {
return fmt.Errorf("number cannot be empty")
}
// Validate account type
if !isValidAccountType(req.AccountType) {
return fmt.Errorf("invalid account type")
}
// Validate number format (alphanumeric)
if !isValidAccountNumberFormat(req.Number) {
return fmt.Errorf("number must be alphanumeric")
}
return nil
}
func (v *AccountValidatorImpl) ValidateUpdateAccount(req *models.UpdateAccountRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if req.Name != nil && strings.TrimSpace(*req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if req.Number != nil && strings.TrimSpace(*req.Number) == "" {
return fmt.Errorf("number cannot be empty")
}
// Validate account type if provided
if req.AccountType != nil && !isValidAccountType(*req.AccountType) {
return fmt.Errorf("invalid account type")
}
// Validate number format if provided
if req.Number != nil && !isValidAccountNumberFormat(*req.Number) {
return fmt.Errorf("number must be alphanumeric")
}
return nil
}
func isValidAccountType(accountType string) bool {
validTypes := []string{"cash", "wallet", "bank", "credit", "debit", "asset", "liability", "equity", "revenue", "expense"}
for _, validType := range validTypes {
if accountType == validType {
return true
}
}
return false
}
func isValidAccountNumberFormat(number string) bool {
// Check if number is alphanumeric
for _, char := range number {
if !((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')) {
return false
}
}
return true
}
@@ -0,0 +1,78 @@
package validator
import (
"apskel-pos-be/internal/models"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type ChartOfAccountTypeValidator interface {
ValidateCreateChartOfAccountType(req *models.CreateChartOfAccountTypeRequest) error
ValidateUpdateChartOfAccountType(req *models.UpdateChartOfAccountTypeRequest) error
}
type ChartOfAccountTypeValidatorImpl struct {
validator *validator.Validate
}
func NewChartOfAccountTypeValidator() ChartOfAccountTypeValidator {
return &ChartOfAccountTypeValidatorImpl{
validator: validator.New(),
}
}
func (v *ChartOfAccountTypeValidatorImpl) ValidateCreateChartOfAccountType(req *models.CreateChartOfAccountTypeRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if strings.TrimSpace(req.Code) == "" {
return fmt.Errorf("code cannot be empty")
}
// Validate code format (alphanumeric, uppercase)
if !isValidCodeFormat(req.Code) {
return fmt.Errorf("code must be alphanumeric and uppercase")
}
return nil
}
func (v *ChartOfAccountTypeValidatorImpl) ValidateUpdateChartOfAccountType(req *models.UpdateChartOfAccountTypeRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if req.Name != nil && strings.TrimSpace(*req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if req.Code != nil && strings.TrimSpace(*req.Code) == "" {
return fmt.Errorf("code cannot be empty")
}
// Validate code format if provided
if req.Code != nil && !isValidCodeFormat(*req.Code) {
return fmt.Errorf("code must be alphanumeric and uppercase")
}
return nil
}
func isValidCodeFormat(code string) bool {
// Check if code is alphanumeric and uppercase
for _, char := range code {
if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
return false
}
}
return true
}
@@ -0,0 +1,78 @@
package validator
import (
"apskel-pos-be/internal/models"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
)
type ChartOfAccountValidator interface {
ValidateCreateChartOfAccount(req *models.CreateChartOfAccountRequest) error
ValidateUpdateChartOfAccount(req *models.UpdateChartOfAccountRequest) error
}
type ChartOfAccountValidatorImpl struct {
validator *validator.Validate
}
func NewChartOfAccountValidator() ChartOfAccountValidator {
return &ChartOfAccountValidatorImpl{
validator: validator.New(),
}
}
func (v *ChartOfAccountValidatorImpl) ValidateCreateChartOfAccount(req *models.CreateChartOfAccountRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if strings.TrimSpace(req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if strings.TrimSpace(req.Code) == "" {
return fmt.Errorf("code cannot be empty")
}
// Validate code format (alphanumeric)
if !isValidAccountCodeFormat(req.Code) {
return fmt.Errorf("code must be alphanumeric")
}
return nil
}
func (v *ChartOfAccountValidatorImpl) ValidateUpdateChartOfAccount(req *models.UpdateChartOfAccountRequest) error {
if err := v.validator.Struct(req); err != nil {
return err
}
// Additional custom validations
if req.Name != nil && strings.TrimSpace(*req.Name) == "" {
return fmt.Errorf("name cannot be empty")
}
if req.Code != nil && strings.TrimSpace(*req.Code) == "" {
return fmt.Errorf("code cannot be empty")
}
// Validate code format if provided
if req.Code != nil && !isValidAccountCodeFormat(*req.Code) {
return fmt.Errorf("code must be alphanumeric")
}
return nil
}
func isValidAccountCodeFormat(code string) bool {
// Check if code is alphanumeric
for _, char := range code {
if !((char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9')) {
return false
}
}
return true
}
@@ -0,0 +1,122 @@
package validator
import (
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
"errors"
"strings"
)
type IngredientUnitConverterValidator interface {
ValidateCreateIngredientUnitConverterRequest(req *contract.CreateIngredientUnitConverterRequest) (error, string)
ValidateUpdateIngredientUnitConverterRequest(req *contract.UpdateIngredientUnitConverterRequest) (error, string)
ValidateListIngredientUnitConvertersRequest(req *contract.ListIngredientUnitConvertersRequest) (error, string)
ValidateConvertUnitRequest(req *contract.ConvertUnitRequest) (error, string)
}
type IngredientUnitConverterValidatorImpl struct{}
func NewIngredientUnitConverterValidator() IngredientUnitConverterValidator {
return &IngredientUnitConverterValidatorImpl{}
}
func (v *IngredientUnitConverterValidatorImpl) ValidateCreateIngredientUnitConverterRequest(req *contract.CreateIngredientUnitConverterRequest) (error, string) {
if req == nil {
return errors.New("request cannot be nil"), constants.ValidationErrorCode
}
// Validate required fields
if req.IngredientID.String() == "" {
return errors.New("ingredient_id is required"), constants.MissingFieldErrorCode
}
if req.FromUnitID.String() == "" {
return errors.New("from_unit_id is required"), constants.MissingFieldErrorCode
}
if req.ToUnitID.String() == "" {
return errors.New("to_unit_id is required"), constants.MissingFieldErrorCode
}
if req.ConversionFactor <= 0 {
return errors.New("conversion_factor must be greater than 0"), constants.ValidationErrorCode
}
// Validate that from and to units are different
if req.FromUnitID == req.ToUnitID {
return errors.New("from_unit_id and to_unit_id must be different"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *IngredientUnitConverterValidatorImpl) ValidateUpdateIngredientUnitConverterRequest(req *contract.UpdateIngredientUnitConverterRequest) (error, string) {
if req == nil {
return errors.New("request cannot be nil"), constants.ValidationErrorCode
}
// At least one field must be provided for update
if req.FromUnitID == nil && req.ToUnitID == nil && req.ConversionFactor == nil && req.IsActive == nil {
return errors.New("at least one field must be provided for update"), constants.ValidationErrorCode
}
// Validate conversion factor if provided
if req.ConversionFactor != nil && *req.ConversionFactor <= 0 {
return errors.New("conversion_factor must be greater than 0"), constants.ValidationErrorCode
}
// Validate that from and to units are different if both are provided
if req.FromUnitID != nil && req.ToUnitID != nil && *req.FromUnitID == *req.ToUnitID {
return errors.New("from_unit_id and to_unit_id must be different"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *IngredientUnitConverterValidatorImpl) ValidateListIngredientUnitConvertersRequest(req *contract.ListIngredientUnitConvertersRequest) (error, string) {
if req == nil {
return errors.New("request cannot be nil"), constants.ValidationErrorCode
}
// Validate pagination
if req.Page < 1 {
return errors.New("page must be at least 1"), constants.ValidationErrorCode
}
if req.Limit < 1 || req.Limit > 100 {
return errors.New("limit must be between 1 and 100"), constants.ValidationErrorCode
}
// Validate search string length
if req.Search != "" && len(strings.TrimSpace(req.Search)) < 2 {
return errors.New("search term must be at least 2 characters"), constants.ValidationErrorCode
}
return nil, ""
}
func (v *IngredientUnitConverterValidatorImpl) ValidateConvertUnitRequest(req *contract.ConvertUnitRequest) (error, string) {
if req == nil {
return errors.New("request cannot be nil"), constants.ValidationErrorCode
}
// Validate required fields
if req.IngredientID.String() == "" {
return errors.New("ingredient_id is required"), constants.MissingFieldErrorCode
}
if req.FromUnitID.String() == "" {
return errors.New("from_unit_id is required"), constants.MissingFieldErrorCode
}
if req.ToUnitID.String() == "" {
return errors.New("to_unit_id is required"), constants.MissingFieldErrorCode
}
if req.Quantity <= 0 {
return errors.New("quantity must be greater than 0"), constants.ValidationErrorCode
}
return nil, ""
}
@@ -0,0 +1,189 @@
package validator
import (
"errors"
"strings"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
)
type PurchaseOrderValidator interface {
ValidateCreatePurchaseOrderRequest(req *contract.CreatePurchaseOrderRequest) (error, string)
ValidateUpdatePurchaseOrderRequest(req *contract.UpdatePurchaseOrderRequest) (error, string)
ValidateListPurchaseOrdersRequest(req *contract.ListPurchaseOrdersRequest) (error, string)
}
type PurchaseOrderValidatorImpl struct{}
func NewPurchaseOrderValidator() *PurchaseOrderValidatorImpl {
return &PurchaseOrderValidatorImpl{}
}
func (v *PurchaseOrderValidatorImpl) ValidateCreatePurchaseOrderRequest(req *contract.CreatePurchaseOrderRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if req.VendorID.String() == "" {
return errors.New("vendor_id is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.PONumber) == "" {
return errors.New("po_number is required"), constants.MissingFieldErrorCode
}
if len(req.PONumber) < 1 || len(req.PONumber) > 50 {
return errors.New("po_number must be between 1 and 50 characters"), constants.MalformedFieldErrorCode
}
if req.TransactionDate.IsZero() {
return errors.New("transaction_date is required"), constants.MissingFieldErrorCode
}
if req.DueDate.IsZero() {
return errors.New("due_date is required"), constants.MissingFieldErrorCode
}
if req.DueDate.Before(req.TransactionDate) {
return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode
}
if req.Reference != nil && len(*req.Reference) > 100 {
return errors.New("reference must be at most 100 characters"), constants.MalformedFieldErrorCode
}
if req.Status != nil {
validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"}
if !contains(validStatuses, *req.Status) {
return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode
}
}
if len(req.Items) == 0 {
return errors.New("at least one item is required"), constants.MissingFieldErrorCode
}
// Validate items
for i, item := range req.Items {
if err, code := v.validatePurchaseOrderItem(&item, i); err != nil {
return err, code
}
}
return nil, ""
}
func (v *PurchaseOrderValidatorImpl) ValidateUpdatePurchaseOrderRequest(req *contract.UpdatePurchaseOrderRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if req.PONumber != nil {
if strings.TrimSpace(*req.PONumber) == "" {
return errors.New("po_number cannot be empty"), constants.MalformedFieldErrorCode
}
if len(*req.PONumber) < 1 || len(*req.PONumber) > 50 {
return errors.New("po_number must be between 1 and 50 characters"), constants.MalformedFieldErrorCode
}
}
if req.TransactionDate != nil && req.DueDate != nil {
if req.DueDate.Before(*req.TransactionDate) {
return errors.New("due_date must be after transaction_date"), constants.MalformedFieldErrorCode
}
}
if req.Reference != nil && len(*req.Reference) > 100 {
return errors.New("reference must be at most 100 characters"), constants.MalformedFieldErrorCode
}
if req.Status != nil {
validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"}
if !contains(validStatuses, *req.Status) {
return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode
}
}
// Validate items if provided
if req.Items != nil {
for i, item := range req.Items {
if err, code := v.validateUpdatePurchaseOrderItem(&item, i); err != nil {
return err, code
}
}
}
return nil, ""
}
func (v *PurchaseOrderValidatorImpl) ValidateListPurchaseOrdersRequest(req *contract.ListPurchaseOrdersRequest) (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
}
if req.Status != "" {
validStatuses := []string{"draft", "sent", "approved", "received", "cancelled"}
if !contains(validStatuses, req.Status) {
return errors.New("status must be one of: draft, sent, approved, received, cancelled"), constants.MalformedFieldErrorCode
}
}
if req.StartDate != nil && req.EndDate != nil {
if req.EndDate.Before(*req.StartDate) {
return errors.New("end_date must be after start_date"), constants.MalformedFieldErrorCode
}
}
return nil, ""
}
func (v *PurchaseOrderValidatorImpl) validatePurchaseOrderItem(item *contract.CreatePurchaseOrderItemRequest, index int) (error, string) {
if item.IngredientID.String() == "" {
return errors.New("items[" + string(rune(index)) + "].ingredient_id is required"), constants.MissingFieldErrorCode
}
if item.Quantity <= 0 {
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
}
if item.UnitID.String() == "" {
return errors.New("items[" + string(rune(index)) + "].unit_id is required"), constants.MissingFieldErrorCode
}
if item.Amount < 0 {
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
}
return nil, ""
}
func (v *PurchaseOrderValidatorImpl) validateUpdatePurchaseOrderItem(item *contract.UpdatePurchaseOrderItemRequest, index int) (error, string) {
if item.Quantity != nil && *item.Quantity <= 0 {
return errors.New("items[" + string(rune(index)) + "].quantity must be greater than 0"), constants.MalformedFieldErrorCode
}
if item.Amount != nil && *item.Amount < 0 {
return errors.New("items[" + string(rune(index)) + "].amount must be greater than or equal to 0"), constants.MalformedFieldErrorCode
}
return nil, ""
}
// Helper function to check if a string is in a slice
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
+118
View File
@@ -0,0 +1,118 @@
package validator
import (
"errors"
"strings"
"apskel-pos-be/internal/constants"
"apskel-pos-be/internal/contract"
)
type VendorValidator interface {
ValidateCreateVendorRequest(req *contract.CreateVendorRequest) (error, string)
ValidateUpdateVendorRequest(req *contract.UpdateVendorRequest) (error, string)
ValidateListVendorsRequest(req *contract.ListVendorsRequest) (error, string)
}
type VendorValidatorImpl struct{}
func NewVendorValidator() *VendorValidatorImpl {
return &VendorValidatorImpl{}
}
func (v *VendorValidatorImpl) ValidateCreateVendorRequest(req *contract.CreateVendorRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if strings.TrimSpace(req.Name) == "" {
return errors.New("name is required"), constants.MissingFieldErrorCode
}
if len(req.Name) < 1 || len(req.Name) > 255 {
return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode
}
if req.Email != nil && strings.TrimSpace(*req.Email) != "" {
if !isValidEmail(*req.Email) {
return errors.New("email format is invalid"), constants.MalformedFieldErrorCode
}
}
if req.PhoneNumber != nil && strings.TrimSpace(*req.PhoneNumber) != "" {
if !isValidPhone(*req.PhoneNumber) {
return errors.New("phone_number format is invalid"), constants.MalformedFieldErrorCode
}
}
if req.ContactPerson != nil && len(*req.ContactPerson) > 255 {
return errors.New("contact_person must be at most 255 characters"), constants.MalformedFieldErrorCode
}
if req.TaxNumber != nil && len(*req.TaxNumber) > 50 {
return errors.New("tax_number must be at most 50 characters"), constants.MalformedFieldErrorCode
}
if req.PaymentTerms != nil && len(*req.PaymentTerms) > 100 {
return errors.New("payment_terms must be at most 100 characters"), constants.MalformedFieldErrorCode
}
return nil, ""
}
func (v *VendorValidatorImpl) ValidateUpdateVendorRequest(req *contract.UpdateVendorRequest) (error, string) {
if req == nil {
return errors.New("request body is required"), constants.MissingFieldErrorCode
}
if req.Name != nil {
if strings.TrimSpace(*req.Name) == "" {
return errors.New("name cannot be empty"), constants.MalformedFieldErrorCode
}
if len(*req.Name) < 1 || len(*req.Name) > 255 {
return errors.New("name must be between 1 and 255 characters"), constants.MalformedFieldErrorCode
}
}
if req.Email != nil && strings.TrimSpace(*req.Email) != "" {
if !isValidEmail(*req.Email) {
return errors.New("email format is invalid"), constants.MalformedFieldErrorCode
}
}
if req.PhoneNumber != nil && strings.TrimSpace(*req.PhoneNumber) != "" {
if !isValidPhone(*req.PhoneNumber) {
return errors.New("phone_number format is invalid"), constants.MalformedFieldErrorCode
}
}
if req.ContactPerson != nil && len(*req.ContactPerson) > 255 {
return errors.New("contact_person must be at most 255 characters"), constants.MalformedFieldErrorCode
}
if req.TaxNumber != nil && len(*req.TaxNumber) > 50 {
return errors.New("tax_number must be at most 50 characters"), constants.MalformedFieldErrorCode
}
if req.PaymentTerms != nil && len(*req.PaymentTerms) > 100 {
return errors.New("payment_terms must be at most 100 characters"), constants.MalformedFieldErrorCode
}
return nil, ""
}
func (v *VendorValidatorImpl) ValidateListVendorsRequest(req *contract.ListVendorsRequest) (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, ""
}