package db import ( "context" "fmt" "time" "github.com/jackc/pgx/v5" ) // ItemApproval represents a row in the item_approvals table. type ItemApproval struct { ID string ItemID string WorkflowName string ECONumber *string State string // draft | pending | approved | rejected UpdatedAt time.Time UpdatedBy *string Signatures []ApprovalSignature // populated by WithSignatures methods } // ApprovalSignature represents a row in the approval_signatures table. type ApprovalSignature struct { ID string ApprovalID string Username string Role string Status string // pending | approved | rejected SignedAt *time.Time Comment *string } // ItemApprovalRepository provides item_approvals database operations. type ItemApprovalRepository struct { db *DB } // NewItemApprovalRepository creates a new item approval repository. func NewItemApprovalRepository(db *DB) *ItemApprovalRepository { return &ItemApprovalRepository{db: db} } // Create inserts a new approval row. The ID is populated on return. func (r *ItemApprovalRepository) Create(ctx context.Context, a *ItemApproval) error { return r.db.pool.QueryRow(ctx, ` INSERT INTO item_approvals (item_id, workflow_name, eco_number, state, updated_by) VALUES ($1, $2, $3, $4, $5) RETURNING id, updated_at `, a.ItemID, a.WorkflowName, a.ECONumber, a.State, a.UpdatedBy).Scan(&a.ID, &a.UpdatedAt) } // AddSignature inserts a new signature row. The ID is populated on return. func (r *ItemApprovalRepository) AddSignature(ctx context.Context, s *ApprovalSignature) error { return r.db.pool.QueryRow(ctx, ` INSERT INTO approval_signatures (approval_id, username, role, status) VALUES ($1, $2, $3, $4) RETURNING id `, s.ApprovalID, s.Username, s.Role, s.Status).Scan(&s.ID) } // GetWithSignatures returns a single approval with its signatures. func (r *ItemApprovalRepository) GetWithSignatures(ctx context.Context, approvalID string) (*ItemApproval, error) { a := &ItemApproval{} err := r.db.pool.QueryRow(ctx, ` SELECT id, item_id, workflow_name, eco_number, state, updated_at, updated_by FROM item_approvals WHERE id = $1 `, approvalID).Scan(&a.ID, &a.ItemID, &a.WorkflowName, &a.ECONumber, &a.State, &a.UpdatedAt, &a.UpdatedBy) if err != nil { if err == pgx.ErrNoRows { return nil, nil } return nil, fmt.Errorf("getting approval: %w", err) } sigs, err := r.signaturesForApproval(ctx, approvalID) if err != nil { return nil, err } a.Signatures = sigs return a, nil } // ListByItemWithSignatures returns all approvals for an item, each with signatures. func (r *ItemApprovalRepository) ListByItemWithSignatures(ctx context.Context, itemID string) ([]*ItemApproval, error) { rows, err := r.db.pool.Query(ctx, ` SELECT id, item_id, workflow_name, eco_number, state, updated_at, updated_by FROM item_approvals WHERE item_id = $1 ORDER BY updated_at DESC `, itemID) if err != nil { return nil, fmt.Errorf("listing approvals: %w", err) } defer rows.Close() var approvals []*ItemApproval var approvalIDs []string for rows.Next() { a := &ItemApproval{} if err := rows.Scan(&a.ID, &a.ItemID, &a.WorkflowName, &a.ECONumber, &a.State, &a.UpdatedAt, &a.UpdatedBy); err != nil { return nil, fmt.Errorf("scanning approval: %w", err) } approvals = append(approvals, a) approvalIDs = append(approvalIDs, a.ID) } if len(approvalIDs) == 0 { return approvals, nil } // Batch-fetch all signatures sigRows, err := r.db.pool.Query(ctx, ` SELECT id, approval_id, username, role, status, signed_at, comment FROM approval_signatures WHERE approval_id = ANY($1) ORDER BY username `, approvalIDs) if err != nil { return nil, fmt.Errorf("listing signatures: %w", err) } defer sigRows.Close() sigMap := make(map[string][]ApprovalSignature) for sigRows.Next() { var s ApprovalSignature if err := sigRows.Scan(&s.ID, &s.ApprovalID, &s.Username, &s.Role, &s.Status, &s.SignedAt, &s.Comment); err != nil { return nil, fmt.Errorf("scanning signature: %w", err) } sigMap[s.ApprovalID] = append(sigMap[s.ApprovalID], s) } for _, a := range approvals { a.Signatures = sigMap[a.ID] if a.Signatures == nil { a.Signatures = []ApprovalSignature{} } } return approvals, nil } // UpdateState updates the approval state and updated_by. func (r *ItemApprovalRepository) UpdateState(ctx context.Context, approvalID, state, updatedBy string) error { _, err := r.db.pool.Exec(ctx, ` UPDATE item_approvals SET state = $2, updated_by = $3, updated_at = now() WHERE id = $1 `, approvalID, state, updatedBy) if err != nil { return fmt.Errorf("updating approval state: %w", err) } return nil } // GetSignatureForUser returns the signature for a specific user on an approval. func (r *ItemApprovalRepository) GetSignatureForUser(ctx context.Context, approvalID, username string) (*ApprovalSignature, error) { s := &ApprovalSignature{} err := r.db.pool.QueryRow(ctx, ` SELECT id, approval_id, username, role, status, signed_at, comment FROM approval_signatures WHERE approval_id = $1 AND username = $2 `, approvalID, username).Scan(&s.ID, &s.ApprovalID, &s.Username, &s.Role, &s.Status, &s.SignedAt, &s.Comment) if err != nil { if err == pgx.ErrNoRows { return nil, nil } return nil, fmt.Errorf("getting signature: %w", err) } return s, nil } // UpdateSignature updates a signature's status, comment, and signed_at timestamp. func (r *ItemApprovalRepository) UpdateSignature(ctx context.Context, sigID, status string, comment *string) error { _, err := r.db.pool.Exec(ctx, ` UPDATE approval_signatures SET status = $2, comment = $3, signed_at = now() WHERE id = $1 `, sigID, status, comment) if err != nil { return fmt.Errorf("updating signature: %w", err) } return nil } // signaturesForApproval returns all signatures for a single approval. func (r *ItemApprovalRepository) signaturesForApproval(ctx context.Context, approvalID string) ([]ApprovalSignature, error) { rows, err := r.db.pool.Query(ctx, ` SELECT id, approval_id, username, role, status, signed_at, comment FROM approval_signatures WHERE approval_id = $1 ORDER BY username `, approvalID) if err != nil { return nil, fmt.Errorf("listing signatures: %w", err) } defer rows.Close() var sigs []ApprovalSignature for rows.Next() { var s ApprovalSignature if err := rows.Scan(&s.ID, &s.ApprovalID, &s.Username, &s.Role, &s.Status, &s.SignedAt, &s.Comment); err != nil { return nil, fmt.Errorf("scanning signature: %w", err) } sigs = append(sigs, s) } if sigs == nil { sigs = []ApprovalSignature{} } return sigs, nil }