azaharizaman/nexus-account-period-close

Period close validation, closing entry generation, and reopen management

Maintainers

Package info

github.com/azaharizaman/nexus-account-period-close

pkg:composer/azaharizaman/nexus-account-period-close

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0-alpha1 2026-05-05 02:28 UTC

This package is auto-updated.

Last update: 2026-05-05 02:29:56 UTC


README

Framework-Agnostic Period Close Engine

PHP Version License

Overview

Nexus\AccountPeriodClose is a pure PHP package that provides the core engine for managing accounting period close processes. It handles close readiness validation, closing entry generation, retained earnings calculation, and period reopening controls. The package ensures proper month-end, quarter-end, and year-end close procedures.

This package is framework-agnostic and contains no database access, no HTTP controllers, and no framework-specific code. Consuming applications provide period data and execute the actual database operations through injected interfaces.

Installation

composer require azaharizaman/nexus-account-period-close

Package Responsibilities

Responsibility Description
Close Readiness Validation Verify all prerequisites are met before closing
Closing Entry Generation Generate entries to close temporary accounts
Retained Earnings Calculation Calculate and post retained earnings entries
Reopen Validation Control when and how periods can be reopened
Close Sequence Management Ensure periods close in proper order
Year-End Processing Handle annual close with equity rollforward

Key Concepts

Close Types

Type Scope Actions
Soft Close Period-level Lock transactions, allow adjustments
Hard Close Period-level No changes allowed
Year-End Close Annual Close income/expense to retained earnings
Interim Close Sub-period Monthly/quarterly within fiscal year

Close Sequence

Periods must close in chronological order:

  1. Earlier periods must close before later periods
  2. All sub-periods must close before parent period
  3. All subledgers must close before GL close

Architecture

src/
├── Contracts/           # Interfaces defining the public API
├── ValueObjects/        # Immutable close process data structures
├── Enums/               # Close types, statuses, severities
├── Services/            # Core close process logic
├── Rules/               # Close validation rules
└── Exceptions/          # Domain-specific errors

Contracts (Interfaces)

Core Interfaces

CloseReadinessValidatorInterface

Validates that a period is ready to close.

interface CloseReadinessValidatorInterface
{
    /**
     * Validate period close readiness
     *
     * @param string $tenantId
     * @param string $periodId
     * @return CloseReadinessResult
     */
    public function validate(string $tenantId, string $periodId): CloseReadinessResult;
    
    /**
     * Run a specific validation rule
     */
    public function runRule(CloseRuleInterface $rule, string $periodId): CloseCheckResult;
    
    /**
     * Get all registered validation rules
     *
     * @return array<CloseRuleInterface>
     */
    public function getRules(): array;
    
    /**
     * Check if period can be closed (all rules pass)
     */
    public function canClose(string $tenantId, string $periodId): bool;
}

ClosingEntryGeneratorInterface

Generates closing journal entries.

interface ClosingEntryGeneratorInterface
{
    /**
     * Generate closing entries for a period
     *
     * @param string $tenantId
     * @param string $periodId
     * @param CloseType $closeType
     * @return array<ClosingEntrySpec>
     */
    public function generate(
        string $tenantId,
        string $periodId,
        CloseType $closeType = CloseType::HARD
    ): array;
    
    /**
     * Generate year-end closing entries
     * (Close revenue/expense accounts to retained earnings)
     */
    public function generateYearEndEntries(
        string $tenantId,
        string $fiscalYearId
    ): array;
    
    /**
     * Preview closing entries without posting
     */
    public function preview(
        string $tenantId,
        string $periodId
    ): array;
}

ReopenValidatorInterface

Validates and controls period reopening.

interface ReopenValidatorInterface
{
    /**
     * Validate if a period can be reopened
     *
     * @param string $tenantId
     * @param string $periodId
     * @param ReopenRequest $request
     * @return ValidationResult
     */
    public function validate(
        string $tenantId,
        string $periodId,
        ReopenRequest $request
    ): ValidationResult;
    
    /**
     * Check if period is eligible for reopening
     */
    public function canReopen(string $tenantId, string $periodId): bool;
    
    /**
     * Get reopen restrictions for period
     */
    public function getRestrictions(string $periodId): array;
    
    /**
     * Verify reopen authorization
     */
    public function verifyAuthorization(
        string $userId,
        string $periodId
    ): bool;
}

CloseSequenceInterface

Manages the order of period closes.

interface CloseSequenceInterface
{
    /**
     * Get the next period to close
     */
    public function getNextToClose(string $tenantId): ?string;
    
    /**
     * Verify period can close in sequence
     */
    public function verifySequence(
        string $tenantId,
        string $periodId
    ): bool;
    
    /**
     * Get periods that must close first
     *
     * @return array<string>
     */
    public function getPrerequisitePeriods(string $periodId): array;
    
    /**
     * Check if any dependent periods are open
     */
    public function hasDependentOpenPeriods(string $periodId): bool;
}

CloseRuleInterface

Contract for individual close validation rules.

interface CloseRuleInterface
{
    /**
     * Get rule identifier
     */
    public function getId(): string;
    
