Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
652
ai-compliance-sdk/internal/funding/postgres_store.go
Normal file
652
ai-compliance-sdk/internal/funding/postgres_store.go
Normal file
@@ -0,0 +1,652 @@
|
||||
package funding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// PostgresStore implements Store using PostgreSQL
|
||||
type PostgresStore struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewPostgresStore creates a new PostgreSQL store
|
||||
func NewPostgresStore(pool *pgxpool.Pool) *PostgresStore {
|
||||
return &PostgresStore{pool: pool}
|
||||
}
|
||||
|
||||
// CreateApplication creates a new funding application
|
||||
func (s *PostgresStore) CreateApplication(ctx context.Context, app *FundingApplication) error {
|
||||
app.ID = uuid.New()
|
||||
app.CreatedAt = time.Now()
|
||||
app.UpdatedAt = time.Now()
|
||||
app.TotalSteps = 8 // Default 8-step wizard
|
||||
|
||||
// Generate application number
|
||||
app.ApplicationNumber = s.generateApplicationNumber(app.FundingProgram, app.SchoolProfile)
|
||||
|
||||
// Marshal JSON fields
|
||||
wizardDataJSON, err := json.Marshal(app.WizardData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal wizard data: %w", err)
|
||||
}
|
||||
|
||||
schoolProfileJSON, err := json.Marshal(app.SchoolProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal school profile: %w", err)
|
||||
}
|
||||
|
||||
projectPlanJSON, err := json.Marshal(app.ProjectPlan)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal project plan: %w", err)
|
||||
}
|
||||
|
||||
budgetJSON, err := json.Marshal(app.Budget)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal budget: %w", err)
|
||||
}
|
||||
|
||||
timelineJSON, err := json.Marshal(app.Timeline)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal timeline: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO funding_applications (
|
||||
id, tenant_id, application_number, title, funding_program, status,
|
||||
current_step, total_steps, wizard_data,
|
||||
school_profile, project_plan, budget, timeline,
|
||||
requested_amount, own_contribution,
|
||||
created_at, updated_at, created_by, updated_by
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9,
|
||||
$10, $11, $12, $13,
|
||||
$14, $15,
|
||||
$16, $17, $18, $19
|
||||
)
|
||||
`
|
||||
|
||||
_, err = s.pool.Exec(ctx, query,
|
||||
app.ID, app.TenantID, app.ApplicationNumber, app.Title, app.FundingProgram, app.Status,
|
||||
app.CurrentStep, app.TotalSteps, wizardDataJSON,
|
||||
schoolProfileJSON, projectPlanJSON, budgetJSON, timelineJSON,
|
||||
app.RequestedAmount, app.OwnContribution,
|
||||
app.CreatedAt, app.UpdatedAt, app.CreatedBy, app.UpdatedBy,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create application: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetApplication retrieves an application by ID
|
||||
func (s *PostgresStore) GetApplication(ctx context.Context, id uuid.UUID) (*FundingApplication, error) {
|
||||
query := `
|
||||
SELECT
|
||||
id, tenant_id, application_number, title, funding_program, status,
|
||||
current_step, total_steps, wizard_data,
|
||||
school_profile, project_plan, budget, timeline,
|
||||
requested_amount, own_contribution, approved_amount,
|
||||
created_at, updated_at, submitted_at, created_by, updated_by
|
||||
FROM funding_applications
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
var app FundingApplication
|
||||
var wizardDataJSON, schoolProfileJSON, projectPlanJSON, budgetJSON, timelineJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, query, id).Scan(
|
||||
&app.ID, &app.TenantID, &app.ApplicationNumber, &app.Title, &app.FundingProgram, &app.Status,
|
||||
&app.CurrentStep, &app.TotalSteps, &wizardDataJSON,
|
||||
&schoolProfileJSON, &projectPlanJSON, &budgetJSON, &timelineJSON,
|
||||
&app.RequestedAmount, &app.OwnContribution, &app.ApprovedAmount,
|
||||
&app.CreatedAt, &app.UpdatedAt, &app.SubmittedAt, &app.CreatedBy, &app.UpdatedBy,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("application not found: %s", id)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get application: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal JSON fields
|
||||
if len(wizardDataJSON) > 0 {
|
||||
if err := json.Unmarshal(wizardDataJSON, &app.WizardData); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal wizard data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(schoolProfileJSON) > 0 {
|
||||
app.SchoolProfile = &SchoolProfile{}
|
||||
if err := json.Unmarshal(schoolProfileJSON, app.SchoolProfile); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal school profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(projectPlanJSON) > 0 {
|
||||
app.ProjectPlan = &ProjectPlan{}
|
||||
if err := json.Unmarshal(projectPlanJSON, app.ProjectPlan); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal project plan: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(budgetJSON) > 0 {
|
||||
app.Budget = &Budget{}
|
||||
if err := json.Unmarshal(budgetJSON, app.Budget); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal budget: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(timelineJSON) > 0 {
|
||||
app.Timeline = &ProjectTimeline{}
|
||||
if err := json.Unmarshal(timelineJSON, app.Timeline); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal timeline: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Load attachments
|
||||
attachments, err := s.GetAttachments(ctx, id)
|
||||
if err == nil {
|
||||
app.Attachments = attachments
|
||||
}
|
||||
|
||||
return &app, nil
|
||||
}
|
||||
|
||||
// GetApplicationByNumber retrieves an application by number
|
||||
func (s *PostgresStore) GetApplicationByNumber(ctx context.Context, number string) (*FundingApplication, error) {
|
||||
query := `SELECT id FROM funding_applications WHERE application_number = $1`
|
||||
|
||||
var id uuid.UUID
|
||||
err := s.pool.QueryRow(ctx, query, number).Scan(&id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, fmt.Errorf("application not found: %s", number)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find application by number: %w", err)
|
||||
}
|
||||
|
||||
return s.GetApplication(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateApplication updates an existing application
|
||||
func (s *PostgresStore) UpdateApplication(ctx context.Context, app *FundingApplication) error {
|
||||
app.UpdatedAt = time.Now()
|
||||
|
||||
// Marshal JSON fields
|
||||
wizardDataJSON, _ := json.Marshal(app.WizardData)
|
||||
schoolProfileJSON, _ := json.Marshal(app.SchoolProfile)
|
||||
projectPlanJSON, _ := json.Marshal(app.ProjectPlan)
|
||||
budgetJSON, _ := json.Marshal(app.Budget)
|
||||
timelineJSON, _ := json.Marshal(app.Timeline)
|
||||
|
||||
query := `
|
||||
UPDATE funding_applications SET
|
||||
title = $2, funding_program = $3, status = $4,
|
||||
current_step = $5, wizard_data = $6,
|
||||
school_profile = $7, project_plan = $8, budget = $9, timeline = $10,
|
||||
requested_amount = $11, own_contribution = $12, approved_amount = $13,
|
||||
updated_at = $14, submitted_at = $15, updated_by = $16
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
result, err := s.pool.Exec(ctx, query,
|
||||
app.ID, app.Title, app.FundingProgram, app.Status,
|
||||
app.CurrentStep, wizardDataJSON,
|
||||
schoolProfileJSON, projectPlanJSON, budgetJSON, timelineJSON,
|
||||
app.RequestedAmount, app.OwnContribution, app.ApprovedAmount,
|
||||
app.UpdatedAt, app.SubmittedAt, app.UpdatedBy,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update application: %w", err)
|
||||
}
|
||||
|
||||
if result.RowsAffected() == 0 {
|
||||
return fmt.Errorf("application not found: %s", app.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteApplication soft-deletes an application
|
||||
func (s *PostgresStore) DeleteApplication(ctx context.Context, id uuid.UUID) error {
|
||||
query := `UPDATE funding_applications SET status = 'ARCHIVED', updated_at = $2 WHERE id = $1`
|
||||
result, err := s.pool.Exec(ctx, query, id, time.Now())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete application: %w", err)
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return fmt.Errorf("application not found: %s", id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListApplications returns a paginated list of applications
|
||||
func (s *PostgresStore) ListApplications(ctx context.Context, tenantID uuid.UUID, filter ApplicationFilter) (*ApplicationListResponse, error) {
|
||||
// Build query with filters
|
||||
query := `
|
||||
SELECT
|
||||
id, tenant_id, application_number, title, funding_program, status,
|
||||
current_step, total_steps, wizard_data,
|
||||
school_profile, project_plan, budget, timeline,
|
||||
requested_amount, own_contribution, approved_amount,
|
||||
created_at, updated_at, submitted_at, created_by, updated_by
|
||||
FROM funding_applications
|
||||
WHERE tenant_id = $1 AND status != 'ARCHIVED'
|
||||
`
|
||||
args := []interface{}{tenantID}
|
||||
argIndex := 2
|
||||
|
||||
if filter.Status != nil {
|
||||
query += fmt.Sprintf(" AND status = $%d", argIndex)
|
||||
args = append(args, *filter.Status)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
if filter.FundingProgram != nil {
|
||||
query += fmt.Sprintf(" AND funding_program = $%d", argIndex)
|
||||
args = append(args, *filter.FundingProgram)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// Count total
|
||||
countQuery := `SELECT COUNT(*) FROM funding_applications WHERE tenant_id = $1 AND status != 'ARCHIVED'`
|
||||
var total int
|
||||
s.pool.QueryRow(ctx, countQuery, tenantID).Scan(&total)
|
||||
|
||||
// Add sorting and pagination
|
||||
sortBy := "created_at"
|
||||
if filter.SortBy != "" {
|
||||
sortBy = filter.SortBy
|
||||
}
|
||||
sortOrder := "DESC"
|
||||
if filter.SortOrder == "asc" {
|
||||
sortOrder = "ASC"
|
||||
}
|
||||
query += fmt.Sprintf(" ORDER BY %s %s", sortBy, sortOrder)
|
||||
|
||||
if filter.PageSize <= 0 {
|
||||
filter.PageSize = 20
|
||||
}
|
||||
if filter.Page <= 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
offset := (filter.Page - 1) * filter.PageSize
|
||||
query += fmt.Sprintf(" LIMIT %d OFFSET %d", filter.PageSize, offset)
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list applications: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []FundingApplication
|
||||
for rows.Next() {
|
||||
var app FundingApplication
|
||||
var wizardDataJSON, schoolProfileJSON, projectPlanJSON, budgetJSON, timelineJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&app.ID, &app.TenantID, &app.ApplicationNumber, &app.Title, &app.FundingProgram, &app.Status,
|
||||
&app.CurrentStep, &app.TotalSteps, &wizardDataJSON,
|
||||
&schoolProfileJSON, &projectPlanJSON, &budgetJSON, &timelineJSON,
|
||||
&app.RequestedAmount, &app.OwnContribution, &app.ApprovedAmount,
|
||||
&app.CreatedAt, &app.UpdatedAt, &app.SubmittedAt, &app.CreatedBy, &app.UpdatedBy,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan application: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal JSON fields
|
||||
if len(schoolProfileJSON) > 0 {
|
||||
app.SchoolProfile = &SchoolProfile{}
|
||||
json.Unmarshal(schoolProfileJSON, app.SchoolProfile)
|
||||
}
|
||||
|
||||
apps = append(apps, app)
|
||||
}
|
||||
|
||||
return &ApplicationListResponse{
|
||||
Applications: apps,
|
||||
Total: total,
|
||||
Page: filter.Page,
|
||||
PageSize: filter.PageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchApplications searches applications by text
|
||||
func (s *PostgresStore) SearchApplications(ctx context.Context, tenantID uuid.UUID, query string) ([]FundingApplication, error) {
|
||||
searchQuery := `
|
||||
SELECT id FROM funding_applications
|
||||
WHERE tenant_id = $1
|
||||
AND status != 'ARCHIVED'
|
||||
AND (
|
||||
title ILIKE $2
|
||||
OR application_number ILIKE $2
|
||||
OR school_profile::text ILIKE $2
|
||||
)
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 50
|
||||
`
|
||||
|
||||
rows, err := s.pool.Query(ctx, searchQuery, tenantID, "%"+query+"%")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search applications: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var apps []FundingApplication
|
||||
for rows.Next() {
|
||||
var id uuid.UUID
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
continue
|
||||
}
|
||||
app, err := s.GetApplication(ctx, id)
|
||||
if err == nil {
|
||||
apps = append(apps, *app)
|
||||
}
|
||||
}
|
||||
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
// SaveWizardStep saves data for a wizard step
|
||||
func (s *PostgresStore) SaveWizardStep(ctx context.Context, appID uuid.UUID, step int, data map[string]interface{}) error {
|
||||
// Get current wizard data
|
||||
app, err := s.GetApplication(ctx, appID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize wizard data if nil
|
||||
if app.WizardData == nil {
|
||||
app.WizardData = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Merge step data
|
||||
stepKey := fmt.Sprintf("step_%d", step)
|
||||
app.WizardData[stepKey] = data
|
||||
app.CurrentStep = step
|
||||
|
||||
// Update application
|
||||
return s.UpdateApplication(ctx, app)
|
||||
}
|
||||
|
||||
// GetWizardProgress returns the wizard progress
|
||||
func (s *PostgresStore) GetWizardProgress(ctx context.Context, appID uuid.UUID) (*WizardProgress, error) {
|
||||
app, err := s.GetApplication(ctx, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
progress := &WizardProgress{
|
||||
CurrentStep: app.CurrentStep,
|
||||
TotalSteps: app.TotalSteps,
|
||||
CompletedSteps: []int{},
|
||||
FormData: app.WizardData,
|
||||
LastSavedAt: app.UpdatedAt,
|
||||
}
|
||||
|
||||
// Determine completed steps from wizard data
|
||||
for i := 1; i <= app.TotalSteps; i++ {
|
||||
stepKey := fmt.Sprintf("step_%d", i)
|
||||
if _, ok := app.WizardData[stepKey]; ok {
|
||||
progress.CompletedSteps = append(progress.CompletedSteps, i)
|
||||
}
|
||||
}
|
||||
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
// AddAttachment adds an attachment to an application
|
||||
func (s *PostgresStore) AddAttachment(ctx context.Context, appID uuid.UUID, attachment *Attachment) error {
|
||||
attachment.ID = uuid.New()
|
||||
attachment.UploadedAt = time.Now()
|
||||
|
||||
query := `
|
||||
INSERT INTO funding_attachments (
|
||||
id, application_id, file_name, file_type, file_size,
|
||||
category, description, storage_path, uploaded_at, uploaded_by
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`
|
||||
|
||||
_, err := s.pool.Exec(ctx, query,
|
||||
attachment.ID, appID, attachment.FileName, attachment.FileType, attachment.FileSize,
|
||||
attachment.Category, attachment.Description, attachment.StoragePath,
|
||||
attachment.UploadedAt, attachment.UploadedBy,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAttachments returns all attachments for an application
|
||||
func (s *PostgresStore) GetAttachments(ctx context.Context, appID uuid.UUID) ([]Attachment, error) {
|
||||
query := `
|
||||
SELECT id, file_name, file_type, file_size, category, description, storage_path, uploaded_at, uploaded_by
|
||||
FROM funding_attachments
|
||||
WHERE application_id = $1
|
||||
ORDER BY uploaded_at DESC
|
||||
`
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var attachments []Attachment
|
||||
for rows.Next() {
|
||||
var a Attachment
|
||||
err := rows.Scan(&a.ID, &a.FileName, &a.FileType, &a.FileSize, &a.Category, &a.Description, &a.StoragePath, &a.UploadedAt, &a.UploadedBy)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
attachments = append(attachments, a)
|
||||
}
|
||||
|
||||
return attachments, nil
|
||||
}
|
||||
|
||||
// DeleteAttachment deletes an attachment
|
||||
func (s *PostgresStore) DeleteAttachment(ctx context.Context, attachmentID uuid.UUID) error {
|
||||
query := `DELETE FROM funding_attachments WHERE id = $1`
|
||||
_, err := s.pool.Exec(ctx, query, attachmentID)
|
||||
return err
|
||||
}
|
||||
|
||||
// AddHistoryEntry adds an audit trail entry
|
||||
func (s *PostgresStore) AddHistoryEntry(ctx context.Context, entry *ApplicationHistoryEntry) error {
|
||||
entry.ID = uuid.New()
|
||||
entry.PerformedAt = time.Now().Format(time.RFC3339)
|
||||
|
||||
oldValuesJSON, _ := json.Marshal(entry.OldValues)
|
||||
newValuesJSON, _ := json.Marshal(entry.NewValues)
|
||||
changedFieldsJSON, _ := json.Marshal(entry.ChangedFields)
|
||||
|
||||
query := `
|
||||
INSERT INTO funding_application_history (
|
||||
id, application_id, action, changed_fields, old_values, new_values,
|
||||
performed_by, performed_at, notes
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`
|
||||
|
||||
_, err := s.pool.Exec(ctx, query,
|
||||
entry.ID, entry.ApplicationID, entry.Action, changedFieldsJSON, oldValuesJSON, newValuesJSON,
|
||||
entry.PerformedBy, entry.PerformedAt, entry.Notes,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetHistory returns the audit trail for an application
|
||||
func (s *PostgresStore) GetHistory(ctx context.Context, appID uuid.UUID) ([]ApplicationHistoryEntry, error) {
|
||||
query := `
|
||||
SELECT id, application_id, action, changed_fields, old_values, new_values, performed_by, performed_at, notes
|
||||
FROM funding_application_history
|
||||
WHERE application_id = $1
|
||||
ORDER BY performed_at DESC
|
||||
`
|
||||
|
||||
rows, err := s.pool.Query(ctx, query, appID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var history []ApplicationHistoryEntry
|
||||
for rows.Next() {
|
||||
var entry ApplicationHistoryEntry
|
||||
var changedFieldsJSON, oldValuesJSON, newValuesJSON []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&entry.ID, &entry.ApplicationID, &entry.Action, &changedFieldsJSON, &oldValuesJSON, &newValuesJSON,
|
||||
&entry.PerformedBy, &entry.PerformedAt, &entry.Notes,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
json.Unmarshal(changedFieldsJSON, &entry.ChangedFields)
|
||||
json.Unmarshal(oldValuesJSON, &entry.OldValues)
|
||||
json.Unmarshal(newValuesJSON, &entry.NewValues)
|
||||
|
||||
history = append(history, entry)
|
||||
}
|
||||
|
||||
return history, nil
|
||||
}
|
||||
|
||||
// GetStatistics returns funding statistics
|
||||
func (s *PostgresStore) GetStatistics(ctx context.Context, tenantID uuid.UUID) (*FundingStatistics, error) {
|
||||
stats := &FundingStatistics{
|
||||
ByProgram: make(map[FundingProgram]int),
|
||||
ByState: make(map[FederalState]int),
|
||||
}
|
||||
|
||||
// Total and by status
|
||||
query := `
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(*) FILTER (WHERE status = 'DRAFT') as draft,
|
||||
COUNT(*) FILTER (WHERE status = 'SUBMITTED') as submitted,
|
||||
COUNT(*) FILTER (WHERE status = 'APPROVED') as approved,
|
||||
COUNT(*) FILTER (WHERE status = 'REJECTED') as rejected,
|
||||
COALESCE(SUM(requested_amount), 0) as total_requested,
|
||||
COALESCE(SUM(COALESCE(approved_amount, 0)), 0) as total_approved
|
||||
FROM funding_applications
|
||||
WHERE tenant_id = $1 AND status != 'ARCHIVED'
|
||||
`
|
||||
|
||||
err := s.pool.QueryRow(ctx, query, tenantID).Scan(
|
||||
&stats.TotalApplications, &stats.DraftCount, &stats.SubmittedCount,
|
||||
&stats.ApprovedCount, &stats.RejectedCount,
|
||||
&stats.TotalRequested, &stats.TotalApproved,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// By program
|
||||
programQuery := `
|
||||
SELECT funding_program, COUNT(*)
|
||||
FROM funding_applications
|
||||
WHERE tenant_id = $1 AND status != 'ARCHIVED'
|
||||
GROUP BY funding_program
|
||||
`
|
||||
rows, _ := s.pool.Query(ctx, programQuery, tenantID)
|
||||
for rows.Next() {
|
||||
var program FundingProgram
|
||||
var count int
|
||||
rows.Scan(&program, &count)
|
||||
stats.ByProgram[program] = count
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// SaveExportBundle saves an export bundle record
|
||||
func (s *PostgresStore) SaveExportBundle(ctx context.Context, bundle *ExportBundle) error {
|
||||
bundle.ID = uuid.New()
|
||||
bundle.GeneratedAt = time.Now()
|
||||
bundle.ExpiresAt = time.Now().Add(24 * time.Hour) // 24h expiry
|
||||
|
||||
documentsJSON, _ := json.Marshal(bundle.Documents)
|
||||
|
||||
query := `
|
||||
INSERT INTO funding_export_bundles (
|
||||
id, application_id, documents, generated_at, download_url, expires_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
|
||||
_, err := s.pool.Exec(ctx, query,
|
||||
bundle.ID, bundle.ApplicationID, documentsJSON,
|
||||
bundle.GeneratedAt, bundle.DownloadURL, bundle.ExpiresAt,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// GetExportBundle retrieves an export bundle
|
||||
func (s *PostgresStore) GetExportBundle(ctx context.Context, bundleID uuid.UUID) (*ExportBundle, error) {
|
||||
query := `
|
||||
SELECT id, application_id, documents, generated_at, download_url, expires_at
|
||||
FROM funding_export_bundles
|
||||
WHERE id = $1 AND expires_at > NOW()
|
||||
`
|
||||
|
||||
var bundle ExportBundle
|
||||
var documentsJSON []byte
|
||||
|
||||
err := s.pool.QueryRow(ctx, query, bundleID).Scan(
|
||||
&bundle.ID, &bundle.ApplicationID, &documentsJSON,
|
||||
&bundle.GeneratedAt, &bundle.DownloadURL, &bundle.ExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
json.Unmarshal(documentsJSON, &bundle.Documents)
|
||||
|
||||
return &bundle, nil
|
||||
}
|
||||
|
||||
// generateApplicationNumber creates a unique application number
|
||||
func (s *PostgresStore) generateApplicationNumber(program FundingProgram, school *SchoolProfile) string {
|
||||
year := time.Now().Year()
|
||||
state := "XX"
|
||||
if school != nil {
|
||||
state = string(school.FederalState)
|
||||
}
|
||||
|
||||
prefix := "FA"
|
||||
switch program {
|
||||
case FundingProgramDigitalPakt1:
|
||||
prefix = "DP1"
|
||||
case FundingProgramDigitalPakt2:
|
||||
prefix = "DP2"
|
||||
case FundingProgramLandesfoerderung:
|
||||
prefix = "LF"
|
||||
}
|
||||
|
||||
// Get sequence number
|
||||
var seq int
|
||||
s.pool.QueryRow(context.Background(),
|
||||
`SELECT COALESCE(MAX(CAST(SUBSTRING(application_number FROM '\d{5}$') AS INTEGER)), 0) + 1
|
||||
FROM funding_applications WHERE application_number LIKE $1`,
|
||||
fmt.Sprintf("%s-%s-%d-%%", prefix, state, year),
|
||||
).Scan(&seq)
|
||||
|
||||
return fmt.Sprintf("%s-%s-%d-%05d", prefix, state, year, seq)
|
||||
}
|
||||
Reference in New Issue
Block a user