Compliance Score Engine

Measure and improve your NDPA 2023 compliance posture with the built-in scoring engine

What the Compliance Score Engine Does

The compliance score engine analyses the state of all active toolkit modules and produces a numeric score from 0–100, a rating, per-module breakdowns, and prioritised recommendations. It gives your team an at-a-glance view of where you stand against the Nigeria Data Protection Act (NDPA) 2023 and highlights the highest-impact gaps to close.

Not a legal audit

The score reflects the completeness of your technical implementation using the toolkit. It does not replace a formal Data Protection Impact Assessment or legal advice. Use it as a development health check, not a compliance certificate.

Using getComplianceScore()

getComplianceScore() is a pure function — pass it a ComplianceInput object describing your current implementation state and it returns a full ComplianceReport synchronously. Import it from the main package:

import { getComplianceScore } from '@tantainnovative/ndpr-toolkit';

const report = getComplianceScore({
  // Consent module
  consent: {
    hasConsentMechanism: true,       // affirmative consent collected before processing
    hasPurposeSpecification: true,   // purpose communicated at point of consent
    hasWithdrawalMechanism: true,    // data subjects can withdraw consent
    hasMinorProtection: false,       // age-verification / parental-consent not yet implemented
    consentRecordsRetained: true,    // consent records persisted
  },

  // Data Subject Rights module
  dsr: {
    hasRequestMechanism: true,       // formal channel exists for DSR submissions
    supportsAccess: true,            // right of access honoured
    supportsRectification: true,     // right to rectification honoured
    supportsErasure: false,          // erasure workflow not yet implemented
    supportsPortability: false,      // portability not yet implemented
    supportsObjection: false,        // right to object not yet implemented
    responseTimelineDays: 14,        // max days to respond (must be ≤ 30)
  },

  // Data Protection Impact Assessment module
  dpia: {
    conductedForHighRisk: true,      // DPIA conducted before high-risk processing
    documentedRisks: true,           // risks to data subjects documented
    mitigationMeasures: false,       // mitigation measures not yet documented
  },

  // Breach notification module
  breach: {
    hasNotificationProcess: true,    // documented breach response process exists
    notifiesWithin72Hours: true,     // NDPC notified within 72 hours of discovery
    hasRiskAssessment: false,        // per-breach risk assessment not yet performed
    hasRecordKeeping: true,          // breach register maintained
  },

  // Privacy policy module
  policy: {
    hasPrivacyPolicy: true,          // privacy policy exists
    isPubliclyAccessible: true,      // policy is reachable on website/app
    lastUpdated: '2024-11-01',       // ISO date — >13 months old counts as a gap
    coversAllSections: true,         // all NDPA-mandated disclosures present
  },

  // Lawful basis module
  lawfulBasis: {
    documentedForAllProcessing: true,             // lawful basis recorded for every activity
    hasLegitimateInterestAssessment: false,        // LIA not yet completed
  },

  // Cross-border transfers module
  crossBorder: {
    hasTransferMechanisms: true,     // SCCs / BCRs / adequacy decisions in place
    adequacyAssessed: true,          // destination-country adequacy reviewed
    ndpcApprovalObtained: false,     // NDPC approval pending
  },

  // Records of Processing Activities module
  ropa: {
    maintained: true,                // ROPA exists and is maintained
    includesAllProcessing: false,    // not all activities captured yet
    lastReviewed: '2024-10-15',      // ISO date — >6 months old counts as a gap
  },
});

console.log(report.score);        // e.g. 68
console.log(report.rating);       // 'needs-work'
console.log(report.recommendations); // array of prioritised Recommendation objects

ComplianceInput Reference

The full ComplianceInput interface — every field and its type:

interface ComplianceInput {
  consent: {
    hasConsentMechanism: boolean;
    hasPurposeSpecification: boolean;
    hasWithdrawalMechanism: boolean;
    hasMinorProtection: boolean;
    consentRecordsRetained: boolean;
  };
  dsr: {
    hasRequestMechanism: boolean;
    supportsAccess: boolean;
    supportsRectification: boolean;
    supportsErasure: boolean;
    supportsPortability: boolean;
    supportsObjection: boolean;
    /** Max response time in days — values > 30 count as a gap */
    responseTimelineDays: number;
  };
  dpia: {
    conductedForHighRisk: boolean;
    documentedRisks: boolean;
    mitigationMeasures: boolean;
  };
  breach: {
    hasNotificationProcess: boolean;
    notifiesWithin72Hours: boolean;
    hasRiskAssessment: boolean;
    hasRecordKeeping: boolean;
  };
  policy: {
    hasPrivacyPolicy: boolean;
    isPubliclyAccessible: boolean;
    /** ISO date string (YYYY-MM-DD) — > 13 months old counts as a gap */
    lastUpdated: string;
    coversAllSections: boolean;
  };
  lawfulBasis: {
    documentedForAllProcessing: boolean;
    hasLegitimateInterestAssessment: boolean;
  };
  crossBorder: {
    hasTransferMechanisms: boolean;
    adequacyAssessed: boolean;
    ndpcApprovalObtained: boolean;
  };
  ropa: {
    maintained: boolean;
    includesAllProcessing: boolean;
    /** ISO date string (YYYY-MM-DD) — > 6 months old counts as a gap */
    lastReviewed: string;
  };
}

Understanding the Report

The returned ComplianceReport object has the following shape:

interface ComplianceReport {
  /** Numeric score 0–100 */
  score: number;

  /** Rating bucket */
  rating: 'excellent' | 'good' | 'needs-work' | 'critical';

  /** Per-module breakdown keyed by module name */
  modules: Record<string, {
    name: string;           // e.g. "consent"
    score: number;          // 0–100 for this module
    maxScore: number;       // always 100
    weightedScore: number;  // weighted contribution to the overall score
    ndpaSections: string[]; // NDPA sections this module maps to
    gaps: string[];         // human-readable list of failing checks
  }>;

  /** Recommendations sorted by priority (critical first) */
  recommendations: Array<{
    module: string;
    key: string;
    label: string;
    priority: 'critical' | 'high' | 'medium' | 'low';
    effort: 'low' | 'medium' | 'high';
    recommendation: string;
    ndpaSection: string;
  }>;

  /** Top-level regulatory references */
  regulatoryReferences: Array<{
    section: string;
    title: string;
    url?: string;
  }>;

  /** ISO timestamp of when the report was generated */
  generatedAt: string;
}

Rating thresholds

ScoreRatingInterpretation
90–100excellentFully implemented — only minor gaps remain
70–89goodMostly compliant — a few medium-priority items outstanding
40–69needs-workPartial implementation — several notable gaps
0–39criticalCritical non-compliance — immediate action required

Module Weights & NDPA Section References

Weights reflect the relative emphasis each topic receives in the NDPA 2023 text and the Nigeria Data Protection Commission (NDPC) enforcement guidance.

ModuleWeightPrimary NDPA Sections
Consent20%Section 25, Section 26
Data Subject Rights (DSR)15%Sections 34–39
Breach Notification15%Section 40
Privacy Policy12%Section 29
DPIA12%Section 28
Lawful Basis10%Section 25(1)
Cross-Border Transfers8%Section 43, Section 44
ROPA8%Section 30

Using the useComplianceScore() Hook

useComplianceScore() is the reactive version of getComplianceScore(). It memoises the report and re-computes it whenever the input changes, making it ideal for a live compliance dashboard. It requires a { input } argument — it does not read state from context automatically.

'use client';

import { useState } from 'react';
import { useComplianceScore } from '@tantainnovative/ndpr-toolkit';
import type { ComplianceInput } from '@tantainnovative/ndpr-toolkit';