    /**
     * Get rule name
     */
    public function getName(): string;
    
    /**
     * Get rule description
     */
    public function getDescription(): string;
    
    /**
     * Execute the validation rule
     */
    public function check(string $tenantId, string $periodId): CloseCheckResult;
    
    /**
     * Get severity if rule fails
     */
    public function getSeverity(): ValidationSeverity;
    
    /**
     * Can this rule be bypassed with override?
     */
    public function canBypass(): bool;
}

CloseDataProviderInterface

Contract for consuming applications to provide close data.

interface CloseDataProviderInterface
{
    /**
     * Get period details
     */
    public function getPeriod(string $periodId): PeriodContext;
    
    /**
     * Get unposted entries count
     */
    public function getUnpostedEntriesCount(
        string $tenantId,
        string $periodId
    ): int;
    
    /**
     * Get trial balance status
     */
    public function isTrialBalanceBalanced(
        string $tenantId,
        string $periodId
    ): bool;
    
    /**
     * Get reconciliation status
     */
    public function getReconciliationStatus(
        string $tenantId,
        string $periodId
    ): array;
    
    /**
     * Get subledger close status
     */
    public function getSubledgerCloseStatus(
        string $tenantId,
        string $periodId
    ): array;
}

PeriodContextInterface

Provides period context information.

interface PeriodContextInterface
{
    public function getPeriodId(): string;
    public function getTenantId(): string;
    public function getStartDate(): \DateTimeImmutable;
    public function getEndDate(): \DateTimeImmutable;
    public function getFiscalYearId(): string;
    public function getStatus(): CloseStatus;
    public function isYearEnd(): bool;
    public function getPriorPeriodId(): ?string;
}

Value Objects

CloseReadinessResult

final readonly class CloseReadinessResult
{
    public function __construct(
        public string $periodId,
        public bool $isReady,
        public array $passedChecks,
        public array $failedChecks,
        public array $warnings,
        public \DateTimeImmutable $checkedAt
    ) {}
    
    public function canProceed(): bool
    {
        return $this->isReady || $this->onlyHasWarnings();
    }
    
    public function getBlockingIssues(): array
    {
        return array_filter(
            $this->failedChecks,
            fn($check) => $check->severity === ValidationSeverity::ERROR
        );
    }
}

CloseValidationIssue

final readonly class CloseValidationIssue
{
    public function __construct(
        public string $ruleId,
        public string $ruleName,
        public string $message,
        public ValidationSeverity $severity,
        public bool $canBypass,
        public ?string $resolution = null
    ) {}
}

ClosingEntrySpec

final readonly class ClosingEntrySpec
{
    public function __construct(
        public string $accountId,
        public string $accountCode,
        public string $accountName,
        public Money $debitAmount,
        public Money $creditAmount,
        public string $description,
        public string $closingType,
        public string $retainedEarningsAccountId
    ) {}
}

ReopenRequest

final readonly class ReopenRequest
{
    public function __construct(
        public string $periodId,
        public string $requestedBy,
        public ReopenReason $reason,
        public string $justification,
        public ?\DateTimeImmutable $requestedUntil = null,
        public ?string $approvedBy = null
    ) {}
}

CloseCheckResult

final readonly class CloseCheckResult
{
    public function __construct(
        public string $ruleId,
        public bool $passed,
        public string $message,
        public ValidationSeverity $severity,
        public array $details = []
    ) {}
    
    public static function pass(string $ruleId, string $message = 'Check passed'): self
    {
        return new self($ruleId, true, $message, ValidationSeverity::INFO);
    }
    
    public static function fail(string $ruleId, string $message, ValidationSeverity $severity): self
    {
        return new self($ruleId, false, $message, $severity);
    }
}

Enums

CloseStatus

enum CloseStatus: string
{
    case OPEN = 'open';
    case SOFT_CLOSED = 'soft_closed';
    case HARD_CLOSED = 'hard_closed';
    case REOPENED = 'reopened';
}

CloseType

enum CloseType: string
{
    case SOFT = 'soft';           // Allow adjustments
    case HARD = 'hard';           // No changes allowed
    case YEAR_END = 'year_end';   // Annual close
    case INTERIM = 'interim';     // Sub-period close
}

ValidationSeverity

enum ValidationSeverity: string
{
    case ERROR = 'error';       // Blocks close
    case WARNING = 'warning';   // Proceed with caution
    case INFO = 'info';         // Informational only
}

ReopenReason

enum ReopenReason: string
{
    case CORRECTION = 'correction';           // Fix errors
    case ADJUSTMENT = 'adjustment';           // Late adjustments
    case AUDIT_FINDING = 'audit_finding';     // Auditor requirement
    case REGULATORY = 'regulatory';           // Regulatory requirement
    case SYSTEM_ERROR = 'system_error';       // Technical issue
}

Services

CloseReadinessValidator

Orchestrates readiness validation:

  1. Runs all registered close rules
  2. Aggregates results
  3. Determines overall readiness
  4. Provides resolution guidance

ClosingEntryGenerator

