This system provides automatic generation of unique document numbers for all document types in the application. The format follows the pattern:
AU/CDC/{DIVISION_SHORT_NAME}/IM/{DOCUMENT_TYPE}/{COUNTER}
Important: The {DIVISION_SHORT_NAME} comes from the division_short_name field in the divisions table. Ensure all divisions have this field populated for proper document numbering.
| Type | Code | Description |
|------|------|-------------|
| Quarterly Matrix | QM | Matrix activities |
| Non Travel Memo | NT | Non-travel memos |
| Special Memo | SPM | Special memos |
| Single Memo | SM | Single memos (activities) |
| Change Request | CR | Change requests |
| Service Request | SR | Service requests |
| ARF | ARF | Advance Request Forms |
Models with the HasDocumentNumber trait automatically get document numbers:
// Create a new matrix - document number assigned automatically
$matrix = Matrix::create([
'division_id' => 1,
'year' => 2024,
'quarter' => 'Q1',
// ... other fields
]);
// Document number will be: AU/CDC/CR/IM/QM/001
echo $matrix->document_number;
use App\Services\DocumentNumberService;
// Generate for specific document type
$number = DocumentNumberService::generateDocumentNumber('QM', 'CR', 1);
// Generate for any model
$number = DocumentNumberService::generateForAnyModel($matrix);
// Get preview without incrementing counter
$preview = DocumentNumberService::getNextNumberPreview('QM', $division);
// Generate document number
$number = generateDocumentNumber($model);
// Assign document number via job
assignDocumentNumber($model);
// Get next number preview
$preview = getNextDocumentNumberPreview('QM', $division);
use App\Traits\HasDocumentNumber;
class YourModel extends Model
{
use HasDocumentNumber;
}
protected $fillable = [
// ... other fields
'document_number',
];
$table->string('document_number', 50)->nullable()->unique();
The system automatically detects document types based on model class:
// In DocumentNumberService
return match ($className) {
'Matrix' => null, // No document number - just a container
'NonTravelMemo' => DocumentCounter::TYPE_NON_TRAVEL_MEMO,
'SpecialMemo' => DocumentCounter::TYPE_SPECIAL_MEMO,
'Activity' => self::getActivityDocumentType($model), // QM or SM based on Matrix status
'ServiceRequest' => DocumentCounter::TYPE_SERVICE_REQUEST,
'RequestARF' => DocumentCounter::TYPE_ARF,
default => 'UNKNOWN'
};
Activities get different document types based on their is_single_memo field:
private static function getActivityDocumentType(Model $activity): string
{
// Check if activity is marked as single memo
if (isset($activity->is_single_memo) && $activity->is_single_memo == 1) {
return DocumentCounter::TYPE_SINGLE_MEMO; // SM
}
// Activities not marked as single memo are part of quarterly matrix
return DocumentCounter::TYPE_QUARTERLY_MATRIX; // QM
}
Business Logic:
is_single_memo = 1 → Single Memo (SM)is_single_memo = 0 → Quarterly Matrix (QM)This approach is more reliable as it directly uses the database field that indicates whether an activity should be treated as a single memo or part of a quarterly matrix.
CREATE TABLE document_counters (
id BIGINT PRIMARY KEY,
division_short_name VARCHAR(10),
year INT,
document_type VARCHAR(10),
counter INT DEFAULT 0,
created_at TIMESTAMP,
updated_at TIMESTAMP,
UNIQUE KEY unique_division_year_type (division_short_name, year, document_type),
INDEX idx_division_year (division_short_name, year),
INDEX idx_document_type (document_type)
);
All relevant tables now have a document_number column:
ALTER TABLE matrices ADD COLUMN document_number VARCHAR(50) UNIQUE;
ALTER TABLE activities ADD COLUMN document_number VARCHAR(50) UNIQUE;
ALTER TABLE non_travel_memos ADD COLUMN document_number VARCHAR(50) UNIQUE;
ALTER TABLE special_memos ADD COLUMN document_number VARCHAR(50) UNIQUE;
ALTER TABLE service_requests ADD COLUMN document_number VARCHAR(50) UNIQUE;
ALTER TABLE request_arfs ADD COLUMN document_number VARCHAR(50) UNIQUE;
# Test with default division
php artisan test:document-numbers
# Test with specific division
php artisan test:document-numbers --division=1
# Check which divisions have short names
php artisan check:division-short-names
# Preview what would be assigned
php artisan assign:document-numbers --dry-run
# Assign numbers to existing records
php artisan assign:document-numbers
# Reset all counters for new year (run on January 1st)
php artisan tinker
>>> App\Models\DocumentCounter::resetCountersForNewYear(2025);
# Check for conflicts without making changes (dry run)
php artisan fix:document-conflicts --dry-run
# Fix all document number conflicts
php artisan fix:document-conflicts
# Fix conflicts for specific division only
php artisan fix:document-conflicts --division=1
# Reset counters to next available numbers after deletions
php artisan fix:document-conflicts --reset-counters
# Fix conflicts and reset counters in one command
php artisan fix:document-conflicts --reset-counters
Document number conflicts can happen when:
AU/CDC/CR/IM/SM/001 is deleted, but the counter remains at 001AU/CDC/CR/IM/SM/001The system now includes robust conflict resolution:
Use the fix:document-conflicts command to resolve existing conflicts:
# Dry run to see what would be fixed
php artisan fix:document-conflicts --dry-run
# Fix all conflicts
php artisan fix:document-conflicts
# Fix for specific division
php artisan fix:document-conflicts --division=1
# Reset counters after deletions
php artisan fix:document-conflicts --reset-counters
// Get next counter (atomic operation)
$counter = DocumentCounter::getNextCounter('CR', 'QM', 2024);
// Get division statistics
$stats = DocumentCounter::getDivisionStats('CR', 2024);
// Reset counters for new year
DocumentCounter::resetCountersForNewYear(2025);
// Get all document types
$types = DocumentCounter::getDocumentTypes();
// Generate document number
$number = DocumentNumberService::generateDocumentNumber('QM', 'CR', 1, 2024);
// Generate for model
$number = DocumentNumberService::generateForModel($matrix, 'QM');
// Generate for any model
$number = DocumentNumberService::generateForAnyModel($matrix);
// Get document type from model
$type = DocumentNumberService::getDocumentTypeFromModel($matrix);
// Validate document number
$isValid = DocumentNumberService::validateDocumentNumber($number);
// Parse document number
$components = DocumentNumberService::parseDocumentNumber($number);
// Get preview
$preview = DocumentNumberService::getNextNumberPreview('QM', $division);
// Find next available number (conflict resolution)
$nextNumber = DocumentNumberService::findNextAvailableNumber('SM', 'CR');
// Reset counter after deletions
DocumentNumberService::resetCounterAfterDeletion('SM', 'CR', 2024);
AU/CDC/CR/IM/QM/001 # Central RCC, Quarterly Matrix, #1
AU/CDC/CR/IM/NT/001 # Central RCC, Non Travel Memo, #1
AU/CDC/DHI/IM/SR/001 # Data Hub, Service Request, #1
AU/CDC/EPI/IM/ARF/001 # Epidemiology, ARF, #1
public function store(Request $request)
{
$matrix = Matrix::create($request->validated());
// Document number assigned automatically
return response()->json([
'matrix' => $matrix,
'document_number' => $matrix->document_number
]);
}
@if($matrix->document_number)
<div class="document-number">
<strong>Document Number:</strong> {{ $matrix->document_number }}
</div>
@endif
- System falls back to 'UNKNOWN'
- Ensure divisions have division_short_name set
- Database locking prevents this
- If it occurs, check for race conditions
- System returns 'UNKNOWN'
- Add mapping in DocumentNumberService
All document number operations are logged:
// Check logs
tail -f storage/logs/laravel.log | grep "Document number"
The system includes optimized indexes for:
Consider caching division short names for better performance:
// Cache division short names
$divisionShortName = Cache::remember("division_short_name_{$divisionId}", 3600, function() use ($divisionId) {
return Division::find($divisionId)->division_short_name;
});
Document number assignment uses Laravel queues:
# Process jobs
php artisan queue:work
# Monitor queue
php artisan queue:monitor
php artisan migrate
php artisan assign:document-numbers
{{ $model->document_number ?? 'Pending...' }}
# Test the system
php artisan test:document-numbers
# Test with specific division
php artisan test:document-numbers --division=1
Before using the document numbering system, ensure all divisions have division_short_name populated:
# Check division short names
php artisan check:division-short-names
# Generate missing short names (if using CodeIgniter system)
php artisan settings:force_generate_short_names
CR, DHI, ODDG, P&GM-DThe system reads from the divisions table:
SELECT id, division_name, division_short_name
FROM divisions
WHERE division_short_name IS NOT NULL;
HasDocumentNumber traitdocument_number is in fillable arraydivision_short_name setdivision_short_name (most common issue)DocumentNumberService::validateDocumentNumber()# Check which divisions need short names
php artisan check:division-short-names
# If divisions are missing short names, generate them
php artisan settings:force_generate_short_names
For issues or questions: