eHub — Domain Model¶
All entities derived from source code. Sections marked ⚠ flag ambiguities.
BaseEntity (shared by all TypeORM entities)¶
libs/database/src/entities/base.entity.ts
| Column | Type | Notes |
|---|---|---|
id | uuid | @PrimaryGeneratedColumn('uuid') |
createdAt | timestamptz | @CreateDateColumn |
updatedAt | timestamptz | @UpdateDateColumn |
createdBy | varchar | nullable |
updatedBy | varchar | nullable |
deletedAt | timestamp | @DeleteDateColumn (soft delete) |
1. Core Service (pencom_core_db2)¶
User → user¶
emailvarchar unique (normalised to lowercase on insert/update)passwordHashnullable;phoneNumberunique nullablefirstName,lastNameuserTypeenum:EMPLOYER,ADMINadminRoleenumAdminRolenullable — ⚠ onlySUPER_ADMINis declaredonboardingStatusenum:NOT_STARTED → OTP_VERIFIED → AUTHORIZED_CONTACT_PENDING → COMPLETEDisFirstLoginbool;isActivebool;lastLoginAttimestamp nullable- Relations:
@OneToOne Employer,@OneToMany Employer (registeredByAdmin)
Employer → employers¶
@OneToOne UseremployerCodevarchar unique nullable;cacNumbervarchar uniquecompanyName;companyAddresstext;tinnullablenumberOfEmployeesint default 0registrationStatus:pending_review | approved | rejectedfirstPaymentDatedate nullable — anchor for penalty gap detectiontourCompletedbool;complianceStatusenum nullable- Relations:
@OneToMany Employee,@OneToMany EmployerAuthorizedContact
Employee → employees¶
@ManyToOne EmployerfullName;rsaPinnullable;pencomPinnullable;dateOfAppointmentdateconsolidatedBHTdecimal(18,2)migratedFromECRSbool;ecrsEmployeeMigrationDatenullable@ManyToOne PFAstatusenum:ACTIVE | REMOVED_PENDING_APPROVAL | REMOVED_APPROVED | PENDING_APPROVAL | REJECTEDchangePendingAdminApprovalbool
EmployerAuthorizedContact → employer_authorized_contacts¶
employerCodevarchar(50) — FK toemployers.employerCodefullName;role;emailvarchar(150) unique;phoneNumberpreferredChannelvarchar(10) — DB check:'Email'or'SMS'consentGiven,isPhoneVerified,isEmailVerified— bool default false
EmployerRegistration → employer_registrations¶
- Registration funnel for employers without a PENCOM code.
companyName,emailAddress,organizationTypeCode,sectorCodeadditionalDatajsonb (org-type-specific fields)statusenumEmployerRegistrationStatusdefaultPENDING_VERIFICATION
PFA → pf_as¶
name,code,shortName— all unique nullablerestrictedbool default false- Seeded from Oracle
TBL_PFAviaPencomService.findPfaByAnyIdentifier
Discrepancy → discrepancies¶
employerCode,companyName,rcNumber,emailAddress,commenttextstatusenumDiscrepancyStatusdefaultOPEN@OneToMany DiscrepancyAdminNote
EmployeeChangeRequest → employee_change_requests¶
typeenum:ADDITION | REMOVAL | EDIT(⚠ EDIT throws "not yet supported")statusenum:PENDING | APPROVED | REJECTEDmetadatajsonb — full payload for admin review@ManyToOne Employee;@ManyToOne Employer
Admin auth entities¶
AdminOtpSession → admin_otp_sessions
userId,email,otpHash,expiresAtattemptCount,isUsed,isLocked,lockedUntilpurposeenum:LOGIN | PASSWORD_RESET
AdminSession → admin_sessions
userId,tokentext unique,expiresAt,isActive,lastActivityAt- Index:
(userId, isActive)
AdminJourneyPermission → admin_journey_permissions
userId,journey(9 code enum),role(FIRST_REVIEWER | SECOND_REVIEWER | APPROVER)- Unique index:
(userId, journey)
TutorialVideo → tutorial_videos¶
title,description,videoFileKey(MinIO),thumbnailFileKeytargetHubenum:ADMIN_HUB | EMPLOYER_PLATFORMisPublishedbool;durationSeconds;fileSizeBytescategory— ⚠ legacy column, always written null; active field istutorialCategoryvarchar
Mongo: Otp → otps collection¶
code(stored hashed);recipient,email,expiresAtverifiedbool default false- ⚠ No TTL index — expired records accumulate
2. Compliance Service (pencom_compliance)¶
EmployeeMonthlyContribution → employee_monthly_contributions (hot table)¶
employer_code,rsa_pin,employee_name,pfaemployee_contribution,employer_contributiondecimal(15,2)employee_avc,employer_avcdecimal(15,2)total_contribution,date_of_contribution,value_date,payment_datecontribution_typedefault'COM'job_id,source_row_number— for crash-safe resumepfcId— which integration partner uploaded this row
Key indexes:
(employerCode, valueDate)(rsaPin)UQ_employee_contributions_monthly— partial unique(employer_code, rsa_pin, date_of_contribution, value_date, contribution_type)WHERE all NOT NULL — deduplication constraint- Partial unique
(jobId, sourceRowNumber)WHERE both NOT NULL — per-job idempotency
EmployerMonthlyAggregate → employer_monthly_aggregates¶
employer_code,monthdate (first of month),contribution_typeemployee_count,total_contribution,avg_contribution,max_contribution,min_contribution- Unique:
(employer_code, month, contribution_type)
EmployerMonthlyPenalty → employer_monthly_penalties¶
employer_code,monthvarchar(7)'YYYY-MM'due_date,grace_end_date(= due_date + 11)status:GAP_DETECTED → PENALTY_COMPUTED → PARTIALLY_PAID → PAID_IN_FULLpenalty_rate— locked at first computationdays_in_default,penalty_amount,paid_amount,actual_contributiongrace_notified_at,last_notified_paid_amount— notification watermarks- Unique:
(employer_code, month)
ComplianceEvaluation → compliance_evaluation¶
employer_id,employer_code,cac_numberoverall_statusenumOverallComplianceStatusapproval_statusenumComplianceApprovalStatusrule_resultsjsonb;summaryjsonb;config_versionis_latestbool — only one row per employer hasis_latest=true- Relations:
OneToMany Certificate,OneToMany PccComment,OneToMany PccActivity
Certificate → certificate¶
evaluation_idFKComplianceEvaluationCASCADEemployer_id,employer_code,employer_name,rc_numberpcc_numberunique (formatPCC/YYYY/NNNNNN)issue_date,expiry_datecertificate_path(MinIO URL)status:Active | Expired | Revokedverification_urluniquedigital_signature
PccActivity → pcc_activities¶
activity_type:FIRST_REVIEW | SECOND_REVIEW | APPROVAL | REJECTION | QUERY | EMPLOYER_RESPONSEactor_id,actor_name,actor_role;previous_status,new_status
GroupLifeInsurance → group_life_insurance¶
employer_id;commencement_date;expiry_datenumber_of_lives_covered;insurance_company;policy_number;sum_assuredstatusenumGroupLifeInsuranceStatusfirst_reviewed_by/at,second_reviewed_by/at,approved_by/at
WebhookAttempt → webhook_attempts¶
entityId,event,payloadjsonbattemptCountdefault 0;status(PENDING → SENT/FAILED)
CronJobRun → cron_job_runs¶
job_name,schedule_expressionstarted_at,finished_at,duration_msstatus,records_processed,gaps_detected- Used for both scheduled cron observability and tracking long-running repair runs
JobStatus → job_status (shared with external-gateway)¶
Stored in the compliance DB. Written by external-gateway upload ingress, read by both.
idempotency_keyuniqueowner_iduuid (pfcId);owner_type;s3_keystatusenum:PENDING | PROCESSING | COMPLETED | FAILED | PARTIAL_SUCCESSjob_id(Bull queue job ID)- Checkpoint fields:
lastProcessedRow,totalRows,processedRows,validRows,invalidRows metajsonb;error_detailsjsonb
3. Payments Service (pencom_payments)¶
Payment → payment¶
identifiervarchar — business key (e.g. complianceevaluationId)employerCodevarcharremitaRrrvarchar;orderIdvarchar NOT NULL (ORD_${Date.now()})amountnumeric(18,2) — stores billed total (serviceFee + applied charges), NOT display total ⚠statusenum:pending | successful | failed | cancelled(⚠cancellednever set by code)paymentTypeenum:certificate_generation(only value)metadatajsonb —{email, phoneNumber, description, charges, totalAmount, remitaAmount}paymentChannel;failureReason;verifiedDate
⚠ No indexes on any lookup column. ⚠ No unique constraint on orderId or (identifier, paymentType).
4. External-Integrations Service (pencom_external_integrations)¶
CacCompany → cac_companies¶
cacNumberunique;companyName;companyType;sectoremail,phone,tin,directorName;employerCodenullable unique- No actual outbound CAC API call — this is local CRUD data, seeded manually or from Oracle.
IntegrationPartner → integration_partners¶
apiKeyvarchar(64) unique (auto-generated)clientIdvarchar(32) unique (auto-generated:client_<16hex>)clientSecretHashvarchar(128) — bcrypt(plaintext, 10). Plaintext returned once on create, never stored.name;organizationType;isActiveboolwhitelistedIpssimple-array — ⚠ check commented out, never enforcedwebhookUrl,webhookSecret,webhookEvents— for outbound eHub → PFC webhooks
5. Audit Service (pencom_audit)¶
AuditLog → audit_logs¶
service,action,outcomeenum (SUCCESS | FAILURE | PENDING | REVERTED)entityTypevarchar NOT NULL — ⚠AuditService.createAuditLognever sets this before insertentityId;correlationId;actorType;userIdbeforeSnapshot,afterSnapshot,metadata— all jsonb- No retention/archival — table grows unbounded.
6. Notifications Service (MongoDB)¶
Notifications → pencom_notifications collection¶
recepientId— ⚠ typo persists across schema, DTOs, controllers, eventscontextenumNotificationContext;notificationTypeenum:email | smssourceServiceenum;sourceEventstringstatusenum:pending | sent | failed(defaultpending)dynamicDataMixed;referenceIdstring indexedretryCount— ⚠ never incremented
OTPs → otps collection (in core's MongoDB, not notifications')¶
- See Core Service section above. Note: env var is
NOTIFICATIONS_MONGO_DB_URLbut consumed by core, not notifications — ⚠ misleading name.
7. Oracle Entities (read-only, pencomConnection)¶
Two processes open Oracle pools: core and compliance (via cross-app import of PencomModule). All Oracle reads are user-triggered — no cron or worker queries Oracle.
| Entity | Oracle Table | Status |
|---|---|---|
PencomEmployer | TBL_EMPLOYER_CODE_MST | Active — employer lookup during auth, registration, discrepancy |
PencomContributorBiodata | BIODATA1 | Active — bulk employee migration on first login and admin employer view |
PencomPfa | TBL_PFA | Active — PFA fallback during registration |
PencomCertificateRequest3 | CERTIFICATE_REQUEST3 | Active — historical PCC migration (compliance only) |
PencomCertificate | ECRSPCC.CERTIFICATE_REQUESTS | ⚠ Registered but no repository is injected — dead entity |
8. Cross-Service Relationships¶
No database-level foreign keys exist between services. All cross-service references are loose string keys:
Employer.employerCode— universal join key. Appears inemployee_monthly_contributions,employer_monthly_penalties,compliance_evaluation,certificate,cac_companies, andpayment.Compliance.payment_idreferencespayments.payment.id— resolved via HTTP.Notifications.recepientId— typically aUser.idor authorized-contact ID; not enforced by DB constraint.AuditLog.entityId— whatever the producing service provides; no FK.
9. Key Enums¶
| Enum | Values |
|---|---|
OnboardingStatus | NOT_STARTED, OTP_VERIFIED, AUTHORIZED_CONTACT_PENDING, COMPLETED |
EmployeeStatus | ACTIVE, REMOVED_PENDING_APPROVAL, REMOVED_APPROVED, PENDING_APPROVAL, REJECTED |
ChangeRequestType | ADDITION, REMOVAL, EDIT (EDIT throws) |
PaymentStatus | PENDING, SUCCESSFUL, FAILED, CANCELLED (CANCELLED never set) |
EJobStatus | PENDING, PROCESSING, COMPLETED, FAILED, PARTIAL_SUCCESS |
ComplianceApprovalStatus | PENDING, REVIEWED_FIRST, REVIEWED_SECOND, APPROVED, QUERIED, EMPLOYER_RESPONDED, REJECTED, EXPIRED |
Journey (RBAC) | GROUP_LIFE_INSURANCE, PCC_APPLICATION, EMPLOYEE_MANAGEMENT, PAYMENT_MANAGEMENT, HELP_AND_SUPPORT, 4× EMPLOYER_CODE_REQUEST_* |
AdminRole | SUPER_ADMIN (only declared value) |