Generates closing journal entries:

  • Close revenue accounts to income summary
  • Close expense accounts to income summary
  • Close income summary to retained earnings
  • Handle dividends accounts

ReopenValidator

Controls period reopening:

  • Verify authorization levels
  • Check dependent period status
  • Validate reason and justification
  • Enforce reopen time limits

RetainedEarningsCalculator

Calculates retained earnings:

  • Beginning retained earnings
  • Add: Net income (loss)
  • Less: Dividends declared
  • Equals: Ending retained earnings

EquityRollForwardGenerator

Generates equity rollforward:

  • Track each equity component
  • Opening → Changes → Closing
  • Comprehensive income items

YearEndCloseHandler

Special year-end processing:

  • Close all temporary accounts
  • Post to retained earnings
  • Create opening balances for new year
  • Lock fiscal year

AdjustingEntryGenerator

Generates period-end adjusting entries:

  • Accruals (revenue/expense)
  • Deferrals (prepaid/unearned)
  • Depreciation
  • Allowances

DeferredRevenueCalculator

Calculates deferred revenue recognition:

  • Revenue recognition schedules
  • Deferred balance calculation
  • Period-end adjustments

Rules

Built-in Close Rules

Rule Severity Description
TrialBalanceMustBalanceRule ERROR Trial balance debits must equal credits
NoUnpostedEntriesRule ERROR All entries must be posted
ReconciliationCompleteRule WARNING Bank reconciliations should be complete
AllSubledgersClosedRule* ERROR AR, AP, Inventory must be closed first
WorkflowApprovalRule* ERROR Required approvals obtained

*Located in orchestrators/AccountingOperations

Exceptions

Exception When Thrown
PeriodCloseException General period close failure
PeriodNotReadyException Validation rules not satisfied
ReopenNotAllowedException Period cannot be reopened
CloseSequenceException Periods closing out of order

Usage Example

use Nexus\AccountPeriodClose\Contracts\CloseReadinessValidatorInterface;
use Nexus\AccountPeriodClose\Contracts\ClosingEntryGeneratorInterface;
use Nexus\AccountPeriodClose\Enums\CloseType;

final readonly class PeriodCloseService
{
    public function __construct(
        private CloseReadinessValidatorInterface $readinessValidator,
        private ClosingEntryGeneratorInterface $entryGenerator,
        private AuditLogManagerInterface $auditLogger
    ) {}
    
    public function closePeriod(string $tenantId, string $periodId): CloseResult
    {
        // Step 1: Validate readiness
        $readiness = $this->readinessValidator->validate($tenantId, $periodId);
        
        if (!$readiness->canProceed()) {
            throw new PeriodNotReadyException(
                $periodId,
                $readiness->getBlockingIssues()
            );
        }
        
        // Step 2: Generate closing entries
        $closingEntries = $this->entryGenerator->generate(
            tenantId: $tenantId,
            periodId: $periodId,
            closeType: CloseType::HARD
        );
        
        // Step 3: Post entries (delegated to consuming application)
        foreach ($closingEntries as $entry) {
            $this->postClosingEntry($entry);
        }
        
        // Step 4: Log the close event
        $this->auditLogger->log(
            entityId: $periodId,
            action: 'period_closed',
            description: "Period {$periodId} closed with " . count($closingEntries) . " entries"
        );
        
        return new CloseResult(
            periodId: $periodId,
            status: CloseStatus::HARD_CLOSED,
            entriesGenerated: count($closingEntries)
        );
    }
}

Period Close Process Flow

┌─────────────────────────────────────────────────────────────┐
│                    PERIOD CLOSE PROCESS                      │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. READINESS VALIDATION                                     │
│     ├── Trial balance balanced?                              │
│     ├── All entries posted?                                  │
│     ├── Reconciliations complete?                            │
│     ├── Subledgers closed?                                   │
│     └── Approvals obtained?                                  │
│                                                              │
│  2. ADJUSTING ENTRIES (if needed)                            │
│     ├── Accruals                                             │
│     ├── Deferrals                                            │
│     ├── Depreciation                                         │
│     └── Allowances                                           │
│                                                              │
│  3. CLOSING ENTRY GENERATION                                 │
│     ├── Close revenue to income summary                      │
│     ├── Close expenses to income summary                     │
│     └── Close income summary to retained earnings            │
│                                                              │
│  4. POST CLOSING ENTRIES                                     │
│     └── Post all generated entries                           │
│                                                              │
│  5. LOCK PERIOD                                              │
│     ├── Set status to HARD_CLOSED                            │
│     └── Prevent further transactions                         │
│                                                              │
│  6. YEAR-END (if applicable)                                 │
│     ├── Calculate retained earnings                          │
│     ├── Create opening balances                              │
│     └── Roll forward equity                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Integration with Other Packages

Package Integration
Nexus\Finance Provides GL data, posts closing entries
Nexus\Period Provides period definitions and status
Nexus\FinancialStatements Statement generation on close
Nexus\AuditLogger Log close events
Nexus\Workflow Approval workflows for close

Related Documentation

License

MIT License - See LICENSE for details.