const initialInput: ComplianceInput = {
  consent: {
    hasConsentMechanism: true,
    hasPurposeSpecification: true,
    hasWithdrawalMechanism: true,
    hasMinorProtection: false,
    consentRecordsRetained: true,
  },
  dsr: {
    hasRequestMechanism: true,
    supportsAccess: true,
    supportsRectification: true,
    supportsErasure: false,
    supportsPortability: false,
    supportsObjection: false,
    responseTimelineDays: 14,
  },
  dpia: {
    conductedForHighRisk: true,
    documentedRisks: true,
    mitigationMeasures: false,
  },
  breach: {
    hasNotificationProcess: true,
    notifiesWithin72Hours: true,
    hasRiskAssessment: false,
    hasRecordKeeping: true,
  },
  policy: {
    hasPrivacyPolicy: true,
    isPubliclyAccessible: true,
    lastUpdated: '2024-11-01',
    coversAllSections: true,
  },
  lawfulBasis: {
    documentedForAllProcessing: true,
    hasLegitimateInterestAssessment: false,
  },
  crossBorder: {
    hasTransferMechanisms: true,
    adequacyAssessed: true,
    ndpcApprovalObtained: false,
  },
  ropa: {
    maintained: true,
    includesAllProcessing: false,
    lastReviewed: '2024-10-15',
  },
};

