diff --git a/TODO.md b/TODO.md
index fff4912..f934127 100644
--- a/TODO.md
+++ b/TODO.md
@@ -16,5 +16,16 @@
[ ] jana surat lepas lulus anggota
## Present to Boss (2/7/2026)
-[ ] discuss logo baru MyKOPKB
-[ ] layout footer untuk surat rasmi
\ No newline at end of file
+[x] discuss logo baru MyKOPKB
+[x] layout footer untuk surat rasmi
+[ ] maklumat pekerjaan kena buat dropdown untuk nama majikan
+[ ] tambah minima untuk borang permohonan (tukar jadi caruman saham potongan bulanan)
+[x] tukar waris ke penama
+[x] ic dan surat pengesahan majikkan perlu mandatori
+[ ] semakan lembaga tukar jadi maklumat keputusan
+[x] boleh attach document untuk peringkat pentadbir
+[x] surat attach dekat email selepas bos
+[ ] pembayaran pertama tukar jadi one-off
+[ ] syer maksima silap tukar jadi rm50.00
+[ ] letak sign digital
+[x] running no anggota dalam surat dan masa.
\ No newline at end of file
diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
index ba3f09a..36018fc 100644
--- a/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
+++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationFailedNotification.php
@@ -2,19 +2,28 @@
namespace Modules\MembershipApplication\Emails;
+use App\Models\Document;
+use App\Notifications\Concerns\BuildsMailMessage;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Support\Facades\Storage;
use Modules\MembershipApplication\Entities\MembershipApplication;
-class MembershipApplicationFailedNotification extends Notification
+class MembershipApplicationFailedNotification extends Notification implements ShouldQueue
{
+ use BuildsMailMessage;
use Queueable;
public function __construct(
- protected MembershipApplication $application
+ public MembershipApplication $application,
+ public Document $resultLetter,
) {}
+ /**
+ * @return array
+ */
public function via(object $notifiable): array
{
return ['mail'];
@@ -23,13 +32,21 @@ class MembershipApplicationFailedNotification extends Notification
public function toMail(object $notifiable): MailMessage
{
$this->application->loadMissing('applicant');
+ $applicant = $this->application->applicant;
- return (new MailMessage)
- ->subject('Keputusan Permohonan Keahlian')
- ->markdown('membershipapplication::emails.failed', [
- 'name' => $this->application->applicant->name,
- 'applicationNumber' => $this->application->application_number,
- 'logoPath' => public_path('images/logo-kopkb.svg'),
- ]);
+ return $this->mailMessage(
+ subject: 'Keputusan Permohonan Keahlian KOPKB - TIDAK LULUS',
+ view: 'membershipapplication::emails.failed',
+ data: [
+ 'application' => $this->application,
+ 'applicant' => $applicant,
+ ],
+ )->attach(
+ Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
+ [
+ 'as' => $this->resultLetter->name,
+ 'mime' => $this->resultLetter->mime_type,
+ ]
+ );
}
}
diff --git a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
index e9ccbd8..c07ec92 100644
--- a/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
+++ b/be/Modules/MembershipApplication/Emails/MembershipApplicationPassedNotification.php
@@ -2,19 +2,28 @@
namespace Modules\MembershipApplication\Emails;
+use App\Models\Document;
+use App\Notifications\Concerns\BuildsMailMessage;
use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
+use Illuminate\Support\Facades\Storage;
use Modules\MembershipApplication\Entities\MembershipApplication;
-class MembershipApplicationPassedNotification extends Notification
+class MembershipApplicationPassedNotification extends Notification implements ShouldQueue
{
+ use BuildsMailMessage;
use Queueable;
public function __construct(
- protected MembershipApplication $application
+ public MembershipApplication $application,
+ public Document $resultLetter,
) {}
+ /**
+ * @return array
+ */
public function via(object $notifiable): array
{
return ['mail'];
@@ -23,13 +32,21 @@ class MembershipApplicationPassedNotification extends Notification
public function toMail(object $notifiable): MailMessage
{
$this->application->loadMissing('applicant');
+ $applicant = $this->application->applicant;
- return (new MailMessage)
- ->subject('Permohonan Keahlian Diluluskan')
- ->markdown('membershipapplication::emails.passed', [
- 'name' => $this->application->applicant->name,
- 'applicationNumber' => $this->application->application_number,
- 'logoPath' => public_path('images/logo-kopkb.svg'),
- ]);
+ return $this->mailMessage(
+ subject: 'Keputusan Permohonan Keahlian KOPKB - LULUS',
+ view: 'membershipapplication::emails.passed',
+ data: [
+ 'application' => $this->application,
+ 'applicant' => $applicant,
+ ],
+ )->attach(
+ Storage::disk(Document::STORAGE_DISK)->path($this->resultLetter->path),
+ [
+ 'as' => $this->resultLetter->name,
+ 'mime' => $this->resultLetter->mime_type,
+ ]
+ );
}
}
diff --git a/be/Modules/MembershipApplication/Entities/MembershipApplication.php b/be/Modules/MembershipApplication/Entities/MembershipApplication.php
index 9464541..15a7ff0 100644
--- a/be/Modules/MembershipApplication/Entities/MembershipApplication.php
+++ b/be/Modules/MembershipApplication/Entities/MembershipApplication.php
@@ -18,6 +18,8 @@ class MembershipApplication extends Model
public const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter';
+ public const ADMIN_ATTACHMENT_DOCUMENT_TYPE = 'admin_attachment';
+
public const MINIMUM_SHARE_CAPITAL = 500;
public const SHARE_INSTALLMENT_MONTHS = 6;
diff --git a/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php b/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
index 006de7b..7da1e09 100644
--- a/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
+++ b/be/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
@@ -17,6 +17,7 @@ use Modules\MembershipApplication\Enums\ManagementDecision;
use Modules\MembershipApplication\Http\Requests\AssignReferencesRequest;
use Modules\MembershipApplication\Http\Requests\BatchCompleteRequest;
use Modules\MembershipApplication\Http\Requests\BoardReviewRequest;
+use Modules\MembershipApplication\Http\Requests\CompleteMembershipApplicationRequest;
use Modules\MembershipApplication\Http\Requests\GenerateResultLetterRequest;
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
@@ -180,18 +181,19 @@ class MembershipApplicationController extends Controller
]);
}
- public function complete(Request $request, MembershipApplication $membershipApplication): JsonResponse
+ public function complete(CompleteMembershipApplicationRequest $request, MembershipApplication $membershipApplication): JsonResponse
{
$this->authorize('complete', $membershipApplication);
$application = $this->membershipApplicationService->complete(
$membershipApplication,
- $request->user()
+ $request->user(),
+ $request->validated('board_meeting_reference'),
);
$message = $application->board_result === BoardResult::Pass->value
- ? 'Permohonan selesai. Akaun ahli telah dicipta dan e-mel dihantar.'
- : 'Permohonan selesai. E-mel keputusan dihantar.';
+ ? 'Permohonan selesai. Surat keputusan dijana, akaun ahli dicipta dan e-mel dihantar.'
+ : 'Permohonan selesai. Surat keputusan dijana dan e-mel dihantar.';
return response()->json([
'success' => true,
@@ -206,7 +208,8 @@ class MembershipApplicationController extends Controller
$result = $this->membershipApplicationService->completeBatch(
$request->validated('application_ids'),
- $request->user()
+ $request->user(),
+ $request->validated('board_meeting_reference'),
);
$succeededCount = count($result['succeeded']);
@@ -245,6 +248,22 @@ class MembershipApplicationController extends Controller
return $this->documentService->downloadDocument($document->id);
}
+ public function deleteDocument(MembershipApplication $membershipApplication, string $documentId): JsonResponse
+ {
+ $this->authorize('update', $membershipApplication);
+
+ $application = $this->membershipApplicationService->deleteDocument(
+ $membershipApplication,
+ $documentId,
+ );
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Lampiran pentadbir berjaya dipadam.',
+ 'data' => new MembershipApplicationResource($application),
+ ]);
+ }
+
public function generateResultLetter(GenerateResultLetterRequest $request, MembershipApplication $membershipApplication,): JsonResponse {
$this->authorize('generateResultLetter', $membershipApplication);
diff --git a/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
index deb9c19..0a4c3ae 100644
--- a/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/BatchCompleteRequest.php
@@ -19,6 +19,7 @@ class BatchCompleteRequest extends FormRequest
return [
'application_ids' => 'required|array|min:1|max:100',
'application_ids.*' => 'required|uuid|distinct|exists:membership_applications,id',
+ 'board_meeting_reference' => 'required|string|max:255',
];
}
@@ -33,6 +34,8 @@ class BatchCompleteRequest extends FormRequest
'application_ids.max' => 'Maksimum 100 permohonan setiap batch.',
'application_ids.*.exists' => 'Salah satu permohonan tidak dijumpai.',
'application_ids.*.distinct' => 'Permohonan duplikat tidak dibenarkan.',
+ 'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
+ 'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
];
}
}
diff --git a/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php
new file mode 100644
index 0000000..971bfde
--- /dev/null
+++ b/be/Modules/MembershipApplication/Http/Requests/CompleteMembershipApplicationRequest.php
@@ -0,0 +1,34 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'board_meeting_reference' => 'required|string|max:255',
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function messages(): array
+ {
+ return [
+ 'board_meeting_reference.required' => 'Rujukan mesyuarat lembaga diperlukan.',
+ 'board_meeting_reference.max' => 'Rujukan mesyuarat lembaga tidak boleh melebihi 255 aksara.',
+ ];
+ }
+}
diff --git a/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
index 8027dc1..f15abed 100644
--- a/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/StoreMembershipApplicationRequest.php
@@ -48,7 +48,7 @@ class StoreMembershipApplicationRequest extends FormRequest
'documents.ic_copy' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
'documents.photo' => 'nullable|file|mimes:jpg,jpeg,png|max:10240',
'documents.salary_slip' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
- 'documents.employer_letter' => 'nullable|file|mimes:pdf,jpg,jpeg,png|max:10240',
+ 'documents.employer_letter' => 'required|file|mimes:pdf,jpg,jpeg,png|max:10240',
];
}
@@ -89,6 +89,8 @@ class StoreMembershipApplicationRequest extends FormRequest
'documents.ic_copy.required' => 'Salinan kad pengenalan diperlukan.',
'documents.ic_copy.mimes' => 'Salinan kad pengenalan mestilah PDF, JPG, JPEG atau PNG.',
+ 'documents.employer_letter.required' => 'Surat pengesahan majikan diperlukan.',
+ 'documents.employer_letter.mimes' => 'Surat pengesahan majikan mestilah PDF, JPG, JPEG atau PNG.',
];
}
}
diff --git a/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
index 542495f..47198a4 100644
--- a/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
+++ b/be/Modules/MembershipApplication/Http/Requests/UploadMembershipApplicationDocumentRequest.php
@@ -4,6 +4,7 @@ namespace Modules\MembershipApplication\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
+use Modules\MembershipApplication\Entities\MembershipApplication;
class UploadMembershipApplicationDocumentRequest extends FormRequest
{
@@ -18,7 +19,13 @@ class UploadMembershipApplicationDocumentRequest extends FormRequest
public function rules(): array
{
return [
- 'type' => ['required', Rule::in(['ic_copy', 'photo', 'salary_slip', 'employer_letter'])],
+ 'type' => ['required', Rule::in([
+ 'ic_copy',
+ 'photo',
+ 'salary_slip',
+ 'employer_letter',
+ MembershipApplication::ADMIN_ATTACHMENT_DOCUMENT_TYPE,
+ ])],
'file' => [
'required',
'file',
diff --git a/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php b/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
index 679ef22..06fccd4 100644
--- a/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
+++ b/be/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
@@ -23,6 +23,7 @@ class MembershipApplicationRepository implements MembershipApplicationRepository
return MembershipApplication::with([
'applicant',
'heirs',
+ 'user:id,member_number',
'references.member:id,name,ic_number',
'documents',
'reviews.reviewer:id,name',
diff --git a/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
index 62b70a8..2982892 100644
--- a/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/emails/failed.blade.php
@@ -1,18 +1,15 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@endif
+@include('emails.partials.header')
# Keputusan Permohonan Keahlian
-Assalamualaikum **{{ $name }}**,
+Assalamualaikum **{{ $applicant->name }}**,
-Permohonan keahlian anda (**{{ $applicationNumber }}**) **tidak diluluskan** pada peringkat lembaga.
+Permohonan keahlian anda (**{{ $application->application_number }}**) **tidak diluluskan** pada peringkat lembaga.
+
+Surat keputusan rasmi dilampirkan dalam e-mel ini.
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
-Terima kasih,
-{{ config('app.name') }}
+@include('emails.partials.footer')
@endcomponent
diff --git a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
index 3c962c2..b9752cd 100644
--- a/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/emails/passed.blade.php
@@ -1,18 +1,15 @@
@component('mail::message')
-@if (! empty($logoPath) && file_exists($logoPath))
-
-
-
-@endif
+@include('emails.partials.header')
# Keputusan Permohonan Keahlian
-Assalamualaikum **{{ $name }}**,
+Assalamualaikum **{{ $applicant->name }}**,
-Permohonan keahlian anda (**{{ $applicationNumber }}**) **diluluskan** pada peringkat lembaga.
+Permohonan keahlian anda (**{{ $application->application_number }}**) **diluluskan** pada peringkat lembaga.
+
+Surat keputusan rasmi dilampirkan dalam e-mel ini.
Terima kasih atas minat anda. Untuk sebarang pertanyaan, sila hubungi pejabat koperasi.
-Terima kasih,
-{{ config('app.name') }}
+@include('emails.partials.footer')
@endcomponent
diff --git a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
index cdfc055..8646b8d 100644
--- a/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
+++ b/be/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
@@ -22,7 +22,7 @@
2. {{ $openingTone }} dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
- Permodalan Kelantan Berhad (KoPKB) telah {{ $boardResultLabel }} dalam {{ $boardMeetingReference }}.
+ Permodalan Kelantan Berhad (KoPKB) telah {{ $boardResultLabel }} dalam {{ $boardMeetingReference }}@if ($isPassed && $memberNumber). Nombor keanggotaan tuan adalah {{ $memberNumber }}@endif.
@if ($isPassed)
@@ -67,7 +67,7 @@
Dengan hormatnya,
KOPERASI PERMODALAN KELANTAN BERHAD
-
MUHAMMAD NAFIS BIN ZAINUDDIN
+
MUHAMMAD NAFIS BIN ZAINUDIN
Pengurus Besar
b/p Setiausaha
@endsection
diff --git a/be/Modules/MembershipApplication/Routes/api.php b/be/Modules/MembershipApplication/Routes/api.php
index 5259d36..7c5e34c 100644
--- a/be/Modules/MembershipApplication/Routes/api.php
+++ b/be/Modules/MembershipApplication/Routes/api.php
@@ -38,6 +38,7 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
// Membership application upload document routes
Route::post('membership-applications/{membershipApplication}/documents', [MembershipApplicationController::class, 'uploadDocument'])->name('membership-application.upload-document');
+ Route::delete('membership-applications/{membershipApplication}/documents/{documentId}', [MembershipApplicationController::class, 'deleteDocument'])->name('membership-application.delete-document');
// download document routes
Route::get('membership-applications/{membershipApplication}/documents/{documentId}/download', [MembershipApplicationController::class, 'downloadDocument'])->name('membership-application.download-document');
diff --git a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
index 10c6284..f5a6ba8 100644
--- a/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
+++ b/be/Modules/MembershipApplication/Services/MembershipApplicationService.php
@@ -2,6 +2,7 @@
namespace Modules\MembershipApplication\Services;
+use App\Models\Document;
use App\Services\DocumentService;
use App\Services\LetterService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
@@ -137,15 +138,38 @@ class MembershipApplicationService
$this->documentService->validateFile($file);
- $application->documentsOfType($type)->get()->each(
- fn ($document) => $this->documentService->deleteDocument($document->id)
- );
+ 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([
@@ -230,8 +254,11 @@ class MembershipApplicationService
});
}
- public function complete(MembershipApplication $application, User $processedBy): MembershipApplication
- {
+ public function complete(
+ MembershipApplication $application,
+ User $processedBy,
+ string $boardMeetingReference,
+ ): MembershipApplication {
if ($application->status !== ApplicationStatus::PendingNotification) {
throw ValidationException::withMessages([
'status' => ['Permohonan ini tidak boleh diselesaikan pada masa ini.'],
@@ -244,13 +271,16 @@ class MembershipApplicationService
]);
}
- return DB::transaction(function () use ($application) {
+ return DB::transaction(function () use ($application, $boardMeetingReference) {
if ($application->board_result === BoardResult::Pass->value && ! $application->user_id) {
$user = $this->createMemberFromApplication($application);
$application->update(['user_id' => $user->id]);
+ $application->refresh();
}
- $this->sendResultNotification($application);
+ $resultLetter = $this->storeResultLetter($application, $boardMeetingReference);
+
+ $this->sendResultNotification($application, $resultLetter);
$application->update([
'status' => ApplicationStatus::Completed,
@@ -268,7 +298,7 @@ class MembershipApplicationService
* failed: list
* }
*/
- public function completeBatch(array $applicationIds, User $processedBy): array
+ public function completeBatch(array $applicationIds, User $processedBy, string $boardMeetingReference): array
{
$succeeded = [];
$failed = [];
@@ -287,7 +317,7 @@ class MembershipApplicationService
}
try {
- $succeeded[] = $this->complete($application, $processedBy);
+ $succeeded[] = $this->complete($application, $processedBy, $boardMeetingReference);
} catch (ValidationException $e) {
$failed[] = [
'id' => $id,
@@ -317,40 +347,47 @@ class MembershipApplicationService
}
return DB::transaction(function () use ($application, $boardMeetingReference) {
- $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);
- $pdf = $this->letterService->renderPdf('membershipapplication::pdf.approved-letter', $viewData);
- $fileName = 'surat-keputusan-'.$application->application_number.'.pdf';
-
- $this->documentService->storeGeneratedDocument(
- $application,
- $pdf,
- $fileName,
- MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE,
- );
+ $this->storeResultLetter($application, $boardMeetingReference);
return $this->repository->findByIdWithRelations($application->id);
});
}
+ protected function storeResultLetter(
+ MembershipApplication $application,
+ string $boardMeetingReference,
+ ): 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);
+ $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
*/
@@ -378,12 +415,13 @@ class MembershipApplicationService
): array {
$applicant = $application->applicant;
$isPassed = $application->board_result === BoardResult::Pass->value;
- $monthlyShareInstallment = number_format(
- MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
- 2,
- '.',
- '',
- );
+ // $monthlyShareInstallment = number_format(
+ // MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
+ // 2,
+ // '.',
+ // '',
+ // );
+ $monthlyShareInstallment = 84;
return [
'application' => $application,
@@ -400,6 +438,7 @@ class MembershipApplicationService
'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
+ 'memberNumber' => $isPassed ? $application->user?->member_number : null,
];
}
@@ -488,20 +527,30 @@ class MembershipApplicationService
->notify(new MembershipApplicationSubmittedNotification($application));
}
- protected function sendResultNotification(MembershipApplication $application): void
+ protected function sendResultNotification(MembershipApplication $application, Document $resultLetter): void
{
$application->loadMissing('applicant');
$notification = $application->board_result === BoardResult::Pass->value
- ? new MembershipApplicationPassedNotification($application)
- : new MembershipApplicationFailedNotification($application);
+ ? new MembershipApplicationPassedNotification($application, $resultLetter)
+ : new MembershipApplicationFailedNotification($application, $resultLetter);
Notification::route('mail', $application->applicant->email)->notify($notification);
}
protected function generateMemberNumber(): int
{
- return ((int) User::max('member_number')) + 1;
+ $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;
}
/**
diff --git a/be/database/migrations/0001_01_01_000002_create_jobs_table.php b/be/database/migrations/0001_01_01_000002_create_jobs_table.php
index e8ec6ec..425e705 100644
--- a/be/database/migrations/0001_01_01_000002_create_jobs_table.php
+++ b/be/database/migrations/0001_01_01_000002_create_jobs_table.php
@@ -12,7 +12,7 @@ return new class extends Migration
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
@@ -22,7 +22,7 @@ return new class extends Migration
});
Schema::create('job_batches', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
@@ -35,7 +35,7 @@ return new class extends Migration
});
Schema::create('failed_jobs', function (Blueprint $table) {
- $table->uuid('id')->primary();
+ $table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
diff --git a/fe/src/assets/images/logo-kopkb.svg b/fe/src/assets/images/logo-kopkb.svg
deleted file mode 100644
index ef88ccb..0000000
--- a/fe/src/assets/images/logo-kopkb.svg
+++ /dev/null
@@ -1,418 +0,0 @@
-
-
diff --git a/fe/src/assets/images/logo.svg b/fe/src/assets/images/logo.svg
index 28c5228..ef88ccb 100644
--- a/fe/src/assets/images/logo.svg
+++ b/fe/src/assets/images/logo.svg
@@ -1,9 +1,418 @@
-