649 lines
24 KiB
PHP
649 lines
24 KiB
PHP
<?php
|
|
|
|
namespace Modules\MembershipApplication\Services;
|
|
|
|
use App\Models\Document;
|
|
use App\Services\DocumentService;
|
|
use App\Services\LetterService;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Notification;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Modules\Auth\Entities\User;
|
|
use Modules\Auth\Services\EmailVerificationOtpService;
|
|
use Modules\MembershipApplication\Entities\MembershipApplication;
|
|
use Modules\MembershipApplication\Enums\ApplicationStatus;
|
|
use Modules\MembershipApplication\Enums\BoardDecision;
|
|
use Modules\MembershipApplication\Enums\BoardResult;
|
|
use Modules\MembershipApplication\Enums\ManagementDecision;
|
|
use Modules\MembershipApplication\Enums\ReferenceType;
|
|
use Modules\MembershipApplication\Enums\ReviewStage;
|
|
use Modules\MembershipApplication\Emails\MembershipApplicationFailedNotification;
|
|
use Modules\MembershipApplication\Emails\MembershipApplicationPassedNotification;
|
|
use Modules\MembershipApplication\Emails\MembershipApplicationSubmittedNotification;
|
|
use Modules\MembershipApplication\Repositories\Contracts\MembershipApplicationRepositoryInterface;
|
|
use Modules\Role\Entities\Role;
|
|
|
|
class MembershipApplicationService
|
|
{
|
|
public function __construct(
|
|
protected MembershipApplicationRepositoryInterface $repository,
|
|
protected DocumentService $documentService,
|
|
protected LetterService $letterService,
|
|
) {}
|
|
|
|
/**
|
|
* @param array{
|
|
* applicant: array<string, mixed>,
|
|
* heirs: list<array<string, mixed>>,
|
|
* references?: array{proposer_ic_number?: string|null, supporter_ic_number?: string|null}
|
|
* } $data
|
|
* @param array<string, UploadedFile> $documents
|
|
*/
|
|
public function submit(array $data, array $documents): MembershipApplication
|
|
{
|
|
$application = DB::transaction(function () use ($data, $documents) {
|
|
// membership_applications table
|
|
$application = $this->repository->create([
|
|
'application_number' => $this->generateApplicationNumber(),
|
|
'status' => ApplicationStatus::Submitted,
|
|
'board_result' => '',
|
|
'submitted_at' => now()->toDateTimeString(),
|
|
'completed_at' => '',
|
|
]);
|
|
|
|
// membership_application_applicants table
|
|
$application->applicant()->create($data['applicant']);
|
|
|
|
// membership_application_heirs table
|
|
foreach ($data['heirs'] as $heir) {
|
|
$application->heirs()->create($heir);
|
|
}
|
|
|
|
// membership_application_references table
|
|
$this->syncReferences(
|
|
$application,
|
|
$data['references'] ?? [],
|
|
(string) ($data['applicant']['ic_number'] ?? ''),
|
|
);
|
|
|
|
// documents table
|
|
foreach ($documents as $type => $file) {
|
|
$this->documentService->validateFile($file);
|
|
$this->documentService->uploadDocument($application, $file, $type);
|
|
}
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
|
|
$this->sendSubmissionConfirmation($application);
|
|
|
|
return $application;
|
|
}
|
|
|
|
public function getPaginatedList(int $perPage, string $search, ?string $status, string $sortBy, string $sortOrder): LengthAwarePaginator
|
|
{
|
|
return $this->repository->getAllPaginated($perPage, $search, $status, $sortBy, $sortOrder);
|
|
}
|
|
|
|
public function getByIdWithRelations(string $id): ?MembershipApplication
|
|
{
|
|
return $this->repository->findByIdWithRelations($id);
|
|
}
|
|
|
|
public function update(MembershipApplication $application, array $data): MembershipApplication
|
|
{
|
|
if ($application->status === ApplicationStatus::Completed) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($application, $data) {
|
|
$application->loadMissing('applicant');
|
|
|
|
if (isset($data['applicant'])) {
|
|
$application->applicant->update($data['applicant']);
|
|
}
|
|
|
|
if (isset($data['heirs'])) {
|
|
$application->heirs()->delete();
|
|
foreach ($data['heirs'] as $heir) {
|
|
$application->heirs()->create($heir);
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('references', $data)) {
|
|
$this->syncReferences(
|
|
$application,
|
|
$data['references'] ?? [],
|
|
(string) ($data['applicant']['ic_number'] ?? $application->applicant->ic_number),
|
|
);
|
|
}
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
public function uploadDocument(MembershipApplication $application, string $type, UploadedFile $file): MembershipApplication
|
|
{
|
|
if ($application->status === ApplicationStatus::Completed) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
|
]);
|
|
}
|
|
|
|
$this->documentService->validateFile($file);
|
|
|
|
if ($type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
|
|
$application->documentsOfType($type)->get()->each(
|
|
fn ($document) => $this->documentService->deleteDocument($document->id)
|
|
);
|
|
}
|
|
|
|
$this->documentService->uploadDocument($application, $file, $type);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
}
|
|
|
|
public function deleteDocument(MembershipApplication $application, string $documentId): MembershipApplication
|
|
{
|
|
if ($application->status === ApplicationStatus::Completed) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan yang telah selesai tidak boleh dikemaskini.'],
|
|
]);
|
|
}
|
|
|
|
$document = $application->documents()->findOrFail($documentId);
|
|
|
|
if ($document->type !== MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE) {
|
|
throw ValidationException::withMessages([
|
|
'document' => ['Hanya lampiran pentadbir boleh dipadam.'],
|
|
]);
|
|
}
|
|
|
|
$this->documentService->deleteDocument($document->id);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
}
|
|
|
|
public function managementReview(MembershipApplication $application, ManagementDecision $decision, ?string $remarks, User $reviewer): MembershipApplication {
|
|
if ($application->status !== ApplicationStatus::Submitted) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan ini tidak boleh disemak pada peringkat pentadbiran.'],
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
|
|
$application->reviews()->create([
|
|
'stage' => ReviewStage::Management,
|
|
'decision' => $decision->value,
|
|
'remarks' => $remarks,
|
|
'reviewer_id' => $reviewer->id,
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
$application->update([
|
|
'status' => $decision === ManagementDecision::Approved
|
|
? ApplicationStatus::PendingBoard
|
|
: ApplicationStatus::ManagementRejected,
|
|
]);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
public function boardReview(MembershipApplication $application, BoardDecision $decision, ?string $remarks, User $reviewer): MembershipApplication
|
|
{
|
|
if ($application->status !== ApplicationStatus::PendingBoard) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan ini tidak boleh disemak pada peringkat ahli lembaga.'],
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($application, $decision, $remarks, $reviewer) {
|
|
$application->reviews()->create([
|
|
'stage' => ReviewStage::Board,
|
|
'decision' => $decision->value,
|
|
'remarks' => $remarks,
|
|
'reviewer_id' => $reviewer->id,
|
|
'reviewed_at' => now(),
|
|
]);
|
|
|
|
$application->update([
|
|
'board_result' => $decision === BoardDecision::Pass
|
|
? BoardResult::Pass->value
|
|
: BoardResult::Fail->value,
|
|
'status' => ApplicationStatus::PendingNotification,
|
|
]);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array{proposer_id?: string|null, supporter_id?: string|null} $references
|
|
*/
|
|
public function assignReferences(MembershipApplication $application, array $references, User $assignedBy): MembershipApplication
|
|
{
|
|
return DB::transaction(function () use ($application, $references, $assignedBy) {
|
|
$referenceMap = [
|
|
'proposer_id' => ReferenceType::Proposer,
|
|
'supporter_id' => ReferenceType::Supporter,
|
|
];
|
|
|
|
foreach ($referenceMap as $key => $type) {
|
|
if (! array_key_exists($key, $references)) {
|
|
continue;
|
|
}
|
|
|
|
$application->references()->updateOrCreate(
|
|
['reference_type' => $type->value],
|
|
[
|
|
'user_id' => $references[$key],
|
|
'assigned_by' => $references[$key] ? $assignedBy->id : null,
|
|
'assigned_at' => $references[$key] ? now() : null,
|
|
]
|
|
);
|
|
}
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
public function complete(
|
|
MembershipApplication $application,
|
|
User $processedBy,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): MembershipApplication {
|
|
if ($application->status !== ApplicationStatus::PendingNotification) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
|
|
]);
|
|
}
|
|
|
|
if (! in_array($application->board_result, [BoardResult::Pass->value, BoardResult::Fail->value], true)) {
|
|
throw ValidationException::withMessages([
|
|
'board_result' => ['Keputusan lembaga belum ditetapkan.'],
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) {
|
|
$plainPassword = null;
|
|
|
|
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
|
|
$member = $this->createMemberFromApplication($application);
|
|
$application->update(['user_id' => $member['user']->id]);
|
|
$plainPassword = $member['plainPassword'];
|
|
$application->refresh();
|
|
}
|
|
|
|
$resultLetter = $this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate);
|
|
|
|
$this->sendResultNotification($application, $resultLetter, $plainPassword);
|
|
|
|
$application->update([
|
|
'status' => ApplicationStatus::Completed,
|
|
'completed_at' => now()->toDateTimeString(),
|
|
]);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $applicationIds
|
|
* @return array{
|
|
* succeeded: list<MembershipApplication>,
|
|
* failed: list<array{id: string, application_number: string|null, message: string}>
|
|
* }
|
|
*/
|
|
public function completeBatch(
|
|
array $applicationIds,
|
|
User $processedBy,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): array {
|
|
$succeeded = [];
|
|
$failed = [];
|
|
|
|
foreach ($applicationIds as $id) {
|
|
$application = $this->repository->findById($id);
|
|
|
|
if (! $application) {
|
|
$failed[] = [
|
|
'id' => $id,
|
|
'application_number' => null,
|
|
'message' => 'Permohonan tidak dijumpai.',
|
|
];
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$succeeded[] = $this->complete(
|
|
$application,
|
|
$processedBy,
|
|
$boardMeetingReference,
|
|
$boardMeetingDate,
|
|
);
|
|
} catch (ValidationException $e) {
|
|
$failed[] = [
|
|
'id' => $id,
|
|
'application_number' => $application->application_number,
|
|
'message' => collect($e->errors())->flatten()->first() ?? 'Permohonan tidak boleh diselesaikan.',
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'succeeded' => $succeeded,
|
|
'failed' => $failed,
|
|
];
|
|
}
|
|
|
|
public function generateResultLetter(
|
|
MembershipApplication $application,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): MembershipApplication {
|
|
if ($application->status !== ApplicationStatus::Completed) {
|
|
throw ValidationException::withMessages([
|
|
'status' => ['Surat keputusan hanya boleh dijana untuk permohonan yang telah selesai.'],
|
|
]);
|
|
}
|
|
|
|
if (! in_array($application->board_result, [BoardResult::Pass->value, BoardResult::Fail->value], true)) {
|
|
throw ValidationException::withMessages([
|
|
'board_result' => ['Keputusan lembaga belum ditetapkan.'],
|
|
]);
|
|
}
|
|
|
|
return DB::transaction(function () use ($application, $boardMeetingReference, $boardMeetingDate) {
|
|
$this->storeResultLetter($application, $boardMeetingReference, $boardMeetingDate);
|
|
|
|
return $this->repository->findByIdWithRelations($application->id);
|
|
});
|
|
}
|
|
|
|
protected function storeResultLetter(
|
|
MembershipApplication $application,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): Document {
|
|
$lockedApplication = MembershipApplication::query()
|
|
->whereKey($application->id)
|
|
->lockForUpdate()
|
|
->firstOrFail();
|
|
|
|
if ($lockedApplication->hasDocumentsOfType(MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE)) {
|
|
throw ValidationException::withMessages([
|
|
'result_letter' => ['Surat keputusan telah dijana dan tidak boleh dijana semula.'],
|
|
]);
|
|
}
|
|
|
|
$application = $this->repository->findByIdWithRelations($lockedApplication->id);
|
|
|
|
if (! $application || ! $application->applicant) {
|
|
throw ValidationException::withMessages([
|
|
'application' => ['Maklumat pemohon tidak lengkap.'],
|
|
]);
|
|
}
|
|
|
|
$viewData = $this->buildResultLetterViewData($application, $boardMeetingReference, $boardMeetingDate);
|
|
$pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
|
|
$fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
|
|
|
|
return $this->documentService->storeGeneratedDocument(
|
|
$application,
|
|
$pdf,
|
|
$fileName,
|
|
MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getResultLetterViewData(
|
|
MembershipApplication $application,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): array {
|
|
$application = $this->repository->findByIdWithRelations($application->id);
|
|
|
|
if (! $application || ! $application->applicant) {
|
|
throw ValidationException::withMessages([
|
|
'application' => ['Maklumat pemohon tidak lengkap.'],
|
|
]);
|
|
}
|
|
|
|
return $this->buildResultLetterViewData($application, $boardMeetingReference, $boardMeetingDate);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function buildResultLetterViewData(
|
|
MembershipApplication $application,
|
|
string $boardMeetingReference,
|
|
string $boardMeetingDate,
|
|
): array {
|
|
$applicant = $application->applicant;
|
|
$isPassed = $application->board_result === BoardResult::Pass->value;
|
|
|
|
return [
|
|
'application' => $application,
|
|
'applicant' => $applicant,
|
|
'boardMeetingReference' => $boardMeetingReference,
|
|
'boardMeetingDate' => Carbon::parse($boardMeetingDate)->translatedFormat('d F Y'),
|
|
'isPassed' => $isPassed,
|
|
'letterSubject' => $isPassed
|
|
? 'KELULUSAN PERMOHONAN MENJADI ANGGOTA KOPERASI PERMODALAN KELANTAN BERHAD (KoPKB)'
|
|
: 'KEPUTUSAN PERMOHONAN MENJADI ANGGOTA KOPERASI PERMODALAN KELANTAN BERHAD (KoPKB)',
|
|
'memberNumber' => $isPassed ? $application->user?->member_number : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{user: User, plainPassword: string}
|
|
*/
|
|
protected function createMemberFromApplication(MembershipApplication $application): array
|
|
{
|
|
$application->loadMissing(['applicant', 'heirs']);
|
|
$applicant = $application->applicant;
|
|
|
|
if (User::where('email', $applicant->email)->exists()) {
|
|
throw ValidationException::withMessages([
|
|
'email' => ['E-mel pemohon sudah didaftarkan dalam sistem.'],
|
|
]);
|
|
}
|
|
|
|
$plainPassword = Str::password(16);
|
|
|
|
$user = User::create([
|
|
'name' => $applicant->name,
|
|
'email' => $applicant->email,
|
|
'password' => Hash::make($plainPassword),
|
|
'ic_number' => $applicant->ic_number,
|
|
'phone_number' => $applicant->phone_number,
|
|
'position' => $applicant->current_position,
|
|
'status' => 'active',
|
|
'gender' => $applicant->gender,
|
|
'marriage_status' => $applicant->marriage_status,
|
|
'member_number' => $this->generateMemberNumber(),
|
|
'member_type' => 'Anggota',
|
|
'join_date' => now()->toDateString(),
|
|
'birth_date' => $applicant->birth_date,
|
|
'birth_place' => $applicant->birth_place,
|
|
]);
|
|
|
|
$role = Role::where('name', 'Anggota')->first();
|
|
if ($role) {
|
|
$user->assignRole($role);
|
|
}
|
|
|
|
$user->addresses()->create([
|
|
'address_type' => 'home',
|
|
'address_line_1' => $applicant->address,
|
|
'postcode' => $applicant->postcode,
|
|
]);
|
|
|
|
$user->employments()->create([
|
|
'company_name' => $applicant->employer_name,
|
|
'job_title' => $applicant->current_position,
|
|
'employment_type' => 'Permanent',
|
|
'salary' => 0,
|
|
'start_date' => $applicant->start_work_date,
|
|
'is_current' => true,
|
|
]);
|
|
|
|
foreach ($application->heirs as $index => $heir) {
|
|
$user->heirs()->create([
|
|
'name' => $heir->name,
|
|
'ic_number' => $heir->ic_number,
|
|
'relationship' => $heir->relationship,
|
|
'phone_number' => $heir->phone_number,
|
|
'is_primary' => $index === 0,
|
|
]);
|
|
}
|
|
|
|
app(EmailVerificationOtpService::class)->send($user);
|
|
|
|
return [
|
|
'user' => $user,
|
|
'plainPassword' => $plainPassword,
|
|
];
|
|
}
|
|
|
|
public function lookupMemberByIcNumber(string $icNumber): ?User
|
|
{
|
|
$icNumber = trim($icNumber);
|
|
|
|
if ($icNumber === '') {
|
|
return null;
|
|
}
|
|
|
|
return User::query()
|
|
->where('ic_number', $icNumber)
|
|
->where('status', 'active')
|
|
->first();
|
|
}
|
|
|
|
protected function sendSubmissionConfirmation(MembershipApplication $application): void
|
|
{
|
|
$application->loadMissing(['applicant', 'heirs', 'documents', 'references.member']);
|
|
|
|
Notification::route('mail', $application->applicant->email)
|
|
->notify(new MembershipApplicationSubmittedNotification($application));
|
|
}
|
|
|
|
protected function sendResultNotification(
|
|
MembershipApplication $application,
|
|
Document $resultLetter,
|
|
?string $plainPassword = null,
|
|
): void {
|
|
$application->loadMissing('applicant');
|
|
|
|
$notification = $application->board_result === BoardResult::Pass->value
|
|
? new MembershipApplicationPassedNotification($application, $resultLetter, $plainPassword)
|
|
: new MembershipApplicationFailedNotification($application, $resultLetter);
|
|
|
|
Notification::route('mail', $application->applicant->email)->notify($notification);
|
|
}
|
|
|
|
protected function generateMemberNumber(): int
|
|
{
|
|
$latestUser = User::query()
|
|
->whereNotNull('member_number')
|
|
->orderByDesc('member_number')
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($latestUser === null && DB::getDriverName() === 'pgsql') {
|
|
DB::selectOne('SELECT pg_advisory_xact_lock(?) AS locked', [742891035]);
|
|
}
|
|
|
|
return ((int) ($latestUser?->member_number ?? 0)) + 1;
|
|
}
|
|
|
|
/**
|
|
* @param array{proposer_ic_number?: string|null, supporter_ic_number?: string|null} $references
|
|
*/
|
|
protected function syncReferences(MembershipApplication $application, array $references, string $applicantIcNumber): void
|
|
{
|
|
$referenceMap = [
|
|
'references.proposer_ic_number' => ['field' => 'proposer_ic_number', 'type' => ReferenceType::Proposer],
|
|
'references.supporter_ic_number' => ['field' => 'supporter_ic_number', 'type' => ReferenceType::Supporter],
|
|
];
|
|
|
|
$resolvedUserIds = [];
|
|
$applicantIcNumber = trim($applicantIcNumber);
|
|
|
|
foreach ($referenceMap as $errorKey => $config) {
|
|
if (! array_key_exists($config['field'], $references)) {
|
|
continue;
|
|
}
|
|
|
|
$icNumber = trim((string) ($references[$config['field']] ?? ''));
|
|
|
|
if ($icNumber === '') {
|
|
$application->references()->where('reference_type', $config['type']->value)->delete();
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($applicantIcNumber !== '' && strcasecmp($icNumber, $applicantIcNumber) === 0) {
|
|
throw ValidationException::withMessages([
|
|
$errorKey => ['Pencadang/penyokong tidak boleh sama dengan pemohon.'],
|
|
]);
|
|
}
|
|
|
|
$member = $this->lookupMemberByIcNumber($icNumber);
|
|
|
|
if (! $member) {
|
|
throw ValidationException::withMessages([
|
|
$errorKey => ['Ahli dengan nombor kad pengenalan ini tidak dijumpai.'],
|
|
]);
|
|
}
|
|
|
|
if (in_array($member->id, $resolvedUserIds, true)) {
|
|
throw ValidationException::withMessages([
|
|
$errorKey => ['Pencadang dan penyokong mestilah ahli yang berbeza.'],
|
|
]);
|
|
}
|
|
|
|
$resolvedUserIds[] = $member->id;
|
|
|
|
$application->references()->updateOrCreate(
|
|
['reference_type' => $config['type']->value],
|
|
['user_id' => $member->id]
|
|
);
|
|
}
|
|
}
|
|
|
|
protected function generateApplicationNumber(): string
|
|
{
|
|
$year = now()->year;
|
|
$prefix = "APP-{$year}-";
|
|
|
|
$latestNumber = MembershipApplication::query()
|
|
->where('application_number', 'like', "{$prefix}%")
|
|
->orderByDesc('application_number')
|
|
->value('application_number');
|
|
|
|
$sequence = 1;
|
|
|
|
if ($latestNumber && preg_match('/-(\d+)$/', $latestNumber, $matches)) {
|
|
$sequence = ((int) $matches[1]) + 1;
|
|
}
|
|
|
|
return sprintf('%s%05d', $prefix, $sequence);
|
|
}
|
|
}
|