export function ComplianceWidget() {
  const [input] = useState<ComplianceInput>(initialInput);

  // Pass { input } — the hook does NOT read from context
  const report = useComplianceScore({ input });
  const { score, rating, recommendations, modules } = report;

  const ratingColor =
    rating === 'excellent' ? 'text-green-600' :
    rating === 'good'      ? 'text-blue-600' :
    rating === 'needs-work'? 'text-yellow-600' :
                             'text-red-600';

  return (
    <div className="p-6 rounded-xl border">
      <div className="flex items-center gap-4 mb-4">
        <span className="text-5xl font-bold">{score}</span>
        <span className="text-2xl font-semibold text-gray-500">/ 100</span>
        <span className={ratingColor}>
          {rating}
        </span>
      </div>

      <h3 className="font-semibold mb-2">Top Recommendations</h3>
      <ul className="space-y-2">
        {recommendations.slice(0, 3).map((rec) => (
          <li key={rec.key} className="text-sm">
            <span className="font-medium">[{rec.priority.toUpperCase()}]</span>{' '}
            {rec.recommendation}{' '}
            <span className="text-gray-500">({rec.ndpaSection})</span>
          </li>
        ))}
      </ul>

      <h3 className="font-semibold mt-4 mb-2">Module Scores</h3>
      <ul className="space-y-1">
        {Object.values(modules).map((mod) => (
          <li key={mod.name} className="text-sm flex justify-between">
            <span className="capitalize">{mod.name}</span>
            <span>{mod.score}/100</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

The NDPRDashboard Component

For teams that want a complete compliance overview without building a custom UI, the toolkit ships a ready-made NDPRDashboard component. It renders a score ring, per-module cards with progress bars, and a prioritised recommendations list — all driven by a ComplianceReport you pass in as a prop.

Props

interface NDPRDashboardProps {
  /** Compliance report produced by getComplianceScore() — required */
  report: ComplianceReport;

  /** Dashboard heading. Defaults to "NDPA Compliance Dashboard" */
  title?: string;

  /** Show/hide the recommendations list. Defaults to true */
  showRecommendations?: boolean;

  /** Maximum number of recommendations to render. Defaults to 5 */
  maxRecommendations?: number;

  /** Per-section class name overrides for custom styling */
  classNames?: NDPRDashboardClassNames;

  /** Strip all default classes so you can style from scratch */
  unstyled?: boolean;
}

Basic usage

Call getComplianceScore() with your ComplianceInput, then pass the resulting report to NDPRDashboard:

'use client';

import { getComplianceScore, NDPRDashboard } from '@tantainnovative/ndpr-toolkit';

const report = getComplianceScore({
  consent: {
    hasConsentMechanism: true,
    hasPurposeSpecification: true,
    hasWithdrawalMechanism: true,
    hasMinorProtection: false,
    consentRecordsRetained: true,
  },
  dsr: {
    hasRequestMechanism: true,
    supportsAccess: true,
    supportsRectification: true,
    supportsErasure: false,
    supportsPortability: false,
    supportsObjection: false,
    responseTimelineDays: 14,
  },
  dpia: {
    conductedForHighRisk: true,
    documentedRisks: true,
    mitigationMeasures: false,
  },
  breach: {
    hasNotificationProcess: true,
    notifiesWithin72Hours: true,
    hasRiskAssessment: false,
    hasRecordKeeping: true,
  },
  policy: {
    hasPrivacyPolicy: true,
    isPubliclyAccessible: true,
    lastUpdated: '2024-11-01',
    coversAllSections: true,
  },
  lawfulBasis: {
    documentedForAllProcessing: true,
    hasLegitimateInterestAssessment: false,
  },
  crossBorder: {
    hasTransferMechanisms: true,
    adequacyAssessed: true,
    ndpcApprovalObtained: false,
  },
  ropa: {
    maintained: true,
    includesAllProcessing: false,
    lastReviewed: '2024-10-15',
  },
});

export default function ComplianceDashboardPage() {
  return (
    <main className="max-w-4xl mx-auto py-12 px-4">
      <NDPRDashboard report={report} />
    </main>
  );
}

Controlling what renders

// Hide recommendations and change the title
<NDPRDashboard
  report={report}
  title="Q2 Compliance Review"
  showRecommendations={false}
/>

// Show at most 3 recommendations
<NDPRDashboard
  report={report}
  maxRecommendations={3}
/>

Custom styling with classNames

Every visual section accepts a class name override via the classNames prop. Pass unstyled to strip all defaults and style from scratch:

<NDPRDashboard
  report={report}
  classNames={{
    root: 'bg-card border border-border rounded-2xl p-8',
    moduleCard: 'bg-muted rounded-lg p-4',
    recommendationItem: 'border border-border rounded-lg p-3',
  }}
/>

Live dashboard with useComplianceScore()

Pair NDPRDashboard with useComplianceScore() for a dashboard that re-renders whenever your input state changes. Remember to pass { input }:

'use client';

import { useState } from 'react';
import { useComplianceScore, NDPRDashboard } from '@tantainnovative/ndpr-toolkit';
import type { ComplianceInput } from '@tantainnovative/ndpr-toolkit';

export function LiveComplianceDashboard({ input }: { input: ComplianceInput }) {
  // Must pass { input } — the hook does not read from context
  const report = useComplianceScore({ input });

  return <NDPRDashboard report={report} />;
}

Preset: NDPRComplianceDashboard

The presets package ships NDPRComplianceDashboard, which combines getComplianceScore() and NDPRDashboard into a single drop-in component — ideal for admin dashboards:

import { NDPRComplianceDashboard } from '@tantainnovative/ndpr-toolkit/presets';

// Pass your ComplianceInput directly — no need to call getComplianceScore() yourself
<NDPRComplianceDashboard input={complianceInput} />

Server-side Compliance Checks

getComplianceScore() has no browser dependencies — you can run it in a Node.js API route to generate reports server-side and persist them to a database or send them in a scheduled email:

// src/app/api/compliance-report/route.ts
import { getComplianceScore } from '@tantainnovative/ndpr-toolkit';
import type { ComplianceInput } from '@tantainnovative/ndpr-toolkit';
import { getCurrentModuleState } from '@/lib/compliance-state';
import { NextResponse } from 'next/server';

export async function GET() {
  // fetch your saved ComplianceInput from DB — must match the ComplianceInput shape
  const input: ComplianceInput = await getCurrentModuleState();
  const report = getComplianceScore(input);

  return NextResponse.json(report);
}