Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 1.0.0
Status: šŸ“ Planned
Related Specs: FA-01, FA-02, FA-04, FA-08
Constitution Reference: Section 3.1 (Module Settings Pattern)

1. Overview

Every domain core should have a {core}_module_settings table and admin page for organization-level configuration. This document specifies the FA (Finance & Accounting) module settings implementation.

2. Database Schema

Table: fa_module_settings

CREATE TABLE fa_module_settings (
  -- Standard columns (required for all module settings)
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID NOT NULL UNIQUE REFERENCES pf_organizations(id),
  created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
  updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
  created_by UUID REFERENCES pf_profiles(id),
  updated_by UUID REFERENCES pf_profiles(id),
  
  -- FA-specific configuration
  -- General Ledger Settings
  default_cash_account_id UUID REFERENCES fa_accounts(id),
  default_ar_account_id UUID REFERENCES fa_accounts(id),
  default_ap_account_id UUID REFERENCES fa_accounts(id),
  default_retained_earnings_account_id UUID REFERENCES fa_accounts(id),
  retained_earnings_close_day INTEGER DEFAULT 31 CHECK (retained_earnings_close_day BETWEEN 1 AND 31),
  
  -- Fiscal Year Settings
  fiscal_year_start_month INTEGER DEFAULT 1 CHECK (fiscal_year_start_month BETWEEN 1 AND 12),
  fiscal_year_start_day INTEGER DEFAULT 1 CHECK (fiscal_year_start_day BETWEEN 1 AND 31),
  auto_create_periods BOOLEAN DEFAULT true,
  period_close_requires_approval BOOLEAN DEFAULT true,
  
  -- Accounts Payable Settings
  default_payment_terms_days INTEGER DEFAULT 30,
  require_po_for_bills BOOLEAN DEFAULT false,
  bill_approval_threshold NUMERIC(12,2), -- NULL = all bills require approval
  three_way_match_required BOOLEAN DEFAULT false,
  
  -- Accounts Receivable Settings
  default_invoice_terms_days INTEGER DEFAULT 30,
  late_fee_percent NUMERIC(5,2) DEFAULT 0.00,
  late_fee_grace_days INTEGER DEFAULT 0,
  auto_apply_payments BOOLEAN DEFAULT true,
  
  -- Bank Reconciliation Settings
  require_bank_rec_approval BOOLEAN DEFAULT true,
  bank_rec_variance_threshold NUMERIC(12,2) DEFAULT 0.01,
  
  -- Budget Settings
  budget_approval_required BOOLEAN DEFAULT true,
  budget_warning_threshold NUMERIC(5,2) DEFAULT 90.00, -- Warn at 90% of budget
  budget_critical_threshold NUMERIC(5,2) DEFAULT 100.00, -- Alert at 100% of budget
  allow_over_budget_posting BOOLEAN DEFAULT true,
  
  -- Reporting Settings
  default_report_format report_format DEFAULT 'pdf',
  consolidation_enabled BOOLEAN DEFAULT false,
  fund_accounting_enabled BOOLEAN DEFAULT true,
  
  -- Audit & Compliance
  require_dual_approval_over NUMERIC(12,2), -- NULL = no dual approval required
  restrict_prior_period_posting BOOLEAN DEFAULT true,
  days_back_posting_allowed INTEGER DEFAULT 5,
  
  -- Notifications
  notify_period_close BOOLEAN DEFAULT true,
  notify_budget_alerts BOOLEAN DEFAULT true,
  notify_large_transactions BOOLEAN DEFAULT true,
  large_transaction_threshold NUMERIC(12,2) DEFAULT 10000.00
);

-- RLS Policies
ALTER TABLE fa_module_settings ENABLE ROW LEVEL SECURITY;

CREATE POLICY "fa_module_settings_view"
  ON fa_module_settings FOR SELECT
  USING (has_org_access(auth.uid(), organization_id));

CREATE POLICY "fa_module_settings_org_admin_manage"
  ON fa_module_settings FOR ALL
  USING (
    EXISTS (
      SELECT 1 FROM pf_user_roles
      WHERE user_id = auth.uid()
        AND organization_id = fa_module_settings.organization_id
        AND role IN ('org_admin', 'platform_admin')
    )
  );

