Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into dev/teguh

This commit is contained in:
aguhh18
2025-12-30 10:42:04 +07:00
60 changed files with 3173 additions and 44 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -84,8 +84,9 @@ func (r *approvalRepositoryImpl) LatestByTargets(
result := make(map[uint]entity.Approval, len(approvableIDs)) result := make(map[uint]entity.Approval, len(approvableIDs))
q := r.DB().WithContext(ctx). q := r.DB().WithContext(ctx).
Select("DISTINCT ON (approvable_id) *").
Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs). Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs).
Order("action_at DESC") Order("approvable_id, action_at DESC")
if modifier != nil { if modifier != nil {
q = modifier(q) q = modifier(q)
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"gorm.io/gorm" "gorm.io/gorm"
@@ -9,45 +10,59 @@ import (
// Exists reports whether a record with the given ID exists for type T. // Exists reports whether a record with the given ID exists for type T.
func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) { func Exists[T any](ctx context.Context, db *gorm.DB, id uint) (bool, error) {
var count int64 var marker int
if err := db.WithContext(ctx). err := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where("id = ?", id). Where("id = ?", id).
Count(&count).Error; err != nil { Limit(1).
Take(&marker).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
if err != nil {
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) { func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeID *uint) (bool, error) {
var count int64
q := db.WithContext(ctx). q := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where("name = ?", name). Where("name = ?", name).
Where("deleted_at IS NULL") Where("deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("id <> ?", *excludeID) q = q.Where("id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { var marker int
if err := q.Limit(1).Take(&marker).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) { func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
if field == "" { if field == "" {
return false, fmt.Errorf("field is required") return false, fmt.Errorf("field is required")
} }
var count int64
q := db.WithContext(ctx). q := db.WithContext(ctx).
Model(new(T)). Model(new(T)).
Select("1").
Where(fmt.Sprintf("%s = ?", field), value). Where(fmt.Sprintf("%s = ?", field), value).
Where("deleted_at IS NULL") Where("deleted_at IS NULL")
if excludeID != nil { if excludeID != nil {
q = q.Where("id <> ?", *excludeID) q = q.Where("id <> ?", *excludeID)
} }
if err := q.Count(&count).Error; err != nil { var marker int
if err := q.Limit(1).Take(&marker).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return false, err return false, err
} }
return count > 0, nil return true, nil
} }
@@ -0,0 +1,3 @@
DROP INDEX IF EXISTS idx_payments_bank_id;
DROP INDEX IF EXISTS payments_party_polymorphic;
DROP TABLE IF EXISTS payments;
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS payments (
id BIGSERIAL PRIMARY KEY,
payment_code VARCHAR(50) NOT NULL,
reference_number VARCHAR(100) NULL,
transaction_type VARCHAR(50),
party_type VARCHAR(50) NOT NULL,
party_id BIGINT NOT NULL,
payment_date TIMESTAMPTZ NOT NULL,
payment_method VARCHAR(20) NOT NULL,
bank_id BIGINT NULL REFERENCES banks(id) ON DELETE RESTRICT ON UPDATE CASCADE,
direction VARCHAR(5) NOT NULL,
nominal NUMERIC(15, 3) NOT NULL,
notes TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
);
-- Indexes
CREATE INDEX payments_party_polymorphic ON payments (party_type, party_id);
CREATE INDEX idx_payments_bank_id ON payments (bank_id);
@@ -0,0 +1,18 @@
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
END LOOP;
END $$;
DROP FUNCTION IF EXISTS soft_delete_handle_fk();
@@ -0,0 +1,126 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
fk.child_table,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1 @@
DROP SEQUENCE IF EXISTS payments_code_seq;
@@ -0,0 +1 @@
CREATE SEQUENCE IF NOT EXISTS payments_code_seq START WITH 1 INCREMENT BY 1;
@@ -0,0 +1,7 @@
DROP INDEX IF EXISTS idx_project_flocks_production_standard_id;
ALTER TABLE project_flocks
DROP CONSTRAINT IF EXISTS fk_project_flocks_production_standard_id;
ALTER TABLE project_flocks
DROP COLUMN IF EXISTS production_standard_id;
@@ -0,0 +1,15 @@
-- Add production_standard_id to project_flocks
ALTER TABLE project_flocks
ADD COLUMN IF NOT EXISTS production_standard_id BIGINT;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE project_flocks
ADD CONSTRAINT fk_project_flocks_production_standard_id
FOREIGN KEY (production_standard_id) REFERENCES production_standards (id) ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flocks_production_standard_id
ON project_flocks (production_standard_id);
+30
View File
@@ -0,0 +1,30 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Initial struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ReferenceNumber string `gorm:"type:varchar(100);not null"`
TransactionType string `gorm:"type:varchar(50);not null"`
InitialBalanceType string `gorm:"type:varchar(20);not null"`
PartyType string `gorm:"type:varchar(50);not null;index:initials_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:initials_party_polymorphic,priority:2"`
BankId *uint `gorm:"index"`
Direction string `gorm:"type:varchar(5);not null"`
Nominal float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;not null"`
CreatedBy uint `gorm:"index" json:"-"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Bank Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+32
View File
@@ -0,0 +1,32 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"`
PaymentMethod string `gorm:"type:varchar(20);not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Direction string `gorm:"type:varchar(5);not null"`
Nominal float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Customer *Customer `gorm:"foreignKey:PartyId;references:Id"`
Supplier *Supplier `gorm:"foreignKey:PartyId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"`
}
+2
View File
@@ -12,6 +12,7 @@ type ProjectFlock struct {
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
Category string `gorm:"type:varchar(20);not null"` Category string `gorm:"type:varchar(20);not null"`
FcrId uint `gorm:"not null"` FcrId uint `gorm:"not null"`
ProductionStandardId uint `gorm:"column:production_standard_id"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
@@ -20,6 +21,7 @@ type ProjectFlock struct {
Area Area `gorm:"foreignKey:AreaId;references:Id"` Area Area `gorm:"foreignKey:AreaId;references:Id"`
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
Location Location `gorm:"foreignKey:LocationId;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"` Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Transaction struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+5 -6
View File
@@ -104,12 +104,11 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
} }
func ActorIDFromContext(c *fiber.Ctx) (uint, error) { func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
// user, ok := AuthenticatedUser(c) user, ok := AuthenticatedUser(c)
// if !ok || user == nil || user.Id == 0 { if !ok || user == nil || user.Id == 0 {
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// } }
// return user.Id, nil return user.Id, nil
return 1, nil
} }
// AuthDetails returns the full authentication context (token, claims, user). // AuthDetails returns the full authentication context (token, claims, user).
+33
View File
@@ -162,8 +162,32 @@ const (
P_WarehousesCreateOne = "lti.master.warehouses.create" P_WarehousesCreateOne = "lti.master.warehouses.create"
P_WarehousesUpdateOne = "lti.master.warehouses.update" P_WarehousesUpdateOne = "lti.master.warehouses.update"
P_WarehousesDeleteOne = "lti.master.warehouses.delete" P_WarehousesDeleteOne = "lti.master.warehouses.delete"
P_Production_Standart_GetAll = "lti.master.production_standards.list"
P_Production_Standart_CreateOne = "lti.master.production_standards.create"
P_Production_Standart_GetOne = "lti.master.production_standards.detail"
P_Production_Standart_UpdateOne = "lti.master.production_standards.update"
P_Production_Standart_DeleteOne = "lti.master.production_standards.delete"
) )
// finance
const (
P_Finances_Initial_Balances_CreateOne = "lti.finance.initial_balances.create"
P_Finances_Initial_Balances_GetOne = "lti.finance.initial_balances.detail"
P_Finances_Initial_Balances_UpdateOne = "lti.finance.initial_balances.update"
P_Finances_Injections_CreateOne = "lti.finance.injections.create"
P_Finances_Injections_GetOne = "lti.finance.injections.detail"
P_Finances_Injections_UpdateOne = "lti.finance.injections.update"
P_Finances_Payments_CreateOne = "lti.finance.payments.create"
P_Finances_Payments_UpdateOne = "lti.finance.payments.update"
P_Finances_Payments_GetOne = "lti.finance.payments.detail"
P_Finances_Transaction_GetAll = "lti.finance.transactions.list"
P_Finances_Transaction_GetOne = "lti.finance.transactions.detail"
P_Finances_Transaction_DeleteOne = "lti.finance.transactions.delete"
)
const ( const (
P_ChickinsCreateOne = "lti.production.chickins.create" P_ChickinsCreateOne = "lti.production.chickins.create"
P_ChickinsGetOne = "lti.production.chickins.detail" P_ChickinsGetOne = "lti.production.chickins.detail"
@@ -193,6 +217,15 @@ const (
P_PurchaseApprovalManager = "lti.Purchase.approve.manager" P_PurchaseApprovalManager = "lti.Purchase.approve.manager"
) )
const (
P_FinanceGetAll = "lti.finance.list"
P_FinanceGetOne = "lti.finance.detail"
P_FinanceCreateOne = "lti.finance.create"
P_FinanceUpdateOne = "lti.finance.update"
P_FinanceDeleteOne = "lti.finance.delete"
P_FinanceApproval = "lti.finance.approve"
)
const ( const (
P_UserGetAll = "lti.users.list" P_UserGetAll = "lti.users.list"
P_UserGetOne = "lti.users.detail" P_UserGetOne = "lti.users.detail"
@@ -0,0 +1,92 @@
package controller
import (
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type InitialController struct {
InitialService service.InitialService
}
func NewInitialController(initialService service.InitialService) *InitialController {
return &InitialController{
InitialService: initialService,
}
}
func (u *InitialController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.InitialService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get initial successfully",
Data: dto.ToInitialListDTO(*result),
})
}
func (u *InitialController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.InitialService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create initial successfully",
Data: dto.ToInitialListDTO(*result),
})
}
func (u *InitialController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.InitialService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update initial successfully",
Data: dto.ToInitialListDTO(*result),
})
}
@@ -0,0 +1,163 @@
package dto
import (
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
type InitialRelationDTO struct {
Id uint `json:"id"`
ReferenceNumber string `json:"reference_number"`
TransactionType string `json:"transaction_type"`
InitialBalanceType string `json:"initial_balance_type"`
InitialBalanceTypeLabel string `json:"initial_balance_type_label"`
Party Party `json:"party"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
Direction string `json:"direction"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
}
type InitialListDTO struct {
InitialRelationDTO
CreatedBy uint `json:"created_by"`
CreatedByUser userDTO.UserRelationDTO `json:"created_by_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type InitialDetailDTO struct {
InitialListDTO
}
type Party struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
}
// === Mapper Functions ===
func ToInitialRelationDTO(e entity.Payment) InitialRelationDTO {
reference := ""
if e.ReferenceNumber != nil {
reference = *e.ReferenceNumber
}
initialBalanceType := initialBalanceTypeFromPayment(e)
return InitialRelationDTO{
Id: e.Id,
ReferenceNumber: reference,
TransactionType: transactionTypeLabel(e.TransactionType),
InitialBalanceType: initialBalanceType,
InitialBalanceTypeLabel: initialBalanceLabel(initialBalanceType),
Party: partyFromInitial(e),
Bank: bankFromInitial(e),
Direction: e.Direction,
Nominal: e.Nominal,
Notes: e.Notes,
}
}
func ToInitialListDTO(e entity.Payment) InitialListDTO {
approval := approvalDTO.ApprovalRelationDTO{}
if e.LatestApproval != nil {
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
}
return InitialListDTO{
InitialRelationDTO: ToInitialRelationDTO(e),
CreatedBy: e.CreatedBy,
CreatedByUser: userFromInitial(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Approval: approval,
}
}
func ToInitialListDTOs(e []entity.Payment) []InitialListDTO {
result := make([]InitialListDTO, len(e))
for i, r := range e {
result[i] = ToInitialListDTO(r)
}
return result
}
func ToInitialDetailDTO(e entity.Payment) InitialDetailDTO {
return InitialDetailDTO{
InitialListDTO: ToInitialListDTO(e),
}
}
func partyFromInitial(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
}
return party
}
func bankFromInitial(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return bankDTO.BankRelationDTO{}
}
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromInitial(e entity.Payment) userDTO.UserRelationDTO {
if e.CreatedUser.Id == 0 {
return userDTO.UserRelationDTO{}
}
return userDTO.ToUserRelationDTO(e.CreatedUser)
}
func transactionTypeLabel(transactionType string) string {
if strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal)) {
return "Saldo Awal"
}
return transactionType
}
func initialBalanceLabel(balanceType string) string {
switch strings.ToUpper(strings.TrimSpace(balanceType)) {
case "NEGATIVE":
return "Saldo Awal Negatif"
case "POSITIVE":
return "Saldo Awal Positif"
default:
return balanceType
}
}
func initialBalanceTypeFromPayment(e entity.Payment) string {
if strings.EqualFold(e.Direction, "OUT") || e.Nominal < 0 {
return "NEGATIVE"
}
return "POSITIVE"
}
@@ -0,0 +1,36 @@
package initials
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
rInitial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/repositories"
sInitial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type InitialModule struct{}
func (InitialModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
initialRepo := rInitial.NewInitialRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInitial, utils.InitialApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register initial approval workflow: %v", err))
}
initialService := sInitial.NewInitialService(initialRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
InitialRoutes(router, userService, initialService)
}
@@ -0,0 +1,51 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type InitialRepository interface {
repository.BaseRepository[entity.Payment]
BankExists(ctx context.Context, bankId uint) (bool, error)
CustomerExists(ctx context.Context, customerId uint) (bool, error)
SupplierExists(ctx context.Context, supplierId uint) (bool, error)
NextPaymentSequence(ctx context.Context) (int64, error)
}
type InitialRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Payment]
db *gorm.DB
}
func NewInitialRepository(db *gorm.DB) InitialRepository {
return &InitialRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
db: db,
}
}
func (r *InitialRepositoryImpl) BankExists(ctx context.Context, bankId uint) (bool, error) {
return repository.Exists[entity.Bank](ctx, r.db, bankId)
}
func (r *InitialRepositoryImpl) CustomerExists(ctx context.Context, customerId uint) (bool, error) {
return repository.Exists[entity.Customer](ctx, r.db, customerId)
}
func (r *InitialRepositoryImpl) SupplierExists(ctx context.Context, supplierId uint) (bool, error) {
return repository.Exists[entity.Supplier](ctx, r.db, supplierId)
}
func (r *InitialRepositoryImpl) NextPaymentSequence(ctx context.Context) (int64, error) {
var next int64
if err := r.db.WithContext(ctx).
Raw("SELECT nextval('payments_code_seq')").
Scan(&next).Error; err != nil {
return 0, err
}
return next, nil
}
@@ -0,0 +1,21 @@
package initials
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/controllers"
initial "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func InitialRoutes(v1 fiber.Router, u user.UserService, s initial.InitialService) {
ctrl := controller.NewInitialController(s)
route := v1.Group("/initial-balances")
route.Use(m.Auth(u))
route.Post("/",m.RequirePermissions(m.P_Finances_Initial_Balances_CreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Initial_Balances_GetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Initial_Balances_UpdateOne), ctrl.UpdateOne)
}
@@ -0,0 +1,336 @@
package service
import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type InitialService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
}
type initialService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.InitialRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
func NewInitialService(
repo repository.InitialRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) InitialService {
return &initialService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowInitial,
}
}
func (s initialService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse").
Preload("Customer").
Preload("Supplier")
}
func (s initialService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
initial, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
if err != nil {
s.Log.Errorf("Failed get initial by id: %+v", err)
return nil, err
}
if !isInitialTransaction(initial.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
if s.ApprovalSvc != nil {
approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approval for initial %d: %+v", id, err)
} else {
initial.LatestApproval = approval
}
}
return initial, nil
}
func (s *initialService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
party, err := normalizePartyType(req.PartyType)
if err != nil {
return nil, err
}
if err := s.ensurePartyExists(c.Context(), party, req.PartyId); err != nil {
return nil, err
}
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
balanceType, err := normalizeInitialBalanceType(req.InitialBalanceType)
if err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
code, err := s.generateInitialCode(c.Context())
if err != nil {
return nil, err
}
reference := req.ReferenceNumber
createBody := &entity.Payment{
PaymentCode: code,
ReferenceNumber: &reference,
TransactionType: string(utils.TransactionTypeSaldoAwal),
PartyType: party,
PartyId: req.PartyId,
PaymentDate: time.Now(),
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
Direction: directionForInitialType(balanceType),
Nominal: signedNominal(balanceType, req.Nominal),
Notes: req.Note,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
initialRepoTx := repository.NewInitialRepository(dbTransaction)
if err := initialRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if s.ApprovalSvc != nil {
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err := approvalSvcTx.CreateApproval(
c.Context(),
s.approvalWorkflow,
createBody.Id,
utils.InitialStepPengajuan,
&action,
actorID,
nil,
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create initial: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s initialService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber
}
if req.Note != nil {
updateBody["notes"] = *req.Note
}
if req.BankId != nil {
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
updateBody["bank_id"] = *req.BankId
}
requiresExisting := req.PartyType != nil || req.PartyId != nil || req.InitialBalanceType != nil || req.Nominal != nil
requiresVerification := requiresExisting || req.ReferenceNumber != nil || req.Note != nil || req.BankId != nil
var existing *entity.Payment
if requiresVerification {
current, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
if err != nil {
s.Log.Errorf("Failed get initial by id: %+v", err)
return nil, err
}
if !isInitialTransaction(current.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
existing = current
}
if req.PartyType != nil || req.PartyId != nil {
partyType := existing.PartyType
partyId := existing.PartyId
if req.PartyType != nil {
normalized, err := normalizePartyType(*req.PartyType)
if err != nil {
return nil, err
}
partyType = normalized
updateBody["party_type"] = partyType
}
if req.PartyId != nil {
partyId = *req.PartyId
updateBody["party_id"] = partyId
}
if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
return nil, err
}
}
if req.InitialBalanceType != nil || req.Nominal != nil {
balanceType := balanceTypeFromPayment(existing)
if req.InitialBalanceType != nil {
normalized, err := normalizeInitialBalanceType(*req.InitialBalanceType)
if err != nil {
return nil, err
}
balanceType = normalized
}
nominal := math.Abs(existing.Nominal)
if req.Nominal != nil {
nominal = *req.Nominal
}
updateBody["direction"] = directionForInitialType(balanceType)
updateBody["nominal"] = signedNominal(balanceType, nominal)
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Initial not found")
}
s.Log.Errorf("Failed to update initial: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func isInitialTransaction(transactionType string) bool {
return strings.EqualFold(transactionType, string(utils.TransactionTypeSaldoAwal))
}
func balanceTypeFromPayment(payment *entity.Payment) string {
if strings.EqualFold(payment.Direction, "OUT") || payment.Nominal < 0 {
return "NEGATIVE"
}
return "POSITIVE"
}
func normalizePartyType(partyType string) (string, error) {
party := strings.ToUpper(strings.TrimSpace(partyType))
if !utils.IsValidPaymentParty(party) {
return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
}
return party, nil
}
func normalizeInitialBalanceType(balanceType string) (string, error) {
normalized := strings.ToUpper(strings.TrimSpace(balanceType))
switch normalized {
case "NEGATIVE", "POSITIVE":
return normalized, nil
default:
return "", utils.BadRequest("`initial_balance_type` must be `NEGATIVE` or `POSITIVE`")
}
}
func directionForInitialType(balanceType string) string {
if strings.EqualFold(balanceType, "NEGATIVE") {
return "OUT"
}
return "IN"
}
func signedNominal(balanceType string, nominal float64) float64 {
normalized := math.Abs(nominal)
if strings.EqualFold(balanceType, "NEGATIVE") {
return -normalized
}
return normalized
}
func (s initialService) generateInitialCode(ctx context.Context) (string, error) {
sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil {
return "", err
}
return fmt.Sprintf("INIT-%05d", sequence), nil
}
func (s initialService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s initialService) ensurePartyExists(ctx context.Context, partyType string, partyId uint) error {
switch utils.PaymentParty(partyType) {
case utils.PaymentPartyCustomer:
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Customer", ID: &partyId, Exists: s.Repository.CustomerExists},
)
case utils.PaymentPartySupplier:
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Supplier", ID: &partyId, Exists: s.Repository.SupplierExists},
)
default:
return utils.BadRequest("`party_type` must be `customer` or `supplier`")
}
}
func (s initialService) ensureBankExists(ctx context.Context, bankId *uint) error {
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
)
}
@@ -0,0 +1,27 @@
package validation
type Create struct {
PartyType string `json:"party_type" validate:"required_strict,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
ReferenceNumber string `json:"reference_number" validate:"required_strict,max=100"`
InitialBalanceType string `json:"initial_balance_type" validate:"required_strict,oneof=NEGATIVE POSITIVE"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Note string `json:"note" validate:"required_strict,max=500"`
}
type Update struct {
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
ReferenceNumber *string `json:"reference_number,omitempty" validate:"omitempty,max=100"`
InitialBalanceType *string `json:"initial_balance_type,omitempty" validate:"omitempty,oneof=NEGATIVE POSITIVE"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Note *string `json:"note,omitempty" validate:"omitempty,max=500"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,92 @@
package controller
import (
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type InjectionController struct {
InjectionService service.InjectionService
}
func NewInjectionController(injectionService service.InjectionService) *InjectionController {
return &InjectionController{
InjectionService: injectionService,
}
}
func (u *InjectionController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.InjectionService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get injection successfully",
Data: dto.ToInjectionListDTO(*result),
})
}
func (u *InjectionController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.InjectionService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Balance injection created successfully",
Data: dto.ToInjectionListDTO(*result),
})
}
func (u *InjectionController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.InjectionService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update injection successfully",
Data: dto.ToInjectionListDTO(*result),
})
}
@@ -0,0 +1,102 @@
package dto
import (
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
type InjectionRelationDTO struct {
Id uint `json:"id"`
TransactionType string `json:"transaction_type"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
AdjustmentDate string `json:"adjustment_date"`
Direction string `json:"direction"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
}
type InjectionListDTO struct {
InjectionRelationDTO
CreatedBy uint `json:"created_by"`
CreatedByUser userDTO.UserRelationDTO `json:"created_by_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type InjectionDetailDTO struct {
InjectionListDTO
}
// === Mapper Functions ===
func ToInjectionRelationDTO(e entity.Payment) InjectionRelationDTO {
return InjectionRelationDTO{
Id: e.Id,
TransactionType: transactionTypeLabel(e.TransactionType),
Bank: bankFromInjection(e),
AdjustmentDate: utils.FormatDate(e.PaymentDate),
Direction: e.Direction,
Nominal: e.Nominal,
Notes: e.Notes,
}
}
func ToInjectionListDTO(e entity.Payment) InjectionListDTO {
approval := approvalDTO.ApprovalRelationDTO{}
if e.LatestApproval != nil {
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
}
return InjectionListDTO{
InjectionRelationDTO: ToInjectionRelationDTO(e),
CreatedBy: e.CreatedBy,
CreatedByUser: userFromInjection(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Approval: approval,
}
}
func ToInjectionListDTOs(e []entity.Payment) []InjectionListDTO {
result := make([]InjectionListDTO, len(e))
for i, r := range e {
result[i] = ToInjectionListDTO(r)
}
return result
}
func ToInjectionDetailDTO(e entity.Payment) InjectionDetailDTO {
return InjectionDetailDTO{
InjectionListDTO: ToInjectionListDTO(e),
}
}
func bankFromInjection(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return bankDTO.BankRelationDTO{}
}
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromInjection(e entity.Payment) userDTO.UserRelationDTO {
if e.CreatedUser.Id == 0 {
return userDTO.UserRelationDTO{}
}
return userDTO.ToUserRelationDTO(e.CreatedUser)
}
func transactionTypeLabel(transactionType string) string {
if strings.EqualFold(transactionType, string(utils.TransactionTypeInjection)) {
return "Injection"
}
return transactionType
}
@@ -0,0 +1,36 @@
package injections
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
rInjection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/repositories"
sInjection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type InjectionModule struct{}
func (InjectionModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
injectionRepo := rInjection.NewInjectionRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInjection, utils.InjectionApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register injection approval workflow: %v", err))
}
injectionService := sInjection.NewInjectionService(injectionRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
InjectionRoutes(router, userService, injectionService)
}
@@ -0,0 +1,41 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type InjectionRepository interface {
repository.BaseRepository[entity.Payment]
BankExists(ctx context.Context, bankId uint) (bool, error)
NextPaymentSequence(ctx context.Context) (int64, error)
}
type InjectionRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Payment]
db *gorm.DB
}
func NewInjectionRepository(db *gorm.DB) InjectionRepository {
return &InjectionRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
db: db,
}
}
func (r *InjectionRepositoryImpl) BankExists(ctx context.Context, bankId uint) (bool, error) {
return repository.Exists[entity.Bank](ctx, r.db, bankId)
}
func (r *InjectionRepositoryImpl) NextPaymentSequence(ctx context.Context) (int64, error) {
var next int64
if err := r.db.WithContext(ctx).
Raw("SELECT nextval('payments_code_seq')").
Scan(&next).Error; err != nil {
return 0, err
}
return next, nil
}
@@ -0,0 +1,21 @@
package injections
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/controllers"
injection "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func InjectionRoutes(v1 fiber.Router, u user.UserService, s injection.InjectionService) {
ctrl := controller.NewInjectionController(s)
route := v1.Group("/injections")
route.Use(m.Auth(u))
route.Post("/", m.RequirePermissions(m.P_Finances_Injections_CreateOne), ctrl.CreateOne)
route.Get("/:id", m.RequirePermissions(m.P_Finances_Injections_GetOne), ctrl.GetOne)
route.Patch("/:id", m.RequirePermissions(m.P_Finances_Injections_UpdateOne), ctrl.UpdateOne)
}
@@ -0,0 +1,230 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type InjectionService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
}
type injectionService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.InjectionRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
func NewInjectionService(
repo repository.InjectionRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) InjectionService {
return &injectionService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowInjection,
}
}
func (s injectionService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse")
}
func (s injectionService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
injection, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
}
if err != nil {
s.Log.Errorf("Failed get injection by id: %+v", err)
return nil, err
}
if !isInjectionTransaction(injection.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
}
if s.ApprovalSvc != nil {
approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approval for injection %d: %+v", id, err)
} else {
injection.LatestApproval = approval
}
}
return injection, nil
}
func (s *injectionService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
adjustmentDate, err := utils.ParseDateString(req.AdjustmentDate)
if err != nil {
return nil, utils.BadRequest(err.Error())
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
code, err := s.generateInjectionCode(c.Context())
if err != nil {
return nil, err
}
createBody := &entity.Payment{
PaymentCode: code,
TransactionType: string(utils.TransactionTypeInjection),
PartyType: string(utils.PaymentPartyCustomer),
PartyId: 0,
PaymentDate: adjustmentDate,
PaymentMethod: string(utils.PaymentMethodSaldo),
BankId: req.BankId,
Direction: "IN",
Nominal: req.Nominal,
Notes: req.Notes,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
injectionRepoTx := repository.NewInjectionRepository(dbTransaction)
if err := injectionRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if s.ApprovalSvc != nil {
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err := approvalSvcTx.CreateApproval(
c.Context(),
s.approvalWorkflow,
createBody.Id,
utils.InjectionStepPengajuan,
&action,
actorID,
nil,
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create injection: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s injectionService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
requiresVerification := req.BankId != nil || req.AdjustmentDate != nil || req.Nominal != nil || req.Notes != nil
if requiresVerification {
current, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
}
if err != nil {
s.Log.Errorf("Failed get injection by id: %+v", err)
return nil, err
}
if !isInjectionTransaction(current.TransactionType) {
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
}
}
if req.BankId != nil {
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
updateBody["bank_id"] = *req.BankId
}
if req.AdjustmentDate != nil {
parsedDate, err := utils.ParseDateString(*req.AdjustmentDate)
if err != nil {
return nil, utils.BadRequest(err.Error())
}
updateBody["payment_date"] = parsedDate
}
if req.Nominal != nil {
updateBody["nominal"] = *req.Nominal
}
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Injection not found")
}
s.Log.Errorf("Failed to update injection: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func isInjectionTransaction(transactionType string) bool {
return strings.EqualFold(transactionType, string(utils.TransactionTypeInjection))
}
func (s injectionService) generateInjectionCode(ctx context.Context) (string, error) {
sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil {
return "", err
}
return fmt.Sprintf("INJ-%05d", sequence), nil
}
func (s injectionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s injectionService) ensureBankExists(ctx context.Context, bankId *uint) error {
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
)
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
BankId *uint `json:"bank_id" validate:"required_strict,number,gt=0"`
AdjustmentDate string `json:"adjustment_date" validate:"required_strict"`
Nominal float64 `json:"nominal" validate:"required_strict,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
}
type Update struct {
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
AdjustmentDate *string `json:"adjustment_date,omitempty" validate:"omitempty"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+13
View File
@@ -0,0 +1,13 @@
package finance
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
type FinanceModule struct{}
func (FinanceModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
RegisterRoutes(router, db, validate)
}
@@ -0,0 +1,92 @@
package controller
import (
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PaymentController struct {
PaymentService service.PaymentService
}
func NewPaymentController(paymentService service.PaymentService) *PaymentController {
return &PaymentController{
PaymentService: paymentService,
}
}
func (u *PaymentController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.PaymentService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get payment successfully",
Data: dto.ToPaymentListDTO(*result),
})
}
func (u *PaymentController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PaymentService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create payment successfully",
Data: dto.ToPaymentListDTO(*result),
})
}
func (u *PaymentController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PaymentService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update payment successfully",
Data: dto.ToPaymentListDTO(*result),
})
}
@@ -0,0 +1,189 @@
package dto
import (
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
type PaymentRelationDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number,omitempty"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user,omitempty"`
}
type PaymentListDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type PaymentDetailDTO struct {
PaymentListDTO
}
type Party struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
}
// === Mapper Functions ===
func ToPaymentRelationDTO(e entity.Payment) PaymentRelationDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
return PaymentRelationDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
}
}
func ToPaymentListDTO(e entity.Payment) PaymentListDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
approval := approvalDTO.ApprovalRelationDTO{}
if e.LatestApproval != nil {
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
}
return PaymentListDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Approval: approval,
}
}
func ToPaymentListDTOs(e []entity.Payment) []PaymentListDTO {
result := make([]PaymentListDTO, len(e))
for i, r := range e {
result[i] = ToPaymentListDTO(r)
}
return result
}
func ToPaymentDetailDTO(e entity.Payment) PaymentDetailDTO {
return PaymentDetailDTO{
PaymentListDTO: ToPaymentListDTO(e),
}
}
func partyFromPayment(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
}
return party
}
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return bankDTO.BankRelationDTO{}
}
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
if e.CreatedUser.Id == 0 {
return userDTO.UserRelationDTO{}
}
return userDTO.ToUserRelationDTO(e.CreatedUser)
}
func paymentCodeFromPayment(e entity.Payment) string {
if e.PaymentCode != "" {
return e.PaymentCode
}
if e.ReferenceNumber != nil {
return *e.ReferenceNumber
}
return ""
}
func transactionTypeFromPayment(e entity.Payment) string {
if e.TransactionType != "" {
return e.TransactionType
}
return e.Direction
}
func paymentAmounts(direction string, nominal float64) (float64, float64) {
switch strings.ToUpper(direction) {
case "IN":
return 0, nominal
case "OUT":
return nominal, 0
default:
return 0, 0
}
}
@@ -0,0 +1,36 @@
package payments
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm"
rPayment "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/repositories"
sPayment "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type PaymentModule struct{}
func (PaymentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
paymentRepo := rPayment.NewPaymentRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPayment, utils.PaymentApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register payment approval workflow: %v", err))
}
paymentService := sPayment.NewPaymentService(paymentRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
PaymentRoutes(router, userService, paymentService)
}
@@ -0,0 +1,62 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type PaymentRepository interface {
repository.BaseRepository[entity.Payment]
BankExists(ctx context.Context, bankId uint) (bool, error)
CustomerExists(ctx context.Context, customerId uint) (bool, error)
SupplierExists(ctx context.Context, supplierId uint) (bool, error)
SupplierCategory(ctx context.Context, supplierId uint) (string, error)
NextPaymentSequence(ctx context.Context) (int64, error)
}
type PaymentRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Payment]
db *gorm.DB
}
func NewPaymentRepository(db *gorm.DB) PaymentRepository {
return &PaymentRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
db: db,
}
}
func (r *PaymentRepositoryImpl) BankExists(ctx context.Context, bankId uint) (bool, error) {
return repository.Exists[entity.Bank](ctx, r.db, bankId)
}
func (r *PaymentRepositoryImpl) CustomerExists(ctx context.Context, customerId uint) (bool, error) {
return repository.Exists[entity.Customer](ctx, r.db, customerId)
}
func (r *PaymentRepositoryImpl) SupplierExists(ctx context.Context, supplierId uint) (bool, error) {
return repository.Exists[entity.Supplier](ctx, r.db, supplierId)
}
func (r *PaymentRepositoryImpl) SupplierCategory(ctx context.Context, supplierId uint) (string, error) {
var supplier entity.Supplier
if err := r.db.WithContext(ctx).
Select("id", "category").
First(&supplier, supplierId).Error; err != nil {
return "", err
}
return supplier.Category, nil
}
func (r *PaymentRepositoryImpl) NextPaymentSequence(ctx context.Context) (int64, error) {
var next int64
if err := r.db.WithContext(ctx).
Raw("SELECT nextval('payments_code_seq')").
Scan(&next).Error; err != nil {
return 0, err
}
return next, nil
}
@@ -0,0 +1,21 @@
package payments
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/controllers"
payment "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func PaymentRoutes(v1 fiber.Router, u user.UserService, s payment.PaymentService) {
ctrl := controller.NewPaymentController(s)
route := v1.Group("/payments")
route.Use(m.Auth(u))
route.Post("/",m.RequirePermissions(m.P_Finances_Payments_CreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Payments_GetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_Finances_Payments_UpdateOne), ctrl.UpdateOne)
}
@@ -0,0 +1,362 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type PaymentService interface {
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Payment, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error)
}
type paymentService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.PaymentRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey
}
func NewPaymentService(
repo repository.PaymentRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) PaymentService {
return &paymentService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowPayment,
}
}
func (s paymentService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse").
Preload("Customer").
Preload("Supplier")
}
func (s paymentService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
payment, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found")
}
if err != nil {
s.Log.Errorf("Failed get payment by id: %+v", err)
return nil, err
}
if s.ApprovalSvc != nil {
approval, err := s.ApprovalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approval for payment %d: %+v", id, err)
} else {
payment.LatestApproval = approval
}
}
return payment, nil
}
func (s *paymentService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
//! CHECK PARTY TYPE
party, err := normalizePartyType(req.PartyType)
if err != nil {
return nil, err
}
//! CHECK EXISTS
if err := s.ensurePartyExists(c.Context(), party, req.PartyId); err != nil {
return nil, err
}
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
//? NORMALIZE
paymentDate, err := utils.ParseDateString(req.PaymentDate)
if err != nil {
return nil, utils.BadRequest(err.Error())
}
method, err := normalizePaymentMethod(req.PaymentMethod)
if err != nil {
return nil, err
}
transactionType, err := s.resolveTransactionType(c.Context(), party, req.PartyId)
if err != nil {
return nil, err
}
//? GET CREATED BY
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
code, err := s.generatePaymentCode(c.Context(), party)
if err != nil {
return nil, err
}
createBody := &entity.Payment{
PaymentCode: code,
ReferenceNumber: req.ReferenceNumber,
TransactionType: transactionType,
PartyType: party,
PartyId: req.PartyId,
PaymentDate: paymentDate,
PaymentMethod: method,
BankId: req.BankId,
Direction: directionForParty(party),
Nominal: req.Nominal,
Notes: req.Notes,
CreatedBy: actorID,
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
paymentRepoTx := repository.NewPaymentRepository(dbTransaction)
if err := paymentRepoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
if s.ApprovalSvc != nil {
action := entity.ApprovalActionCreated
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
_, err := approvalSvcTx.CreateApproval(
c.Context(),
s.approvalWorkflow,
createBody.Id,
utils.PaymentStepPengajuan,
&action,
actorID,
nil,
)
if err != nil {
return err
}
}
return nil
})
if err != nil {
s.Log.Errorf("Failed to create payment: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s paymentService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Payment, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.PaymentDate != nil {
parsedDate, err := utils.ParseDateString(*req.PaymentDate)
if err != nil {
return nil, utils.BadRequest(err.Error())
}
updateBody["payment_date"] = parsedDate
}
if req.Nominal != nil {
updateBody["nominal"] = *req.Nominal
}
if req.ReferenceNumber != nil {
updateBody["reference_number"] = *req.ReferenceNumber
}
if req.PaymentMethod != nil {
method, err := normalizePaymentMethod(*req.PaymentMethod)
if err != nil {
return nil, err
}
updateBody["payment_method"] = method
}
if req.BankId != nil {
if err := s.ensureBankExists(c.Context(), req.BankId); err != nil {
return nil, err
}
updateBody["bank_id"] = *req.BankId
}
if req.Notes != nil {
updateBody["notes"] = *req.Notes
}
if req.PartyType != nil || req.PartyId != nil {
existing, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found")
}
if err != nil {
s.Log.Errorf("Failed get payment by id: %+v", err)
return nil, err
}
partyType := existing.PartyType
partyId := existing.PartyId
if req.PartyType != nil {
normalized, err := normalizePartyType(*req.PartyType)
if err != nil {
return nil, err
}
partyType = normalized
updateBody["party_type"] = partyType
updateBody["direction"] = directionForParty(partyType)
}
if req.PartyId != nil {
partyId = *req.PartyId
updateBody["party_id"] = partyId
}
if err := s.ensurePartyExists(c.Context(), partyType, partyId); err != nil {
return nil, err
}
transactionType, err := s.resolveTransactionType(c.Context(), partyType, partyId)
if err != nil {
return nil, err
}
updateBody["transaction_type"] = transactionType
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Payment not found")
}
s.Log.Errorf("Failed to update payment: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func normalizePartyType(partyType string) (string, error) {
party := strings.ToUpper(strings.TrimSpace(partyType))
if !utils.IsValidPaymentParty(party) {
return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
}
return party, nil
}
func normalizePaymentMethod(method string) (string, error) {
normalized := strings.ToUpper(strings.TrimSpace(method))
if !utils.IsValidPaymentMethod(normalized) {
return "", utils.BadRequest("Invalid payment_method")
}
return normalized, nil
}
func directionForParty(partyType string) string {
if utils.PaymentParty(partyType) == utils.PaymentPartyCustomer {
return "IN"
}
return "OUT"
}
func (s paymentService) resolveTransactionType(ctx context.Context, partyType string, partyId uint) (string, error) {
switch utils.PaymentParty(partyType) {
case utils.PaymentPartyCustomer:
return string(utils.TransactionTypePenjualan), nil
case utils.PaymentPartySupplier:
category, err := s.getSupplierCategory(ctx, partyId)
if err != nil {
return "", err
}
if isSupplierCategoryBiaya(category) {
return string(utils.TransactionTypeBiaya), nil
}
return string(utils.TransactionTypePembelian), nil
default:
return "", utils.BadRequest("`party_type` must be `customer` or `supplier`")
}
}
func (s paymentService) generatePaymentCode(ctx context.Context, partyType string) (string, error) {
prefix := "PAY"
switch utils.PaymentParty(partyType) {
case utils.PaymentPartyCustomer:
prefix = "PAY-IN"
case utils.PaymentPartySupplier:
prefix = "PAY-OUT"
}
sequence, err := s.Repository.NextPaymentSequence(ctx)
if err != nil {
return "", err
}
return fmt.Sprintf("%s-%05d", prefix, sequence), nil
}
func (s paymentService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s paymentService) ensurePartyExists(ctx context.Context, partyType string, partyId uint) error {
switch utils.PaymentParty(partyType) {
case utils.PaymentPartyCustomer:
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Customer", ID: &partyId, Exists: s.Repository.CustomerExists},
)
case utils.PaymentPartySupplier:
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Supplier", ID: &partyId, Exists: s.Repository.SupplierExists},
)
default:
return utils.BadRequest("`party_type` must be `customer` or `supplier`")
}
}
func (s paymentService) ensureBankExists(ctx context.Context, bankId *uint) error {
return commonSvc.EnsureRelations(ctx,
commonSvc.RelationCheck{Name: "Bank", ID: bankId, Exists: s.Repository.BankExists},
)
}
func (s paymentService) getSupplierCategory(ctx context.Context, supplierId uint) (string, error) {
category, err := s.Repository.SupplierCategory(ctx, supplierId)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", utils.NotFound("Supplier not found")
}
return "", err
}
return strings.ToUpper(strings.TrimSpace(category)), nil
}
func isSupplierCategoryBiaya(category string) bool {
switch strings.ToUpper(strings.TrimSpace(category)) {
case string(utils.SupplierCategoryBOP), "BIAYA":
return true
default:
return false
}
}
@@ -0,0 +1,29 @@
package validation
type Create struct {
PartyType string `json:"party_type" validate:"required_strict,min=1,max=50"`
PartyId uint `json:"party_id" validate:"required_strict,number,gt=0"`
PaymentDate string `json:"payment_date" validate:"required_strict,datetime=2006-01-02"`
Nominal float64 `json:"nominal" validate:"required_strict"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod string `json:"payment_method" validate:"required_strict,max=20"`
BankId *uint `json:"bank_id" validate:"omitempty,number,gt=0"`
Notes string `json:"notes" validate:"required_strict,max=500"`
}
type Update struct {
PartyType *string `json:"party_type,omitempty" validate:"omitempty,max=50"`
PartyId *uint `json:"party_id,omitempty" validate:"omitempty,number,gt=0"`
PaymentDate *string `json:"payment_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
Nominal *float64 `json:"nominal,omitempty" validate:"omitempty,gt=0"`
ReferenceNumber *string `json:"reference_number,omitempty"`
PaymentMethod *string `json:"payment_method,omitempty" validate:"omitempty,max=20"`
BankId *uint `json:"bank_id,omitempty" validate:"omitempty,number,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
+31
View File
@@ -0,0 +1,31 @@
package finance
import (
"gitlab.com/mbugroup/lti-api.git/internal/modules"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
payments "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/payments"
initials "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/initials"
injections "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/injections"
transactions "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/finance")
allModules := []modules.Module{
payments.PaymentModule{},
initials.InitialModule{},
injections.InjectionModule{},
transactions.TransactionModule{},
// MODULE REGISTRY
}
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
}
}
@@ -0,0 +1,96 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransactionController struct {
TransactionService service.TransactionService
}
func NewTransactionController(transactionService service.TransactionService) *TransactionController {
return &TransactionController{
TransactionService: transactionService,
}
}
func (u *TransactionController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.TransactionService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransactionListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transactions successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransactionListDTOs(result),
})
}
func (u *TransactionController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.TransactionService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transaction successfully",
Data: dto.ToTransactionListDTO(*result),
})
}
func (u *TransactionController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.TransactionService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transaction successfully",
})
}
@@ -0,0 +1,189 @@
package dto
import (
"strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
bankDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/banks/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
// === DTO Structs ===
type TransactionRelationDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number,omitempty"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank,omitempty"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user,omitempty"`
}
type TransactionListDTO struct {
Id uint `json:"id"`
PaymentCode string `json:"payment_code"`
ReferenceNumber *string `json:"reference_number"`
TransactionType string `json:"transaction_type"`
Party Party `json:"party"`
PaymentDate time.Time `json:"payment_date"`
PaymentMethod string `json:"payment_method"`
Bank bankDTO.BankRelationDTO `json:"bank"`
ExpenseAmount float64 `json:"expense_amount"`
IncomeAmount float64 `json:"income_amount"`
Nominal float64 `json:"nominal"`
Notes string `json:"notes"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type TransactionDetailDTO struct {
TransactionListDTO
}
type Party struct {
Id uint `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
AccountNumber string `json:"account_number"`
}
// === Mapper Functions ===
func ToTransactionRelationDTO(e entity.Payment) TransactionRelationDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
return TransactionRelationDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
}
}
func ToTransactionListDTO(e entity.Payment) TransactionListDTO {
expenseAmount, incomeAmount := paymentAmounts(e.Direction, e.Nominal)
approval := approvalDTO.ApprovalRelationDTO{}
if e.LatestApproval != nil {
approval = approvalDTO.ToApprovalDTO(*e.LatestApproval)
}
return TransactionListDTO{
Id: e.Id,
PaymentCode: paymentCodeFromPayment(e),
ReferenceNumber: e.ReferenceNumber,
TransactionType: transactionTypeFromPayment(e),
Party: partyFromPayment(e),
PaymentDate: e.PaymentDate,
PaymentMethod: e.PaymentMethod,
Bank: bankFromPayment(e),
ExpenseAmount: expenseAmount,
IncomeAmount: incomeAmount,
Nominal: e.Nominal,
Notes: e.Notes,
CreatedUser: userFromPayment(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
Approval: approval,
}
}
func ToTransactionListDTOs(e []entity.Payment) []TransactionListDTO {
result := make([]TransactionListDTO, len(e))
for i, r := range e {
result[i] = ToTransactionListDTO(r)
}
return result
}
func ToTransactionDetailDTO(e entity.Payment) TransactionDetailDTO {
return TransactionDetailDTO{
TransactionListDTO: ToTransactionListDTO(e),
}
}
func partyFromPayment(e entity.Payment) Party {
party := Party{
Id: e.PartyId,
Type: e.PartyType,
}
switch utils.PaymentParty(e.PartyType) {
case utils.PaymentPartyCustomer:
if e.Customer != nil && e.Customer.Id != 0 {
party.Name = e.Customer.Name
party.AccountNumber = e.Customer.AccountNumber
}
case utils.PaymentPartySupplier:
if e.Supplier != nil && e.Supplier.Id != 0 {
party.Name = e.Supplier.Name
if e.Supplier.AccountNumber != nil {
party.AccountNumber = *e.Supplier.AccountNumber
}
}
}
return party
}
func bankFromPayment(e entity.Payment) bankDTO.BankRelationDTO {
if e.BankWarehouse.Id == 0 {
return bankDTO.BankRelationDTO{}
}
return bankDTO.ToBankRelationDTO(e.BankWarehouse)
}
func userFromPayment(e entity.Payment) userDTO.UserRelationDTO {
if e.CreatedUser.Id == 0 {
return userDTO.UserRelationDTO{}
}
return userDTO.ToUserRelationDTO(e.CreatedUser)
}
func paymentCodeFromPayment(e entity.Payment) string {
if e.PaymentCode != "" {
return e.PaymentCode
}
if e.ReferenceNumber != nil {
return *e.ReferenceNumber
}
return ""
}
func transactionTypeFromPayment(e entity.Payment) string {
if e.TransactionType != "" {
return e.TransactionType
}
return e.Direction
}
func paymentAmounts(direction string, nominal float64) (float64, float64) {
switch strings.ToUpper(direction) {
case "IN":
return 0, nominal
case "OUT":
return nominal, 0
default:
return 0, 0
}
}
@@ -0,0 +1,42 @@
package transactions
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
rTransaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/repositories"
sTransaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransactionModule struct{}
func (TransactionModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transactionRepo := rTransaction.NewTransactionRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPayment, utils.PaymentApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register payment approval workflow: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInitial, utils.InitialApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register initial approval workflow: %v", err))
}
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowInjection, utils.InjectionApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register injection approval workflow: %v", err))
}
transactionService := sTransaction.NewTransactionService(transactionRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate)
TransactionRoutes(router, userService, transactionService)
}
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type TransactionRepository interface {
repository.BaseRepository[entity.Payment]
}
type TransactionRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Payment]
}
func NewTransactionRepository(db *gorm.DB) TransactionRepository {
return &TransactionRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Payment](db),
}
}
@@ -0,0 +1,21 @@
package transactions
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/controllers"
transaction "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransactionRoutes(v1 fiber.Router, u user.UserService, s transaction.TransactionService) {
ctrl := controller.NewTransactionController(s)
route := v1.Group("/transactions")
route.Use(m.Auth(u))
route.Get("/",m.RequirePermissions(m.P_Finances_Transaction_GetAll), ctrl.GetAll)
route.Get("/:id",m.RequirePermissions(m.P_Finances_Transaction_GetOne), ctrl.GetOne)
route.Delete("/:id",m.RequirePermissions(m.P_Finances_Transaction_DeleteOne), ctrl.DeleteOne)
}
@@ -0,0 +1,175 @@
package service
import (
"context"
"errors"
"strings"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/finance/transactions/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type TransactionService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Payment, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type transactionService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransactionRepository
ApprovalSvc commonSvc.ApprovalService
approvalWorkflows map[string]approvalutils.ApprovalWorkflowKey
}
func NewTransactionService(
repo repository.TransactionRepository,
approvalSvc commonSvc.ApprovalService,
validate *validator.Validate,
) TransactionService {
return &transactionService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ApprovalSvc: approvalSvc,
approvalWorkflows: map[string]approvalutils.ApprovalWorkflowKey{
string(utils.TransactionTypeSaldoAwal): utils.ApprovalWorkflowInitial,
string(utils.TransactionTypeInjection): utils.ApprovalWorkflowInjection,
},
}
}
func (s transactionService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser").
Preload("BankWarehouse").
Preload("Customer").
Preload("Supplier")
}
func (s transactionService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Payment, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transactions, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
like := "%" + strings.ToLower(strings.TrimSpace(params.Search)) + "%"
return db.Where(
`LOWER(payment_code) LIKE ? OR
LOWER(COALESCE(reference_number, '')) LIKE ? OR
LOWER(COALESCE(transaction_type, '')) LIKE ? OR
LOWER(COALESCE(notes, '')) LIKE ?`,
like, like, like, like,
)
}
return db.Order("payment_date DESC").Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get transactions: %+v", err)
return nil, 0, err
}
s.attachApprovals(c.Context(), transactions)
return transactions, total, nil
}
func (s transactionService) GetOne(c *fiber.Ctx, id uint) (*entity.Payment, error) {
transaction, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transaction not found")
}
if err != nil {
s.Log.Errorf("Failed get transaction by id: %+v", err)
return nil, err
}
if s.ApprovalSvc != nil {
approval, err := s.ApprovalSvc.LatestByTarget(
c.Context(),
s.workflowForTransaction(transaction),
id,
s.approvalQueryModifier(),
)
if err != nil {
s.Log.Warnf("Unable to load latest approval for transaction %d: %+v", id, err)
} else {
transaction.LatestApproval = approval
}
}
return transaction, nil
}
func (s transactionService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Transaction not found")
}
s.Log.Errorf("Failed to delete transaction: %+v", err)
return err
}
return nil
}
func (s transactionService) attachApprovals(ctx context.Context, transactions []entity.Payment) {
if s.ApprovalSvc == nil || len(transactions) == 0 {
return
}
workflowIDs := map[approvalutils.ApprovalWorkflowKey][]uint{}
for _, transaction := range transactions {
workflow := s.workflowForTransaction(&transaction)
workflowIDs[workflow] = append(workflowIDs[workflow], transaction.Id)
}
approvalByWorkflow := make(map[approvalutils.ApprovalWorkflowKey]map[uint]*entity.Approval, len(workflowIDs))
for workflow, ids := range workflowIDs {
if len(ids) == 0 {
continue
}
approvals, err := s.ApprovalSvc.LatestByTargets(ctx, workflow, ids, s.approvalQueryModifier())
if err != nil {
s.Log.Warnf("Unable to load latest approvals for transactions: %+v", err)
continue
}
approvalByWorkflow[workflow] = approvals
}
for i := range transactions {
workflow := s.workflowForTransaction(&transactions[i])
if approvals, ok := approvalByWorkflow[workflow]; ok {
transactions[i].LatestApproval = approvals[transactions[i].Id]
}
}
}
func (s transactionService) workflowForTransaction(transaction *entity.Payment) approvalutils.ApprovalWorkflowKey {
if transaction == nil {
return utils.ApprovalWorkflowPayment
}
transactionType := strings.TrimSpace(strings.ToUpper(transaction.TransactionType))
if transactionType == "" {
return utils.ApprovalWorkflowPayment
}
if workflow, ok := s.approvalWorkflows[transactionType]; ok {
return workflow
}
return utils.ApprovalWorkflowPayment
}
func (s transactionService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
@@ -0,0 +1,15 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -84,6 +84,7 @@ func ToKandangListDTO(e entity.Kandang) KandangListDTO {
Name: e.Name, Name: e.Name,
Status: e.Status, Status: e.Status,
Location: location, Location: location,
Capacity: e.Capacity,
Pic: pic, Pic: pic,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
@@ -40,7 +40,7 @@ func (u *ProductionStandardController) GetAll(c *fiber.Ctx) error {
} }
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductionStandardListDTO]{ JSON(response.SuccessWithPaginate[dto.ProductionStandardRelationDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get all productionStandards successfully", Message: "Get all productionStandards successfully",
@@ -7,7 +7,7 @@ import (
// === DTO Structs === // === DTO Structs ===
type ProductionStandardListDTO struct { type ProductionStandardRelationDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ProjectCategory string `json:"project_category"` ProjectCategory string `json:"project_category"`
@@ -15,7 +15,7 @@ type ProductionStandardListDTO struct {
} }
type ProductionStandardDetailDTO struct { type ProductionStandardDetailDTO struct {
ProductionStandardListDTO ProductionStandardRelationDTO
Details []WeeklyProductionStandardDTO `json:"details"` Details []WeeklyProductionStandardDTO `json:"details"`
} }
@@ -44,14 +44,14 @@ type WeeklyProductionStandardDTO struct {
// === Mapper Functions === // === Mapper Functions ===
func ToProductionStandardListDTO(e entity.ProductionStandard) ProductionStandardListDTO { func ToProductionStandardListDTO(e entity.ProductionStandard) ProductionStandardRelationDTO {
var createdUser *userDTO.UserRelationDTO var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser) mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
return ProductionStandardListDTO{ return ProductionStandardRelationDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
ProjectCategory: e.ProjectCategory, ProjectCategory: e.ProjectCategory,
@@ -59,8 +59,16 @@ func ToProductionStandardListDTO(e entity.ProductionStandard) ProductionStandard
} }
} }
func ToProductionStandardListDTOs(e []entity.ProductionStandard) []ProductionStandardListDTO { func ToProductionStandardRelationDTO(e entity.ProductionStandard) ProductionStandardRelationDTO {
result := make([]ProductionStandardListDTO, len(e)) return ProductionStandardRelationDTO{
Id: e.Id,
Name: e.Name,
ProjectCategory: e.ProjectCategory,
}
}
func ToProductionStandardListDTOs(e []entity.ProductionStandard) []ProductionStandardRelationDTO {
result := make([]ProductionStandardRelationDTO, len(e))
for i, r := range e { for i, r := range e {
result[i] = ToProductionStandardListDTO(r) result[i] = ToProductionStandardListDTO(r)
} }
@@ -152,7 +160,7 @@ func ToProductionStandardDetailDTO(
productionStandardDetails []entity.ProductionStandardDetail, productionStandardDetails []entity.ProductionStandardDetail,
) ProductionStandardDetailDTO { ) ProductionStandardDetailDTO {
return ProductionStandardDetailDTO{ return ProductionStandardDetailDTO{
ProductionStandardListDTO: ToProductionStandardListDTO(standard), ProductionStandardRelationDTO: ToProductionStandardRelationDTO(standard),
Details: ToWeeklyProductionStandardDTOsWithDetails(growthDetails, productionStandardDetails), Details: ToWeeklyProductionStandardDTOsWithDetails(growthDetails, productionStandardDetails),
} }
} }
@@ -15,9 +15,9 @@ func ProductionStandardRoutes(v1 fiber.Router, u user.UserService, s productionS
route := v1.Group("/production-standards") route := v1.Group("/production-standards")
route.Use(m.Auth(u)) route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_Production_Standart_GetAll), ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_Production_Standart_CreateOne), ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_Production_Standart_GetOne), ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", m.RequirePermissions(m.P_Production_Standart_UpdateOne), ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", m.RequirePermissions(m.P_Production_Standart_DeleteOne), ctrl.DeleteOne)
} }
@@ -10,6 +10,7 @@ import (
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" chickinDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto"
@@ -30,6 +31,7 @@ type ProjectFlockDTO struct {
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
@@ -82,6 +84,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO
Area: pf.Area, Area: pf.Area,
Category: pf.Category, Category: pf.Category,
Fcr: pf.Fcr, Fcr: pf.Fcr,
ProductionStandard: pf.ProductionStandard,
Location: pf.Location, Location: pf.Location,
CreatedUser: pf.CreatedUser, CreatedUser: pf.CreatedUser,
CreatedAt: pf.CreatedAt, CreatedAt: pf.CreatedAt,
@@ -10,6 +10,7 @@ import (
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto" nonstockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/nonstocks/dto"
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" // pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
@@ -28,6 +29,7 @@ type ProjectFlockListDTO struct {
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"` ProjectBudgets []ProjectBudgetDTO `json:"project_budgets,omitempty"`
@@ -103,6 +105,12 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
fcrSummary = &mapped fcrSummary = &mapped
} }
var productionStandardSummary *productionStandardDTO.ProductionStandardRelationDTO
if e.ProductionStandard.Id != 0 {
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProductionStandard)
productionStandardSummary = &mapped
}
var locationSummary *locationDTO.LocationRelationDTO var locationSummary *locationDTO.LocationRelationDTO
if e.Location.Id != 0 { if e.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.Location) mapped := locationDTO.ToLocationRelationDTO(e.Location)
@@ -122,6 +130,7 @@ func ToProjectFlockListDTOWithPeriod(e entity.ProjectFlock, period int) ProjectF
ProjectBudgets: ToProjectBudgetDTOs(e.Budgets), ProjectBudgets: ToProjectBudgetDTOs(e.Budgets),
Category: e.Category, Category: e.Category,
Fcr: fcrSummary, Fcr: fcrSummary,
ProductionStandard: productionStandardSummary,
Location: locationSummary, Location: locationSummary,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,
@@ -6,6 +6,7 @@ import (
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
productionStandardDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
@@ -19,6 +20,7 @@ type ProjectFlockWithPivotDTO struct {
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"` Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
Category string `json:"category"` Category string `json:"category"`
Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"` Fcr *fcrDTO.FcrRelationDTO `json:"fcr,omitempty"`
ProductionStandard *productionStandardDTO.ProductionStandardRelationDTO `json:"production_standard,omitempty"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"` Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"` CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
@@ -61,6 +63,10 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr) mapped := fcrDTO.ToFcrRelationDTO(e.ProjectFlock.Fcr)
pfLocal.Fcr = &mapped pfLocal.Fcr = &mapped
} }
if e.ProjectFlock.ProductionStandard.Id != 0 {
mapped := productionStandardDTO.ToProductionStandardRelationDTO(e.ProjectFlock.ProductionStandard)
pfLocal.ProductionStandard = &mapped
}
if e.ProjectFlock.Location.Id != 0 { if e.ProjectFlock.Location.Id != 0 {
mapped := locationDTO.ToLocationRelationDTO(e.ProjectFlock.Location) mapped := locationDTO.ToLocationRelationDTO(e.ProjectFlock.Location)
pfLocal.Location = &mapped pfLocal.Location = &mapped
@@ -22,6 +22,7 @@ type ProjectflockRepository interface {
GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error) GetActiveByLocationID(ctx context.Context, locationID uint64) ([]entity.ProjectFlock, error)
AreaExists(ctx context.Context, id uint) (bool, error) AreaExists(ctx context.Context, id uint) (bool, error)
FcrExists(ctx context.Context, id uint) (bool, error) FcrExists(ctx context.Context, id uint) (bool, error)
ProductionStandardExists(ctx context.Context, id uint) (bool, error)
LocationExists(ctx context.Context, id uint) (bool, error) LocationExists(ctx context.Context, id uint) (bool, error)
} }
type KandangPeriodRow struct { type KandangPeriodRow struct {
@@ -52,6 +53,7 @@ func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Area"). Preload("Area").
Preload("Fcr"). Preload("Fcr").
Preload("ProductionStandard").
Preload("Location"). Preload("Location").
Preload("Kandangs"). Preload("Kandangs").
Preload("KandangHistory"). Preload("KandangHistory").
@@ -118,12 +120,14 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
return db. return db.
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id"). Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id"). Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
Joins("LEFT JOIN production_standards ON production_standards.id = project_flocks.production_standard_id").
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id"). Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by"). Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
Where(` Where(`
LOWER(areas.name) LIKE ? LOWER(areas.name) LIKE ?
OR LOWER(project_flocks.category) LIKE ? OR LOWER(project_flocks.category) LIKE ?
OR LOWER(fcrs.name) LIKE ? OR LOWER(fcrs.name) LIKE ?
OR LOWER(production_standards.name) LIKE ?
OR LOWER(locations.name) LIKE ? OR LOWER(locations.name) LIKE ?
OR LOWER(locations.address) LIKE ? OR LOWER(locations.address) LIKE ?
OR LOWER(created_users.name) LIKE ? OR LOWER(created_users.name) LIKE ?
@@ -153,6 +157,7 @@ func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch s
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery, likeQuery,
likeQuery,
) )
} }
@@ -164,6 +169,10 @@ func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bo
return repository.Exists[entity.Fcr](ctx, r.DB(), id) return repository.Exists[entity.Fcr](ctx, r.DB(), id)
} }
func (r *ProjectflockRepositoryImpl) ProductionStandardExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductionStandard](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) { func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Location](ctx, r.DB(), id) return repository.Exists[entity.Location](ctx, r.DB(), id)
} }
@@ -249,6 +249,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists}, commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists}, commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
commonSvc.RelationCheck{Name: "Production Standard", ID: &req.ProductionStandardId, Exists: s.Repository.ProductionStandardExists},
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists}, commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
); err != nil { ); err != nil {
return nil, err return nil, err
@@ -300,6 +301,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
AreaId: req.AreaId, AreaId: req.AreaId,
Category: cat, Category: cat,
FcrId: req.FcrId, FcrId: req.FcrId,
ProductionStandardId: req.ProductionStandardId,
LocationId: req.LocationId, LocationId: req.LocationId,
CreatedBy: actorID, CreatedBy: actorID,
} }
@@ -5,6 +5,7 @@ type Create struct {
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"` Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"` ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
+2 -1
View File
@@ -12,6 +12,7 @@ import (
closings "gitlab.com/mbugroup/lti-api.git/internal/modules/closings" closings "gitlab.com/mbugroup/lti-api.git/internal/modules/closings"
constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants" constants "gitlab.com/mbugroup/lti-api.git/internal/modules/constants"
expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses" expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses"
finance "gitlab.com/mbugroup/lti-api.git/internal/modules/finance"
inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory" inventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory"
marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing" marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing"
master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" master "gitlab.com/mbugroup/lti-api.git/internal/modules/master"
@@ -44,7 +45,7 @@ func Routes(app *fiber.App, db *gorm.DB) {
ssoModule.Module{}, ssoModule.Module{},
closings.ClosingModule{}, closings.ClosingModule{},
repports.RepportModule{}, repports.RepportModule{},
finance.FinanceModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
+109 -1
View File
@@ -148,6 +148,45 @@ const (
ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP" ExpenseCategoryNonBOP ExpenseCategory = "NON-BOP"
) )
// -------------------------------------------------------------------
// Payment Method
// -------------------------------------------------------------------
type PaymentMethod string
const (
PaymentMethodTransfer PaymentMethod = "TRANSFER"
PaymentMethodCash PaymentMethod = "CASH"
PaymentMethodCard PaymentMethod = "CARD"
PaymentMethodCheque PaymentMethod = "CHEQUE"
PaymentMethodSaldo PaymentMethod = "SALDO"
)
// -------------------------------------------------------------------
// Trasaction Type
// -------------------------------------------------------------------
type TransactionType string
const (
TransactionTypePenjualan TransactionType = "PENJUALAN"
TransactionTypePembelian TransactionType = "PEMBELIAN"
TransactionTypeBiaya TransactionType = "BIAYA"
TransactionTypeInjection TransactionType = "INJECTION"
TransactionTypeSaldoAwal TransactionType = "SALDO_AWAL"
)
// -------------------------------------------------------------------
// Payment Party
// -------------------------------------------------------------------
type PaymentParty string
const (
PaymentPartyCustomer PaymentParty = "CUSTOMER"
PaymentPartySupplier PaymentParty = "SUPPLIER"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Kandang Status // Kandang Status
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -316,6 +355,51 @@ var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
ExpenseStepSelesai: "Selesai", ExpenseStepSelesai: "Selesai",
} }
// -------------------------------------------------------------------
// Payment Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowPayment approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PAYMENTS")
PaymentStepPengajuan approvalutils.ApprovalStep = 1
PaymentStepDisetujui approvalutils.ApprovalStep = 2
)
var PaymentApprovalSteps = map[approvalutils.ApprovalStep]string{
PaymentStepPengajuan: "Pengajuan",
PaymentStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// Inisial Balance Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowInitial approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("INITIAL_BALANCES")
InitialStepPengajuan approvalutils.ApprovalStep = 1
InitialStepDisetujui approvalutils.ApprovalStep = 2
)
var InitialApprovalSteps = map[approvalutils.ApprovalStep]string{
InitialStepPengajuan: "Pengajuan",
InitialStepDisetujui: "Disetujui",
}
// -------------------------------------------------------------------
// Injection Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowInjection approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("INJECTIONS")
InjectionStepPengajuan approvalutils.ApprovalStep = 1
InjectionStepDisetujui approvalutils.ApprovalStep = 2
)
var InjectionApprovalSteps = map[approvalutils.ApprovalStep]string{
InjectionStepPengajuan: "Pengajuan",
InjectionStepDisetujui: "Disetujui",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Document // Document
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -467,7 +551,31 @@ func IsValidExpenseCategory(v string) bool {
return false return false
} }
// e xample use func IsValidPaymentMethod(v string) bool {
switch PaymentMethod(v) {
case PaymentMethodTransfer, PaymentMethodCash, PaymentMethodCard, PaymentMethodCheque, PaymentMethodSaldo:
return true
}
return false
}
func IsValidTransactionType(v string) bool {
switch TransactionType(v) {
case TransactionTypePenjualan, TransactionTypePembelian, TransactionTypeBiaya, TransactionTypeInjection, TransactionTypeSaldoAwal:
return true
}
return false
}
func IsValidPaymentParty(v string) bool {
switch PaymentParty(v) {
case PaymentPartyCustomer, PaymentPartySupplier:
return true
}
return false
}
// example use
// Recording helper // Recording helper
+7 -12
View File
@@ -1,7 +1,7 @@
{{define "route"}}package {{Kebab .Entity}}s {{define "route"}}package {{Kebab .Entity}}s
import ( import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/controllers"
{{Camel .Entity}} "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/services" {{Camel .Entity}} "gitlab.com/mbugroup/lti-api.git/internal/modules/{{Kebab .FeatName}}s/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,17 +13,12 @@ func {{Pascal .Entity}}Routes(v1 fiber.Router, u user.UserService, s {{Camel .En
ctrl := controller.New{{Pascal .Entity}}Controller(s) ctrl := controller.New{{Pascal .Entity}}Controller(s)
route := v1.Group("/{{Kebab .Entity}}s") route := v1.Group("/{{Kebab .Entity}}s")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_FinanceGetAll), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_FinanceGetOne), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_FinanceCreateOne), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) route.Patch("/:id", m.RequirePermissions(m.P_FinanceUpdateOne), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) route.Delete("/:id", m.RequirePermissions(m.P_FinanceDeleteOne), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
} }
{{end}} {{end}}