diff --git a/.env.example b/.env.example
index 30da688..f0a4c9c 100644
--- a/.env.example
+++ b/.env.example
@@ -67,11 +67,12 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
# Browsershot (PDF generation via headless Chrome + Puppeteer in be/)
+# Required in Docker/Sail. After first sail up: sail npm install && sail npx puppeteer browsers install chrome-headless-shell
+BROWSERSHOT_NO_SANDBOX=true
# BROWSERSHOT_NODE_BINARY=
# BROWSERSHOT_NPM_BINARY=
# BROWSERSHOT_NODE_MODULE_PATH=
# BROWSERSHOT_CHROME_PATH=
-# BROWSERSHOT_NO_SANDBOX=false
# BROWSERSHOT_TIMEOUT=60
# HttpOnly Sanctum token cookie (set on api.* domain)
diff --git a/Modules/MembershipApplication/Entities/MembershipApplication.php b/Modules/MembershipApplication/Entities/MembershipApplication.php
index 57409a1..9464541 100644
--- a/Modules/MembershipApplication/Entities/MembershipApplication.php
+++ b/Modules/MembershipApplication/Entities/MembershipApplication.php
@@ -2,11 +2,13 @@
namespace Modules\MembershipApplication\Entities;
+use App\Models\Document;
use App\Traits\HasDocuments;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
+use Illuminate\Database\Eloquent\Relations\MorphOne;
use Modules\Auth\Entities\User;
use Modules\MembershipApplication\Enums\ApplicationStatus;
@@ -14,6 +16,12 @@ class MembershipApplication extends Model
{
use HasDocuments, HasUuids;
+ public const RESULT_LETTER_DOCUMENT_TYPE = 'result_letter';
+
+ public const MINIMUM_SHARE_CAPITAL = 500;
+
+ public const SHARE_INSTALLMENT_MONTHS = 6;
+
protected $table = 'membership_applications';
/**
@@ -65,4 +73,10 @@ class MembershipApplication extends Model
{
return $this->belongsTo(User::class);
}
+
+ public function resultLetterDocument(): MorphOne
+ {
+ return $this->morphOne(Document::class, 'documentable')
+ ->where('type', self::RESULT_LETTER_DOCUMENT_TYPE);
+ }
}
diff --git a/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php b/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
index 2a842f6..006de7b 100644
--- a/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
+++ b/Modules/MembershipApplication/Http/Controllers/MembershipApplicationController.php
@@ -17,10 +17,12 @@ 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\GenerateResultLetterRequest;
use Modules\MembershipApplication\Http\Requests\ManagementReviewRequest;
use Modules\MembershipApplication\Http\Requests\UpdateMembershipApplicationRequest;
use Modules\MembershipApplication\Http\Requests\UploadMembershipApplicationDocumentRequest;
use Modules\MembershipApplication\Services\MembershipApplicationService;
+use Modules\MembershipApplication\Transformers\MembershipApplicationDocumentResource;
use Modules\MembershipApplication\Transformers\MembershipApplicationListResource;
use Modules\MembershipApplication\Transformers\MembershipApplicationResource;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -243,6 +245,29 @@ class MembershipApplicationController extends Controller
return $this->documentService->downloadDocument($document->id);
}
+ public function generateResultLetter(GenerateResultLetterRequest $request, MembershipApplication $membershipApplication,): JsonResponse {
+ $this->authorize('generateResultLetter', $membershipApplication);
+
+ $application = $this->membershipApplicationService->generateResultLetter(
+ $membershipApplication,
+ $request->validated('board_meeting_reference'),
+ );
+
+ $resultLetter = $application->documents
+ ->firstWhere('type', MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE);
+
+ return response()->json([
+ 'success' => true,
+ 'message' => 'Surat keputusan berjaya dijana.',
+ 'data' => [
+ 'application' => new MembershipApplicationResource($application),
+ 'document' => $resultLetter
+ ? new MembershipApplicationDocumentResource($resultLetter)
+ : null,
+ ],
+ ]);
+ }
+
public function approvedLetter(Request $request, MembershipApplication $membershipApplication): Response
{
$this->authorize('view', $membershipApplication);
@@ -253,10 +278,18 @@ class MembershipApplicationController extends Controller
abort(404, 'Permohonan tidak dijumpai.');
}
+ $boardMeetingReference = (string) $request->query(
+ 'board_meeting_reference',
+ 'Mesyuarat Lembaga',
+ );
+
$view = 'membershipapplication::pdf.approved-letter';
- $data = ['application' => $application];
+ $data = $this->membershipApplicationService->getResultLetterViewData(
+ $application,
+ $boardMeetingReference,
+ );
$format = $request->query('format', 'html');
- $filename = 'surat-kelulusan-'.$application->application_number.'.pdf';
+ $filename = 'surat-keputusan-'.$application->application_number.'.pdf';
if ($format === 'pdf') {
return $this->letterService->pdfResponse($view, $data, $filename);
diff --git a/Modules/MembershipApplication/Http/Requests/GenerateResultLetterRequest.php b/Modules/MembershipApplication/Http/Requests/GenerateResultLetterRequest.php
new file mode 100644
index 0000000..72aa39c
--- /dev/null
+++ b/Modules/MembershipApplication/Http/Requests/GenerateResultLetterRequest.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/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php b/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php
index 9ce7624..764e7f7 100644
--- a/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php
+++ b/Modules/MembershipApplication/Policies/MembershipApplicationPolicy.php
@@ -65,6 +65,14 @@ class MembershipApplicationPolicy
return $user->hasPermissionTo('tetapkan pencadang penyokong');
}
+ /**
+ * Generate the result letter PDF for a completed application.
+ */
+ public function generateResultLetter($user, ?MembershipApplication $membershipApplication = null): bool
+ {
+ return $user->hasPermissionTo('jana surat keputusan keahlian');
+ }
+
/**
* Admin: edit application details before completion.
*/
diff --git a/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php b/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php
index 26c48fc..bef9c73 100644
--- a/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php
+++ b/Modules/MembershipApplication/Providers/MembershipApplicationServiceProvider.php
@@ -155,7 +155,7 @@ class MembershipApplicationServiceProvider extends ServiceProvider
public function registerViews(): void
{
$viewPath = resource_path('views/modules/'.$this->nameLower);
- $sourcePath = module_path($this->name, 'Resources/Views');
+ $sourcePath = module_path($this->name, 'Resources/views');
$this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']);
diff --git a/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php b/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
index 9e3d1fd..679ef22 100644
--- a/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
+++ b/Modules/MembershipApplication/Repositories/MembershipApplicationRepository.php
@@ -46,7 +46,14 @@ class MembershipApplicationRepository implements MembershipApplicationRepository
$sortOrder = strtolower($sortOrder) === 'asc' ? 'asc' : 'desc';
$query = MembershipApplication::query()
- ->with(['applicant:id,membership_application_id,name,email,ic_number'])
+ ->with([
+ 'applicant:id,membership_application_id,name,email,ic_number',
+ 'resultLetterDocument:id,documentable_type,documentable_id,name,type,mime_type,file_size',
+ ])
+ ->withCount([
+ 'documents as result_letter_count' => fn ($documentQuery) => $documentQuery
+ ->where('type', MembershipApplication::RESULT_LETTER_DOCUMENT_TYPE),
+ ])
->orderBy($sortBy, $sortOrder);
if ($status !== null && $status !== '') {
diff --git a/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php b/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
index 3358ce7..cdfc055 100644
--- a/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
+++ b/Modules/MembershipApplication/Resources/views/pdf/approved-letter.blade.php
@@ -1,12 +1,73 @@
-{{--
- Approval letter — extends the shared base layout.
- Fill in sections below when the final design is ready.
---}}
@extends('pdf.layouts.letter')
-{{-- @section('letter-reference', $application->application_number) --}}
-{{-- @section('letter-date', ...) --}}
-{{-- @section('letter-recipient') ... @endsection --}}
-{{-- @section('letter-subject') PERKARA: ... @endsection --}}
-{{-- @section('letter-body') ... @endsection --}}
-{{-- @section('letter-signature') ... @endsection --}}
+@section('letter-title', 'Surat')
+
+@section('letter-reference', $application->application_number)
+
+@section('letter-date', now()->translatedFormat('d F Y'))
+
+@section('letter-recipient')
+ {{ $applicant->name }}
+ {!! nl2br(e($applicant->address)) !!}
+ {{ $applicant->postcode }}
+@endsection
+
+@section('letter-subject')
+ {{ $letterSubject }}
+@endsection
+
+@section('letter-body')
+
+ Dengan segala hormatnya saya merujuk kepada perkara di atas.
+
+
+ 2. {{ $openingTone }} dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
+ Permodalan Kelantan Berhad (KoPKB) telah {{ $boardResultLabel }} dalam {{ $boardMeetingReference }}.
+
+
+ @if ($isPassed)
+
+ 3. Pihak Koperasi akan membuat potongan gaji melalui majikan tuan sebagaimana ketetapan berikut:-
+
+ - Saham
+
+ - Potongan sebanyak RM {{ $monthlyShareInstallment }} ({{ $shareInstallmentMonths }} bulan)
+ - Potongan sebanyak RM {{ $stockMonthlyContribution }} sehingga Syer Maksima
+
+ - Yuran
+
+ - Potongan sebanyak RM {{ $feeMonthlyContribution }} setiap bulan
+
+ - Fi Masuk
+
+ - Potongan sebanyak RM 10.00 (Pembayaran Pertama)
+
+
+
+
+ 4. Untuk makluman tuan, kelulusan keanggotaan ini adalah bersyarat di mana tuan dikehendaki
+ menjelaskan modal syer minimum sebanyak RM{{ $minimumShareCapital }}.
+ Bayaran tersebut perlu dibuat secara ansuran melalui potongan gaji dalam tempoh enam (6) bulan.
+ Pemenuhan syarat ini adalah bagi melayakkan tuan memperoleh hak, kewajipan dan liabiliti sebagai anggota KoPKB
+ sebagaimana terkandung di dalam Undang-Undang Kecil (UUK), Perkara 59 yang telah didaftarkan pada 11 September 2024.
+ Manakala had maksima modal syer adalah sebanyak RM5,000.00.
+
+ @else
+
+ 3. Oleh yang demikian, permohonan tuan untuk menjadi anggota KoPKB tidak dapat diluluskan pada masa ini.
+
+ @endif
+@endsection
+
+@section('letter-closing')
+ Sekian, terima kasih.
+@endsection
+
+@section('letter-signature')
+
+ KOPERASI PERMODALAN KELANTAN BERHAD
+
+ MUHAMMAD NAFIS BIN ZAINUDDIN
+ Pengurus Besar
+ b/p Setiausaha
+@endsection
diff --git a/Modules/MembershipApplication/Routes/api.php b/Modules/MembershipApplication/Routes/api.php
index 7989d78..5259d36 100644
--- a/Modules/MembershipApplication/Routes/api.php
+++ b/Modules/MembershipApplication/Routes/api.php
@@ -43,4 +43,6 @@ Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(funct
// approved letter preview (html or pdf)
Route::get('membership-applications/{membershipApplication}/approved-letter', [MembershipApplicationController::class, 'approvedLetter'])->name('membership-application.approved-letter');
+ // generate and store result letter (one-time)
+ Route::post('membership-applications/{membershipApplication}/result-letter', [MembershipApplicationController::class, 'generateResultLetter'])->name('membership-application.generate-result-letter');
});
diff --git a/Modules/MembershipApplication/Services/MembershipApplicationService.php b/Modules/MembershipApplication/Services/MembershipApplicationService.php
index 7cb0712..10c6284 100644
--- a/Modules/MembershipApplication/Services/MembershipApplicationService.php
+++ b/Modules/MembershipApplication/Services/MembershipApplicationService.php
@@ -3,6 +3,7 @@
namespace Modules\MembershipApplication\Services;
use App\Services\DocumentService;
+use App\Services\LetterService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB;
@@ -30,6 +31,7 @@ class MembershipApplicationService
public function __construct(
protected MembershipApplicationRepositoryInterface $repository,
protected DocumentService $documentService,
+ protected LetterService $letterService,
) {}
/**
@@ -301,6 +303,106 @@ class MembershipApplicationService
];
}
+ public function generateResultLetter(MembershipApplication $application, string $boardMeetingReference,): 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) {
+ $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,
+ );
+
+ return $this->repository->findByIdWithRelations($application->id);
+ });
+ }
+
+ /**
+ * @return array
+ */
+ public function getResultLetterViewData(
+ MembershipApplication $application,
+ string $boardMeetingReference,
+ ): array {
+ $application = $this->repository->findByIdWithRelations($application->id);
+
+ if (! $application || ! $application->applicant) {
+ throw ValidationException::withMessages([
+ 'application' => ['Maklumat pemohon tidak lengkap.'],
+ ]);
+ }
+
+ return $this->buildResultLetterViewData($application, $boardMeetingReference);
+ }
+
+ /**
+ * @return array
+ */
+ protected function buildResultLetterViewData(
+ MembershipApplication $application,
+ string $boardMeetingReference,
+ ): array {
+ $applicant = $application->applicant;
+ $isPassed = $application->board_result === BoardResult::Pass->value;
+ $monthlyShareInstallment = number_format(
+ MembershipApplication::MINIMUM_SHARE_CAPITAL / MembershipApplication::SHARE_INSTALLMENT_MONTHS,
+ 2,
+ '.',
+ '',
+ );
+
+ return [
+ 'application' => $application,
+ 'applicant' => $applicant,
+ 'boardMeetingReference' => $boardMeetingReference,
+ 'isPassed' => $isPassed,
+ 'boardResultLabel' => $isPassed ? 'lulus' : 'gagal',
+ 'openingTone' => $isPassed ? 'Sukacita' : 'Dukacita',
+ 'letterSubject' => $isPassed
+ ? 'KELULUSAN KEANGGOTAAN KOPERASI PERMODALAN KELANTAN BERHAD'
+ : 'KEPUTUSAN PERMOHONAN KEANGGOTAAN KOPERASI PERMODALAN KELANTAN BERHAD',
+ 'monthlyShareInstallment' => $monthlyShareInstallment,
+ 'stockMonthlyContribution' => number_format((float) $applicant->stock_monthly_contribution, 2, '.', ''),
+ 'feeMonthlyContribution' => number_format((float) $applicant->fee_monthly_contribution, 2, '.', ''),
+ 'shareInstallmentMonths' => MembershipApplication::SHARE_INSTALLMENT_MONTHS,
+ 'minimumShareCapital' => number_format(MembershipApplication::MINIMUM_SHARE_CAPITAL, 2, '.', ''),
+ ];
+ }
+
protected function createMemberFromApplication(MembershipApplication $application): User
{
$application->loadMissing(['applicant', 'heirs']);
diff --git a/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php b/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php
index eafacb6..161060d 100644
--- a/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php
+++ b/Modules/MembershipApplication/Transformers/MembershipApplicationListResource.php
@@ -15,6 +15,11 @@ class MembershipApplicationListResource extends JsonResource
'status' => $this->status?->value ?? $this->status,
'board_result' => $this->board_result ?: null,
'submitted_at' => $this->submitted_at,
+ 'has_result_letter' => ($this->result_letter_count ?? 0) > 0,
+ 'result_letter_document' => $this->when(
+ ($this->result_letter_count ?? 0) > 0 && $this->relationLoaded('resultLetterDocument') && $this->resultLetterDocument,
+ fn () => new MembershipApplicationDocumentResource($this->resultLetterDocument),
+ ),
'applicant' => $this->whenLoaded('applicant', fn () => [
'name' => $this->applicant->name,
'email' => $this->applicant->email,
diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php
index 4b78240..a69d239 100644
--- a/app/Services/DocumentService.php
+++ b/app/Services/DocumentService.php
@@ -13,12 +13,7 @@ class DocumentService
/**
* Upload a document for any model.
*/
- public function uploadDocument(
- Model $model,
- UploadedFile $file,
- string $documentType = 'general',
- ?string $description = null
- ): Document {
+ public function uploadDocument(Model $model, UploadedFile $file, string $documentType = 'general', ?string $description = null): Document {
$fileName = time().'_'.$file->getClientOriginalName();
$folderName = strtolower(class_basename($model));
$filePath = $file->storeAs("documents/{$folderName}", $fileName, Document::STORAGE_DISK);
@@ -34,6 +29,27 @@ class DocumentService
]);
}
+ /**
+ * Store generated file contents (e.g. PDF) for a model.
+ */
+ public function storeGeneratedDocument(Model $model, string $contents, string $fileName, string $documentType = 'general', string $mimeType = 'application/pdf', ?string $description = null,): Document {
+ $folderName = strtolower(class_basename($model));
+ $storedFileName = time().'_'.$fileName;
+ $filePath = "documents/{$folderName}/{$storedFileName}";
+
+ Storage::disk(Document::STORAGE_DISK)->put($filePath, $contents);
+
+ return $model->documents()->create([
+ 'name' => $fileName,
+ 'path' => $filePath,
+ 'file_size' => strlen($contents),
+ 'mime_type' => $mimeType,
+ 'type' => $documentType,
+ 'description' => $description,
+ 'uploaded_by' => auth()->id(),
+ ]);
+ }
+
/**
* Delete a document.
*/
diff --git a/app/Services/LetterService.php b/app/Services/LetterService.php
index efc6931..ba27182 100644
--- a/app/Services/LetterService.php
+++ b/app/Services/LetterService.php
@@ -33,7 +33,7 @@ class LetterService
*/
public function pdfResponse(string $view, array $data = [], string $filename = 'letter.pdf'): Response
{
- $pdf = $this->makeBrowsershot($this->renderHtml($view, $data))->pdf();
+ $pdf = $this->renderPdf($view, $data);
return response($pdf, SymfonyResponse::HTTP_OK, [
'Content-Type' => 'application/pdf',
@@ -41,6 +41,14 @@ class LetterService
]);
}
+ /**
+ * @param array $data
+ */
+ public function renderPdf(string $view, array $data = []): string
+ {
+ return $this->makeBrowsershot($this->renderHtml($view, $data))->pdf();
+ }
+
/**
* @param array $overrides
* @return array
diff --git a/docker-compose.yml b/docker-compose.yml
index c0b34fc..5b2aaf7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,5 +1,8 @@
services:
laravel.test:
+ # Apple Silicon / OrbStack: Puppeteer's Chrome must match container arch.
+ # amd64 emulation avoids linux_arm + linux64 chrome-headless-shell mismatch.
+ platform: linux/amd64
build:
context: "./vendor/laravel/sail/runtimes/8.4"
dockerfile: Dockerfile
diff --git a/package.json b/package.json
index 63e1661..3b8cc5f 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"type": "module",
"scripts": {
"build": "vite build",
- "dev": "vite"
+ "dev": "vite",
+ "browsershot:install": "puppeteer browsers install chrome-headless-shell"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
diff --git a/public/favicon.ico b/public/favicon.ico
index e69de29..4aea46d 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ
diff --git a/resources/views/pdf/preview-approved-membership.blade.php b/resources/views/pdf/preview-approved-membership.blade.php
index 89c7937..fd2819b 100644
--- a/resources/views/pdf/preview-approved-membership.blade.php
+++ b/resources/views/pdf/preview-approved-membership.blade.php
@@ -7,6 +7,7 @@
@section('letter-date', now()->translatedFormat('d F Y'))
@section('letter-recipient')
+{{-- auto fetch from membership_application_applicants --}}
Ahmad bin Abdullah
No. 12, Jalan Contoh,
43000 Kajang, Selangor
@@ -26,20 +27,19 @@
2. Sukacita dimaklumkan bahawa, permohonan tuan untuk menjadi anggota Koperasi
- Permodalan Kelantan Berhad(KoPKB) telah DILULUSKAN dalam Mesyuarat Lembaga
- Koperasi Bil. 4-2025/2026 pada 24 Jun 2026.
+ Permodalan Kelantan Berhad(KoPKB) telah {{ membership_applications.board_result }} dalam {{ User manual fills before generate }}
3. Pihak Koperasi akan membuat potongan gaji melalui majikan tuan sebagaiman ketetapan berikut:-
- Saham
- - Potongan sebanyak RM 84.00 (Mei 2026 - Jun 2026)
- - Potongan sebanyak RM 50.00 sehingga Syer Maksima
+ - Potongan sebanyak RM 84.00 {{(6 months auto calculate)}}
+ - Potongan sebanyak RM {{ membership_application_applicants.stock_monthly_contribution }} sehingga Syer Maksima
- Yuran
- - Potongan sebanyak RM 50.00 setiap bulan
+ - Potongan sebanyak RM {{ membership_application_applicants.fee_monthly_contribution }} setiap bulan
- Fi Masuk