-- Trigger for updated_at
CREATE TRIGGER set_fa_module_settings_updated_at
  BEFORE UPDATE ON fa_module_settings
  FOR EACH ROW
  EXECUTE FUNCTION handle_updated_at();

-- Seed function
CREATE FUNCTION seed_fa_module_settings(_org_id UUID, _created_by UUID)
RETURNS UUID AS $$
DECLARE
  _settings_id UUID;
BEGIN
  INSERT INTO fa_module_settings (organization_id, created_by)
  VALUES (_org_id, _created_by)
  RETURNING id INTO _settings_id;
  
  RETURN _settings_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

3. Frontend Implementation

3.1 Admin Page

Location: src/cores/fa/pages/FASettings.tsx
Route: /fa/settings
Access: org_admin only
Layout:
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/ui/tabs';
import { FASettingsForm } from '../components/FASettingsForm';

export default function FASettings() {
  const { settings, isLoading } = useFAModuleSettings();
  
  return (
    <div className="container mx-auto py-6 space-y-6">
      <div>
        <h1 className="text-3xl font-bold">Finance Settings</h1>
        <p className="text-muted-foreground mt-1">
          Configure organization-wide finance and accounting defaults
        </p>
      </div>
      
      {isLoading ? (
        <div>Loading settings...</div>
      ) : (
        <FASettingsForm settings={settings} />
      )}
    </div>
  );
}

3.2 Settings Form Component

Location: src/cores/fa/components/FASettingsForm.tsx Tabs Structure:
  1. General Ledger - Default accounts, fiscal year settings
  2. Accounts Payable - Payment terms, approval thresholds, PO requirements
  3. Accounts Receivable - Invoice terms, late fees, payment application
  4. Bank Reconciliation - Approval requirements, variance thresholds
  5. Budgeting - Approval workflow, alert thresholds, over-budget controls
  6. Reporting - Default formats, consolidation, fund accounting
  7. Compliance - Dual approval, period restrictions, audit controls
  8. Notifications - Alert preferences for period close, budgets, large transactions
Example Tab:
<TabsContent value="general-ledger">
  <Card>
    <CardHeader>
      <CardTitle>General Ledger Settings</CardTitle>
      <CardDescription>
        Default accounts and fiscal year configuration
      </CardDescription>
    </CardHeader>
    <CardContent className="space-y-4">
      <FormField
        control={form.control}
        name="default_cash_account_id"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Default Cash Account</FormLabel>
            <AccountSelector
              {...field}
              accountTypes={['asset']}
              placeholder="Select cash account"
            />
            <FormDescription>
              Used for cash receipts and payments when no account specified
            </FormDescription>
          </FormItem>
        )}
      />
      
      {/* More fields... */}
    </CardContent>
  </Card>
</TabsContent>

3.3 Hook

Location: src/cores/fa/hooks/useFAModuleSettings.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from '@/shared/lib/hooks/use-toast';

export function useFAModuleSettings() {
  const queryClient = useQueryClient();
  
  const { data: settings, isLoading, error } = useQuery({
    queryKey: ['fa-module-settings'],
    queryFn: async () => {
      const { data: { user } } = await supabase.auth.getUser();
      if (!user) throw new Error('Not authenticated');
      
      // Get user's org
      const { data: roles } = await supabase
        .from('pf_user_roles')
        .select('organization_id')
        .eq('user_id', user.id)
        .limit(1)
        .single();
      
      if (!roles) throw new Error('No organization found');
      
      // Get or create settings
      let { data: settings, error } = await supabase
        .from('fa_module_settings')
        .select('*')
        .eq('organization_id', roles.organization_id)
        .single();
      
      if (error && error.code === 'PGRST116') {
        // Settings don't exist, create defaults
        const { data: newSettings, error: createError } = await supabase
          .rpc('seed_fa_module_settings', {
            _org_id: roles.organization_id,
            _created_by: user.id
          });
        
        if (createError) throw createError;
        
        // Fetch the created settings
        const { data: fetchedSettings } = await supabase
          .from('fa_module_settings')
          .select('*')
          .eq('organization_id', roles.organization_id)
          .single();
        
        return fetchedSettings;
      }
      
      if (error) throw error;
      return settings;
    }
  });
  
  const updateMutation = useMutation({
    mutationFn: async (updates: Partial<FAModuleSettings>) => {
      const { data, error } = await supabase
        .from('fa_module_settings')
        .update(updates)
        .eq('id', settings?.id)
        .select()
        .single();
      
      if (error) throw error;
      return data;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['fa-module-settings'] });
      toast({
        title: 'Settings saved',
        description: 'Finance settings have been updated successfully.'
      });
    },
    onError: (error: Error) => {
      toast({
        title: 'Save failed',
        description: error.message,
        variant: 'destructive'
      });
    }
  });
  
  return {
    settings,
    isLoading,
    error,
    update: updateMutation.mutate,
    isUpdating: updateMutation.isPending
  };
}

4. Navigation Integration

Add to FA Module Registry

File: src/platform/navigation/module-registry.ts
// In FA module config
{
  id: 'fa',
  name: 'Finance',
  icon: DollarSign,
  routes: [
    // ... existing routes
    {
      path: '/fa/settings',
      label: 'Settings',
      icon: Settings,
      permission: 'fa.settings.admin' // V2 permission-based access control
    }
  ]
}

Add Route

File: src/App.tsx
<Route path="/fa/settings" element={<FASettings />} />

5. Usage in Other FA Modules

Example: FA-02 (General Ledger)

// When creating journal entry, use default accounts
const { settings } = useFAModuleSettings();

// Check if posting to prior period is allowed
if (settings?.restrict_prior_period_posting) {
  const daysBack = differenceInDays(new Date(), entryDate);
  if (daysBack > (settings.days_back_posting_allowed || 0)) {
    throw new Error('Cannot post to periods older than configured limit');
  }
}

// Use default cash account if none specified
const cashAccountId = lineItem.account_id || settings?.default_cash_account_id;

Example: FA-04 (Accounts Payable)

const { settings } = useFAModuleSettings();

// Check if bill requires approval
const requiresApproval = settings?.bill_approval_threshold 
  ? bill.amount >= settings.bill_approval_threshold
  : true; // If threshold is null, all bills require approval

// Check if PO is required
if (settings?.require_po_for_bills && !bill.purchase_order_id) {
  throw new Error('Purchase order is required for all bills');
}

Example: FA-08 (Budgeting)

const { settings } = useFAModuleSettings();

// Check budget alert thresholds
const warningThreshold = settings?.budget_warning_threshold || 90;
const criticalThreshold = settings?.budget_critical_threshold || 100;

if (variance >= criticalThreshold) {
  // Send critical alert
  await sendBudgetAlert('critical', ...);
} else if (variance >= warningThreshold) {
  // Send warning alert
  await sendBudgetAlert('warning', ...);
}

// Check if over-budget posting is allowed
if (!settings?.allow_over_budget_posting && wouldExceedBudget) {
  throw new Error('Posting would exceed budget and over-budget posting is disabled');
}

6. Implementation Checklist

  • Create fa_module_settings table with RLS policies
  • Add set_updated_at trigger
  • Create seed_fa_module_settings() function
  • Create hook: useFAModuleSettings.ts
  • Create form: FASettingsForm.tsx with 8 tabs
  • Create page: FASettings.tsx
  • Add route to App.tsx
  • Add navigation item (org_admin only)
  • Document in IMPLEMENTATION_LOG.md
  • Update FA-01, FA-02, FA-04, FA-08 specs to reference module settings

7. Testing Requirements

Unit Tests

  • Settings hook fetches and updates correctly
  • Default values applied when creating new settings
  • Form validation for numeric thresholds

Integration Tests

  • Only org_admin can access settings page
  • Settings auto-created on first access
  • Settings apply correctly across FA modules (GL, AP, AR, Budget)

RLS Tests

  • Users in org can view settings
  • Only org_admin can update settings
  • Users outside org cannot view/modify settings

8. Documentation

Update IMPLEMENTATION_LOG.md:
### FA-10: Module Settings (āœ… Complete)

**Completed:** YYYY-MM-DD

**Database Changes:**
- Created table: `fa_module_settings`
- RLS policies: org view, org_admin manage
- Function: `seed_fa_module_settings()`

**Components & Files:**
- `/src/cores/fa/pages/FASettings.tsx`
- `/src/cores/fa/components/FASettingsForm.tsx`
- `/src/cores/fa/hooks/useFAModuleSettings.ts`

**Testing:**
- RLS tests: āœ… Complete
- Integration tests: āœ… Complete

**Known Limitations:**
- None