ofcoop-shared-core 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/OfcoopCore.d.ts +60 -0
- package/dist/OfcoopCore.js +273 -0
- package/dist/contracts/MemberContract.d.ts +52 -0
- package/dist/contracts/MemberContract.js +2 -0
- package/dist/contracts/MemberNumberPolicyContract.d.ts +44 -0
- package/dist/contracts/MemberNumberPolicyContract.js +2 -0
- package/dist/contracts/SavingComplianceSnapshotContract.d.ts +34 -0
- package/dist/contracts/SavingComplianceSnapshotContract.js +2 -0
- package/dist/contracts/SavingLedgerContract.d.ts +49 -0
- package/dist/contracts/SavingLedgerContract.js +2 -0
- package/dist/contracts/SavingPolicyContract.d.ts +63 -0
- package/dist/contracts/SavingPolicyContract.js +2 -0
- package/dist/contracts/ShuConfigContract.d.ts +39 -0
- package/dist/contracts/ShuConfigContract.js +2 -0
- package/dist/contracts/ShuContract.d.ts +58 -0
- package/dist/contracts/ShuContract.js +2 -0
- package/dist/contracts/crossDomainPrimitives.d.ts +5 -0
- package/dist/contracts/crossDomainPrimitives.js +2 -0
- package/dist/data/applyPendingMigrations.d.ts +2 -0
- package/dist/data/applyPendingMigrations.js +30 -0
- package/dist/data/migrations.d.ts +2 -0
- package/dist/data/migrations.js +20 -0
- package/dist/data/repositories.d.ts +114 -0
- package/dist/data/repositories.js +324 -0
- package/dist/data/schemas.d.ts +14 -0
- package/dist/data/schemas.js +179 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +54 -0
- package/dist/services/AuditTrailQueryService.d.ts +35 -0
- package/dist/services/AuditTrailQueryService.js +62 -0
- package/dist/services/CoopOrchestrationService.d.ts +35 -0
- package/dist/services/CoopOrchestrationService.js +71 -0
- package/dist/services/DailyOpsService.d.ts +2 -0
- package/dist/services/DailyOpsService.js +7 -0
- package/dist/services/DashboardSummaryService.d.ts +180 -0
- package/dist/services/DashboardSummaryService.js +211 -0
- package/dist/services/DashboardViewModelService.d.ts +42 -0
- package/dist/services/DashboardViewModelService.js +193 -0
- package/dist/services/Member360Service.d.ts +75 -0
- package/dist/services/Member360Service.js +79 -0
- package/dist/services/Member360ViewModelService.d.ts +33 -0
- package/dist/services/Member360ViewModelService.js +66 -0
- package/dist/services/MemberNumberPolicyService.d.ts +4 -0
- package/dist/services/MemberNumberPolicyService.js +18 -0
- package/dist/services/MemberService.d.ts +4 -0
- package/dist/services/MemberService.js +18 -0
- package/dist/services/ReportSummaryService.d.ts +81 -0
- package/dist/services/ReportSummaryService.js +160 -0
- package/dist/services/ReportViewModelService.d.ts +24 -0
- package/dist/services/ReportViewModelService.js +90 -0
- package/dist/services/SavingComplianceSnapshotService.d.ts +4 -0
- package/dist/services/SavingComplianceSnapshotService.js +10 -0
- package/dist/services/SavingLedgerService.d.ts +4 -0
- package/dist/services/SavingLedgerService.js +13 -0
- package/dist/services/SavingPolicyService.d.ts +5 -0
- package/dist/services/SavingPolicyService.js +20 -0
- package/dist/services/ShuConfigService.d.ts +4 -0
- package/dist/services/ShuConfigService.js +16 -0
- package/dist/services/ShuService.d.ts +4 -0
- package/dist/services/ShuService.js +14 -0
- package/dist/services/createActivityAuditTrailProvider.d.ts +22 -0
- package/dist/services/createActivityAuditTrailProvider.js +73 -0
- package/dist/services/createAuditTrailCompositionProvider.d.ts +13 -0
- package/dist/services/createAuditTrailCompositionProvider.js +21 -0
- package/dist/services/createDbAdapterOfcoopServices.d.ts +20 -0
- package/dist/services/createDbAdapterOfcoopServices.js +23 -0
- package/dist/services/createMember360CompositionProviders.d.ts +6 -0
- package/dist/services/createMember360CompositionProviders.js +43 -0
- package/dist/services/createOfauthCompositionProviders.d.ts +39 -0
- package/dist/services/createOfauthCompositionProviders.js +80 -0
- package/dist/services/createOfcoopCreditCompositionProviders.d.ts +59 -0
- package/dist/services/createOfcoopCreditCompositionProviders.js +233 -0
- package/dist/services/createOfcoopDashboardCompositionProviders.d.ts +18 -0
- package/dist/services/createOfcoopDashboardCompositionProviders.js +228 -0
- package/dist/services/createOfcoopDomainAuditCompositionProviders.d.ts +36 -0
- package/dist/services/createOfcoopDomainAuditCompositionProviders.js +117 -0
- package/dist/services/createOfcoopReportCompositionProviders.d.ts +10 -0
- package/dist/services/createOfcoopReportCompositionProviders.js +30 -0
- package/dist/services/errors.d.ts +8 -0
- package/dist/services/errors.js +23 -0
- package/dist/services/impl/DbAdapterMemberNumberPolicyService.d.ts +9 -0
- package/dist/services/impl/DbAdapterMemberNumberPolicyService.js +37 -0
- package/dist/services/impl/DbAdapterMemberService.d.ts +21 -0
- package/dist/services/impl/DbAdapterMemberService.js +142 -0
- package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.d.ts +9 -0
- package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.js +74 -0
- package/dist/services/impl/DbAdapterSavingLedgerService.d.ts +18 -0
- package/dist/services/impl/DbAdapterSavingLedgerService.js +115 -0
- package/dist/services/impl/DbAdapterSavingPolicyService.d.ts +14 -0
- package/dist/services/impl/DbAdapterSavingPolicyService.js +139 -0
- package/dist/services/impl/DbAdapterShuConfigService.d.ts +12 -0
- package/dist/services/impl/DbAdapterShuConfigService.js +62 -0
- package/dist/services/impl/DbAdapterShuService.d.ts +26 -0
- package/dist/services/impl/DbAdapterShuService.js +376 -0
- package/dist/services/impl/runtimeSupport.d.ts +18 -0
- package/dist/services/impl/runtimeSupport.js +29 -0
- package/package.json +41 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { MemberContract, MemberServiceContract } from '../contracts/MemberContract';
|
|
2
|
+
import type { SavingBalance, SavingLedgerContract, SavingLedgerServiceContract, SavingLedgerQueryOptions } from '../contracts/SavingLedgerContract';
|
|
3
|
+
export interface Member360ScopeRef {
|
|
4
|
+
tenantId?: string | null;
|
|
5
|
+
branchId?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface Member360LoanSummary {
|
|
8
|
+
totalLoans: number;
|
|
9
|
+
activeLoans: number;
|
|
10
|
+
closedLoans: number;
|
|
11
|
+
defaultedLoans: number;
|
|
12
|
+
totalPrincipalAmount: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Member360CreditProviderResult {
|
|
15
|
+
summary: Member360LoanSummary;
|
|
16
|
+
recentLoans?: Array<{
|
|
17
|
+
loanId: string;
|
|
18
|
+
status: 'active' | 'closed' | 'defaulted';
|
|
19
|
+
principalAmount: number;
|
|
20
|
+
startDate: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
export interface Member360AuditItem {
|
|
24
|
+
eventId: string;
|
|
25
|
+
eventType: string;
|
|
26
|
+
occurredAt: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Member360SettlementSummary {
|
|
30
|
+
totalOutstanding: number;
|
|
31
|
+
totalOverdue: number;
|
|
32
|
+
settlementStatus: 'clear' | 'pending' | 'overdue';
|
|
33
|
+
notes?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface Member360Providers {
|
|
36
|
+
getCreditByMember?: (memberId: string, scope?: Member360ScopeRef) => Promise<Member360CreditProviderResult | null>;
|
|
37
|
+
getAuditByMember?: (memberId: string, scope?: Member360ScopeRef) => Promise<Member360AuditItem[]>;
|
|
38
|
+
getSettlementSummaryByMember?: (memberId: string, scope?: Member360ScopeRef) => Promise<Member360SettlementSummary | null>;
|
|
39
|
+
}
|
|
40
|
+
export interface Member360Dependencies {
|
|
41
|
+
memberService: MemberServiceContract;
|
|
42
|
+
savingLedgerService: SavingLedgerServiceContract;
|
|
43
|
+
providers?: Member360Providers;
|
|
44
|
+
}
|
|
45
|
+
export interface GetMember360Input {
|
|
46
|
+
memberId: string;
|
|
47
|
+
scope?: Member360ScopeRef;
|
|
48
|
+
savingQuery?: SavingLedgerQueryOptions;
|
|
49
|
+
timelineLimit?: number;
|
|
50
|
+
accessContext?: Member360AccessContext;
|
|
51
|
+
}
|
|
52
|
+
export type Member360Role = 'pengurus' | 'pengawas' | 'anggota';
|
|
53
|
+
export interface Member360AccessContext {
|
|
54
|
+
role: Member360Role;
|
|
55
|
+
actorMemberId?: string | null;
|
|
56
|
+
}
|
|
57
|
+
export interface Member360TimelineItem {
|
|
58
|
+
itemId: string;
|
|
59
|
+
occurredAt: string;
|
|
60
|
+
source: 'saving-ledger' | 'credit' | 'audit';
|
|
61
|
+
title: string;
|
|
62
|
+
detail: string;
|
|
63
|
+
}
|
|
64
|
+
export interface Member360Result {
|
|
65
|
+
member: MemberContract;
|
|
66
|
+
savings: {
|
|
67
|
+
balance: SavingBalance;
|
|
68
|
+
recentEntries: SavingLedgerContract[];
|
|
69
|
+
};
|
|
70
|
+
credit: Member360CreditProviderResult | null;
|
|
71
|
+
settlement: Member360SettlementSummary | null;
|
|
72
|
+
audit: Member360AuditItem[];
|
|
73
|
+
timeline: Member360TimelineItem[];
|
|
74
|
+
}
|
|
75
|
+
export declare function getMember360(deps: Member360Dependencies, input: GetMember360Input): Promise<Member360Result | null>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getMember360 = getMember360;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
function assertMember360Access(input) {
|
|
6
|
+
const context = input.accessContext;
|
|
7
|
+
if (!context)
|
|
8
|
+
return;
|
|
9
|
+
if (context.role !== 'anggota')
|
|
10
|
+
return;
|
|
11
|
+
if (!context.actorMemberId?.trim()) {
|
|
12
|
+
throw (0, errors_1.invalidState)('member360 access denied: anggota context requires actorMemberId');
|
|
13
|
+
}
|
|
14
|
+
if (context.actorMemberId !== input.memberId) {
|
|
15
|
+
throw (0, errors_1.invalidState)('member360 access denied: anggota can only access own memberId');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function toSavingTimeline(entries) {
|
|
19
|
+
return entries.map((entry) => ({
|
|
20
|
+
itemId: `saving-${entry.id}`,
|
|
21
|
+
occurredAt: entry.transactionDate,
|
|
22
|
+
source: 'saving-ledger',
|
|
23
|
+
title: `${entry.entryType} ${entry.savingType}`,
|
|
24
|
+
detail: `${entry.amount}`,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function toCreditTimeline(credit) {
|
|
28
|
+
if (!credit?.recentLoans?.length)
|
|
29
|
+
return [];
|
|
30
|
+
return credit.recentLoans.map((loan) => ({
|
|
31
|
+
itemId: `loan-${loan.loanId}`,
|
|
32
|
+
occurredAt: loan.startDate,
|
|
33
|
+
source: 'credit',
|
|
34
|
+
title: `loan ${loan.status}`,
|
|
35
|
+
detail: `${loan.principalAmount}`,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
function toAuditTimeline(audit) {
|
|
39
|
+
return audit.map((event) => ({
|
|
40
|
+
itemId: `audit-${event.eventId}`,
|
|
41
|
+
occurredAt: event.occurredAt,
|
|
42
|
+
source: 'audit',
|
|
43
|
+
title: event.eventType,
|
|
44
|
+
detail: event.message ?? event.eventType,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
async function getMember360(deps, input) {
|
|
48
|
+
assertMember360Access(input);
|
|
49
|
+
const member = await deps.memberService.getMemberById(input.memberId);
|
|
50
|
+
if (!member || member.deleted)
|
|
51
|
+
return null;
|
|
52
|
+
const savingQuery = {
|
|
53
|
+
...(input.savingQuery ?? {}),
|
|
54
|
+
...(input.scope?.tenantId ? { tenantId: input.scope.tenantId } : {}),
|
|
55
|
+
...(input.scope?.branchId ? { branchId: input.scope.branchId } : {}),
|
|
56
|
+
};
|
|
57
|
+
const [balance, entries, credit, audit, settlement] = await Promise.all([
|
|
58
|
+
deps.savingLedgerService.getSavingBalanceByMember(member.id),
|
|
59
|
+
deps.savingLedgerService.listSavingEntriesByMember(member.id, savingQuery),
|
|
60
|
+
deps.providers?.getCreditByMember?.(member.id, input.scope) ?? Promise.resolve(null),
|
|
61
|
+
deps.providers?.getAuditByMember?.(member.id, input.scope) ?? Promise.resolve([]),
|
|
62
|
+
deps.providers?.getSettlementSummaryByMember?.(member.id, input.scope) ?? Promise.resolve(null),
|
|
63
|
+
]);
|
|
64
|
+
const savingTimeline = toSavingTimeline(entries);
|
|
65
|
+
const creditTimeline = toCreditTimeline(credit);
|
|
66
|
+
const auditTimeline = toAuditTimeline(audit);
|
|
67
|
+
const mergedTimeline = [...savingTimeline, ...creditTimeline, ...auditTimeline].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
68
|
+
return {
|
|
69
|
+
member,
|
|
70
|
+
savings: {
|
|
71
|
+
balance,
|
|
72
|
+
recentEntries: entries,
|
|
73
|
+
},
|
|
74
|
+
credit,
|
|
75
|
+
settlement,
|
|
76
|
+
audit,
|
|
77
|
+
timeline: mergedTimeline.slice(0, Math.max(1, input.timelineLimit ?? 20)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Member360Result } from './Member360Service';
|
|
2
|
+
export interface Member360KpiCard {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
value: number | string;
|
|
6
|
+
unit?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Member360Section {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
rows: Array<{
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
value: number | string;
|
|
15
|
+
unit?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface Member360ViewModel {
|
|
19
|
+
member: {
|
|
20
|
+
id: string;
|
|
21
|
+
memberNumber: string;
|
|
22
|
+
fullName: string;
|
|
23
|
+
status: string;
|
|
24
|
+
joinDate: string;
|
|
25
|
+
exitDate?: string | null;
|
|
26
|
+
tenantId?: string | null;
|
|
27
|
+
branchId?: string | null;
|
|
28
|
+
};
|
|
29
|
+
kpiCards: Member360KpiCard[];
|
|
30
|
+
sections: Member360Section[];
|
|
31
|
+
timeline: Member360Result['timeline'];
|
|
32
|
+
}
|
|
33
|
+
export declare function toMember360ViewModel(snapshot: Member360Result): Member360ViewModel;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toMember360ViewModel = toMember360ViewModel;
|
|
4
|
+
function toMember360ViewModel(snapshot) {
|
|
5
|
+
const creditSummary = snapshot.credit?.summary;
|
|
6
|
+
const settlement = snapshot.settlement;
|
|
7
|
+
return {
|
|
8
|
+
member: {
|
|
9
|
+
id: snapshot.member.id,
|
|
10
|
+
memberNumber: snapshot.member.memberNumber,
|
|
11
|
+
fullName: snapshot.member.fullName,
|
|
12
|
+
status: snapshot.member.status,
|
|
13
|
+
joinDate: snapshot.member.joinDate,
|
|
14
|
+
exitDate: snapshot.member.exitDate ?? null,
|
|
15
|
+
tenantId: snapshot.member.tenantId ?? null,
|
|
16
|
+
branchId: snapshot.member.branchId ?? null,
|
|
17
|
+
},
|
|
18
|
+
kpiCards: [
|
|
19
|
+
{ id: 'saving-total', label: 'Total Simpanan', value: snapshot.savings.balance.total, unit: 'IDR' },
|
|
20
|
+
{ id: 'saving-pokok', label: 'Simpanan Pokok', value: snapshot.savings.balance.pokok, unit: 'IDR' },
|
|
21
|
+
{ id: 'saving-wajib', label: 'Simpanan Wajib', value: snapshot.savings.balance.wajib, unit: 'IDR' },
|
|
22
|
+
{ id: 'saving-sukarela', label: 'Simpanan Sukarela', value: snapshot.savings.balance.sukarela, unit: 'IDR' },
|
|
23
|
+
{ id: 'active-loans', label: 'Pinjaman Aktif', value: creditSummary?.activeLoans ?? 0 },
|
|
24
|
+
{
|
|
25
|
+
id: 'settlement-overdue',
|
|
26
|
+
label: 'Tunggakan Settlement',
|
|
27
|
+
value: settlement?.totalOverdue ?? 0,
|
|
28
|
+
unit: 'IDR',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
sections: [
|
|
32
|
+
{
|
|
33
|
+
id: 'credit-summary',
|
|
34
|
+
title: 'Ringkasan Kredit',
|
|
35
|
+
rows: [
|
|
36
|
+
{ key: 'totalLoans', label: 'Total Pinjaman', value: creditSummary?.totalLoans ?? 0 },
|
|
37
|
+
{ key: 'activeLoans', label: 'Pinjaman Aktif', value: creditSummary?.activeLoans ?? 0 },
|
|
38
|
+
{ key: 'closedLoans', label: 'Pinjaman Lunas', value: creditSummary?.closedLoans ?? 0 },
|
|
39
|
+
{ key: 'defaultedLoans', label: 'Pinjaman Macet', value: creditSummary?.defaultedLoans ?? 0 },
|
|
40
|
+
{
|
|
41
|
+
key: 'totalPrincipalAmount',
|
|
42
|
+
label: 'Total Pokok Pinjaman',
|
|
43
|
+
value: creditSummary?.totalPrincipalAmount ?? 0,
|
|
44
|
+
unit: 'IDR',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'settlement-summary',
|
|
50
|
+
title: 'Ringkasan Settlement',
|
|
51
|
+
rows: [
|
|
52
|
+
{ key: 'totalOutstanding', label: 'Total Outstanding', value: settlement?.totalOutstanding ?? 0, unit: 'IDR' },
|
|
53
|
+
{ key: 'totalOverdue', label: 'Total Overdue', value: settlement?.totalOverdue ?? 0, unit: 'IDR' },
|
|
54
|
+
{ key: 'settlementStatus', label: 'Status Settlement', value: settlement?.settlementStatus ?? 'clear' },
|
|
55
|
+
{ key: 'notes', label: 'Catatan', value: settlement?.notes ?? '-' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'audit-summary',
|
|
60
|
+
title: 'Ringkasan Audit',
|
|
61
|
+
rows: [{ key: 'auditCount', label: 'Jumlah Event Audit', value: snapshot.audit.length }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
timeline: snapshot.timeline,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MemberNumberPolicyServiceContract, UpsertMemberNumberPolicyInput } from '../contracts/MemberNumberPolicyContract';
|
|
2
|
+
export interface MemberNumberPolicyService extends MemberNumberPolicyServiceContract {
|
|
3
|
+
}
|
|
4
|
+
export declare function assertValidMemberNumberPolicyInput(input: UpsertMemberNumberPolicyInput): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertValidMemberNumberPolicyInput = assertValidMemberNumberPolicyInput;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
function assertValidMemberNumberPolicyInput(input) {
|
|
6
|
+
if (!input.koperasiId?.trim())
|
|
7
|
+
throw (0, errors_1.invalidState)('koperasiId is required');
|
|
8
|
+
if (!input.tenantId?.trim())
|
|
9
|
+
throw (0, errors_1.invalidState)('tenantId is required');
|
|
10
|
+
if (!input.effectiveDate?.trim())
|
|
11
|
+
throw (0, errors_1.invalidState)('effectiveDate is required');
|
|
12
|
+
if (input.paddingDigits < 1)
|
|
13
|
+
throw (0, errors_1.invalidState)('paddingDigits must be >= 1');
|
|
14
|
+
if (input.counterStart < 0)
|
|
15
|
+
throw (0, errors_1.invalidState)('counterStart must be >= 0');
|
|
16
|
+
if (input.includeMonth && !input.includeYear)
|
|
17
|
+
throw (0, errors_1.invalidState)('includeMonth requires includeYear');
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertValidCreateMemberInput = assertValidCreateMemberInput;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
function assertValidCreateMemberInput(input) {
|
|
6
|
+
if (!input.memberNumber?.trim()) {
|
|
7
|
+
throw (0, errors_1.invalidState)('memberNumber is required');
|
|
8
|
+
}
|
|
9
|
+
if (!input.fullName?.trim()) {
|
|
10
|
+
throw (0, errors_1.invalidState)('fullName is required');
|
|
11
|
+
}
|
|
12
|
+
if (!input.joinDate?.trim()) {
|
|
13
|
+
throw (0, errors_1.invalidState)('joinDate is required');
|
|
14
|
+
}
|
|
15
|
+
if (!input.tenantId?.trim()) {
|
|
16
|
+
throw (0, errors_1.invalidState)('tenantId is required');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { MemberServiceContract } from '../contracts/MemberContract';
|
|
2
|
+
import type { SavingLedgerServiceContract } from '../contracts/SavingLedgerContract';
|
|
3
|
+
export interface ReportScopeRef {
|
|
4
|
+
tenantId?: string | null;
|
|
5
|
+
branchId?: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface ReportPeriod {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CreditReportSummary {
|
|
12
|
+
loanCount: number;
|
|
13
|
+
activeLoanCount: number;
|
|
14
|
+
closedLoanCount: number;
|
|
15
|
+
defaultedLoanCount: number;
|
|
16
|
+
outstandingPrincipalAmount: number;
|
|
17
|
+
totalOverdueAmount: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ShuReportSummary {
|
|
20
|
+
allocationCount: number;
|
|
21
|
+
totalAllocatedAmount: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ReportSummaryProviders {
|
|
24
|
+
getCreditSummary?: (period: ReportPeriod, scope?: ReportScopeRef) => Promise<CreditReportSummary>;
|
|
25
|
+
getShuSummary?: (period: ReportPeriod, scope?: ReportScopeRef) => Promise<ShuReportSummary>;
|
|
26
|
+
}
|
|
27
|
+
export interface ReportSummaryDependencies {
|
|
28
|
+
memberService: MemberServiceContract;
|
|
29
|
+
savingLedgerService: SavingLedgerServiceContract;
|
|
30
|
+
providers?: ReportSummaryProviders;
|
|
31
|
+
}
|
|
32
|
+
export interface GetReportSummaryInput {
|
|
33
|
+
period: ReportPeriod;
|
|
34
|
+
scope?: ReportScopeRef;
|
|
35
|
+
}
|
|
36
|
+
export interface ReportSummaryResult {
|
|
37
|
+
period: ReportPeriod;
|
|
38
|
+
scope: ReportScopeRef;
|
|
39
|
+
members: {
|
|
40
|
+
total: number;
|
|
41
|
+
active: number;
|
|
42
|
+
inactive: number;
|
|
43
|
+
exited: number;
|
|
44
|
+
};
|
|
45
|
+
savings: {
|
|
46
|
+
creditTotal: number;
|
|
47
|
+
debitTotal: number;
|
|
48
|
+
netTotal: number;
|
|
49
|
+
byType: {
|
|
50
|
+
pokok: number;
|
|
51
|
+
wajib: number;
|
|
52
|
+
sukarela: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
credit: CreditReportSummary;
|
|
56
|
+
shu: ShuReportSummary;
|
|
57
|
+
crossDomain: {
|
|
58
|
+
memberActivationRate: number;
|
|
59
|
+
creditPenetrationRate: number;
|
|
60
|
+
overdueToOutstandingRatio: number;
|
|
61
|
+
savingsPerActiveMember: number;
|
|
62
|
+
shuPerActiveMember: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export interface ReportSummaryJsonExport {
|
|
66
|
+
schemaVersion: '1';
|
|
67
|
+
generatedAt: string;
|
|
68
|
+
summary: ReportSummaryResult;
|
|
69
|
+
}
|
|
70
|
+
export declare function getReportSummary(deps: ReportSummaryDependencies, input: GetReportSummaryInput): Promise<ReportSummaryResult>;
|
|
71
|
+
export declare function exportReportSummaryCsv(deps: ReportSummaryDependencies, input: GetReportSummaryInput): Promise<{
|
|
72
|
+
filename: string;
|
|
73
|
+
contentType: 'text/csv';
|
|
74
|
+
body: string;
|
|
75
|
+
}>;
|
|
76
|
+
export declare function exportReportSummaryJson(deps: ReportSummaryDependencies, input: GetReportSummaryInput): Promise<{
|
|
77
|
+
filename: string;
|
|
78
|
+
contentType: 'application/json';
|
|
79
|
+
body: string;
|
|
80
|
+
data: ReportSummaryJsonExport;
|
|
81
|
+
}>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getReportSummary = getReportSummary;
|
|
4
|
+
exports.exportReportSummaryCsv = exportReportSummaryCsv;
|
|
5
|
+
exports.exportReportSummaryJson = exportReportSummaryJson;
|
|
6
|
+
const EMPTY_CREDIT_SUMMARY = {
|
|
7
|
+
loanCount: 0,
|
|
8
|
+
activeLoanCount: 0,
|
|
9
|
+
closedLoanCount: 0,
|
|
10
|
+
defaultedLoanCount: 0,
|
|
11
|
+
outstandingPrincipalAmount: 0,
|
|
12
|
+
totalOverdueAmount: 0,
|
|
13
|
+
};
|
|
14
|
+
const EMPTY_SHU_SUMMARY = {
|
|
15
|
+
allocationCount: 0,
|
|
16
|
+
totalAllocatedAmount: 0,
|
|
17
|
+
};
|
|
18
|
+
function toIsoBoundary(period) {
|
|
19
|
+
const fromDate = period.from.slice(0, 10);
|
|
20
|
+
const toDate = period.to.slice(0, 10);
|
|
21
|
+
return {
|
|
22
|
+
fromIso: `${fromDate}T00:00:00.000Z`,
|
|
23
|
+
toIso: `${toDate}T23:59:59.999Z`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function aggregateSavings(entries) {
|
|
27
|
+
let creditTotal = 0;
|
|
28
|
+
let debitTotal = 0;
|
|
29
|
+
let pokok = 0;
|
|
30
|
+
let wajib = 0;
|
|
31
|
+
let sukarela = 0;
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const sign = entry.entryType === 'credit' ? 1 : -1;
|
|
34
|
+
if (entry.entryType === 'credit') {
|
|
35
|
+
creditTotal += entry.amount;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
debitTotal += entry.amount;
|
|
39
|
+
}
|
|
40
|
+
if (entry.savingType === 'pokok')
|
|
41
|
+
pokok += sign * entry.amount;
|
|
42
|
+
if (entry.savingType === 'wajib')
|
|
43
|
+
wajib += sign * entry.amount;
|
|
44
|
+
if (entry.savingType === 'sukarela')
|
|
45
|
+
sukarela += sign * entry.amount;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
creditTotal,
|
|
49
|
+
debitTotal,
|
|
50
|
+
netTotal: creditTotal - debitTotal,
|
|
51
|
+
byType: {
|
|
52
|
+
pokok,
|
|
53
|
+
wajib,
|
|
54
|
+
sukarela,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function toCsvRows(summary) {
|
|
59
|
+
return [
|
|
60
|
+
'section,key,value',
|
|
61
|
+
`period,from,${summary.period.from}`,
|
|
62
|
+
`period,to,${summary.period.to}`,
|
|
63
|
+
`members,total,${summary.members.total}`,
|
|
64
|
+
`members,active,${summary.members.active}`,
|
|
65
|
+
`members,inactive,${summary.members.inactive}`,
|
|
66
|
+
`members,exited,${summary.members.exited}`,
|
|
67
|
+
`savings,creditTotal,${summary.savings.creditTotal}`,
|
|
68
|
+
`savings,debitTotal,${summary.savings.debitTotal}`,
|
|
69
|
+
`savings,netTotal,${summary.savings.netTotal}`,
|
|
70
|
+
`savings,pokokNet,${summary.savings.byType.pokok}`,
|
|
71
|
+
`savings,wajibNet,${summary.savings.byType.wajib}`,
|
|
72
|
+
`savings,sukarelaNet,${summary.savings.byType.sukarela}`,
|
|
73
|
+
`credit,loanCount,${summary.credit.loanCount}`,
|
|
74
|
+
`credit,activeLoanCount,${summary.credit.activeLoanCount}`,
|
|
75
|
+
`credit,closedLoanCount,${summary.credit.closedLoanCount}`,
|
|
76
|
+
`credit,defaultedLoanCount,${summary.credit.defaultedLoanCount}`,
|
|
77
|
+
`credit,outstandingPrincipalAmount,${summary.credit.outstandingPrincipalAmount}`,
|
|
78
|
+
`credit,totalOverdueAmount,${summary.credit.totalOverdueAmount}`,
|
|
79
|
+
`shu,allocationCount,${summary.shu.allocationCount}`,
|
|
80
|
+
`shu,totalAllocatedAmount,${summary.shu.totalAllocatedAmount}`,
|
|
81
|
+
`crossDomain,memberActivationRate,${summary.crossDomain.memberActivationRate}`,
|
|
82
|
+
`crossDomain,creditPenetrationRate,${summary.crossDomain.creditPenetrationRate}`,
|
|
83
|
+
`crossDomain,overdueToOutstandingRatio,${summary.crossDomain.overdueToOutstandingRatio}`,
|
|
84
|
+
`crossDomain,savingsPerActiveMember,${summary.crossDomain.savingsPerActiveMember}`,
|
|
85
|
+
`crossDomain,shuPerActiveMember,${summary.crossDomain.shuPerActiveMember}`,
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
function safeRatio(numerator, denominator) {
|
|
89
|
+
if (denominator <= 0)
|
|
90
|
+
return 0;
|
|
91
|
+
return Number((numerator / denominator).toFixed(4));
|
|
92
|
+
}
|
|
93
|
+
async function getReportSummary(deps, input) {
|
|
94
|
+
const scope = input.scope ?? {};
|
|
95
|
+
const { fromIso, toIso } = toIsoBoundary(input.period);
|
|
96
|
+
const members = await deps.memberService.listMembers({
|
|
97
|
+
includeDeleted: false,
|
|
98
|
+
...(scope.tenantId ? { tenantId: scope.tenantId } : {}),
|
|
99
|
+
...(scope.branchId ? { branchId: scope.branchId } : {}),
|
|
100
|
+
});
|
|
101
|
+
const savingEntriesByMember = await Promise.all(members.map((member) => deps.savingLedgerService.listSavingEntriesByMember(member.id, {
|
|
102
|
+
includeDeleted: false,
|
|
103
|
+
dateFrom: fromIso,
|
|
104
|
+
dateTo: toIso,
|
|
105
|
+
...(scope.tenantId ? { tenantId: scope.tenantId } : {}),
|
|
106
|
+
...(scope.branchId ? { branchId: scope.branchId } : {}),
|
|
107
|
+
})));
|
|
108
|
+
const savingEntries = savingEntriesByMember.flat();
|
|
109
|
+
const savings = aggregateSavings(savingEntries);
|
|
110
|
+
const credit = (await deps.providers?.getCreditSummary?.(input.period, scope)) ??
|
|
111
|
+
EMPTY_CREDIT_SUMMARY;
|
|
112
|
+
const shu = (await deps.providers?.getShuSummary?.(input.period, scope)) ??
|
|
113
|
+
EMPTY_SHU_SUMMARY;
|
|
114
|
+
const activeMembers = members.filter((m) => m.status === 'active').length;
|
|
115
|
+
const totalMembers = members.length;
|
|
116
|
+
const crossDomain = {
|
|
117
|
+
memberActivationRate: safeRatio(activeMembers, totalMembers),
|
|
118
|
+
creditPenetrationRate: safeRatio(credit.activeLoanCount, activeMembers),
|
|
119
|
+
overdueToOutstandingRatio: safeRatio(credit.totalOverdueAmount, credit.outstandingPrincipalAmount),
|
|
120
|
+
savingsPerActiveMember: activeMembers > 0 ? Math.round(savings.netTotal / activeMembers) : 0,
|
|
121
|
+
shuPerActiveMember: activeMembers > 0 ? Math.round(shu.totalAllocatedAmount / activeMembers) : 0,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
period: input.period,
|
|
125
|
+
scope,
|
|
126
|
+
members: {
|
|
127
|
+
total: totalMembers,
|
|
128
|
+
active: activeMembers,
|
|
129
|
+
inactive: members.filter((m) => m.status === 'inactive').length,
|
|
130
|
+
exited: members.filter((m) => m.status === 'exited').length,
|
|
131
|
+
},
|
|
132
|
+
savings,
|
|
133
|
+
credit,
|
|
134
|
+
shu,
|
|
135
|
+
crossDomain,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async function exportReportSummaryCsv(deps, input) {
|
|
139
|
+
const summary = await getReportSummary(deps, input);
|
|
140
|
+
const body = toCsvRows(summary).join('\n');
|
|
141
|
+
return {
|
|
142
|
+
filename: `report-summary-${summary.period.from.slice(0, 10)}-${summary.period.to.slice(0, 10)}.csv`,
|
|
143
|
+
contentType: 'text/csv',
|
|
144
|
+
body,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function exportReportSummaryJson(deps, input) {
|
|
148
|
+
const summary = await getReportSummary(deps, input);
|
|
149
|
+
const data = {
|
|
150
|
+
schemaVersion: '1',
|
|
151
|
+
generatedAt: new Date().toISOString(),
|
|
152
|
+
summary,
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
filename: `report-summary-${summary.period.from.slice(0, 10)}-${summary.period.to.slice(0, 10)}.json`,
|
|
156
|
+
contentType: 'application/json',
|
|
157
|
+
body: JSON.stringify(data),
|
|
158
|
+
data,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReportSummaryResult } from './ReportSummaryService';
|
|
2
|
+
export interface ReportKpiCard {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
value: number | string;
|
|
6
|
+
unit?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ReportTableSection {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
rows: Array<{
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
value: number | string;
|
|
15
|
+
unit?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface ReportSummaryViewModel {
|
|
19
|
+
period: ReportSummaryResult['period'];
|
|
20
|
+
scope: ReportSummaryResult['scope'];
|
|
21
|
+
kpiCards: ReportKpiCard[];
|
|
22
|
+
tableSections: ReportTableSection[];
|
|
23
|
+
}
|
|
24
|
+
export declare function toReportSummaryViewModel(summary: ReportSummaryResult): ReportSummaryViewModel;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toReportSummaryViewModel = toReportSummaryViewModel;
|
|
4
|
+
function toReportSummaryViewModel(summary) {
|
|
5
|
+
return {
|
|
6
|
+
period: summary.period,
|
|
7
|
+
scope: summary.scope,
|
|
8
|
+
kpiCards: [
|
|
9
|
+
{ id: 'members-total', label: 'Total Anggota', value: summary.members.total },
|
|
10
|
+
{ id: 'members-active', label: 'Anggota Aktif', value: summary.members.active },
|
|
11
|
+
{ id: 'savings-net', label: 'Net Simpanan', value: summary.savings.netTotal, unit: 'IDR' },
|
|
12
|
+
{ id: 'credit-overdue', label: 'Total Tunggakan', value: summary.credit.totalOverdueAmount, unit: 'IDR' },
|
|
13
|
+
{ id: 'shu-total', label: 'Total SHU Alokasi', value: summary.shu.totalAllocatedAmount, unit: 'IDR' },
|
|
14
|
+
],
|
|
15
|
+
tableSections: [
|
|
16
|
+
{
|
|
17
|
+
id: 'members',
|
|
18
|
+
title: 'Ringkasan Anggota',
|
|
19
|
+
rows: [
|
|
20
|
+
{ key: 'total', label: 'Total', value: summary.members.total },
|
|
21
|
+
{ key: 'active', label: 'Aktif', value: summary.members.active },
|
|
22
|
+
{ key: 'inactive', label: 'Non-Aktif', value: summary.members.inactive },
|
|
23
|
+
{ key: 'exited', label: 'Keluar', value: summary.members.exited },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'savings',
|
|
28
|
+
title: 'Ringkasan Simpanan',
|
|
29
|
+
rows: [
|
|
30
|
+
{ key: 'creditTotal', label: 'Total Masuk', value: summary.savings.creditTotal, unit: 'IDR' },
|
|
31
|
+
{ key: 'debitTotal', label: 'Total Keluar', value: summary.savings.debitTotal, unit: 'IDR' },
|
|
32
|
+
{ key: 'netTotal', label: 'Total Net', value: summary.savings.netTotal, unit: 'IDR' },
|
|
33
|
+
{ key: 'pokokNet', label: 'Net Pokok', value: summary.savings.byType.pokok, unit: 'IDR' },
|
|
34
|
+
{ key: 'wajibNet', label: 'Net Wajib', value: summary.savings.byType.wajib, unit: 'IDR' },
|
|
35
|
+
{ key: 'sukarelaNet', label: 'Net Sukarela', value: summary.savings.byType.sukarela, unit: 'IDR' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'credit',
|
|
40
|
+
title: 'Ringkasan Kredit',
|
|
41
|
+
rows: [
|
|
42
|
+
{ key: 'loanCount', label: 'Total Pinjaman', value: summary.credit.loanCount },
|
|
43
|
+
{ key: 'activeLoanCount', label: 'Pinjaman Aktif', value: summary.credit.activeLoanCount },
|
|
44
|
+
{ key: 'closedLoanCount', label: 'Pinjaman Lunas', value: summary.credit.closedLoanCount },
|
|
45
|
+
{ key: 'defaultedLoanCount', label: 'Pinjaman Macet', value: summary.credit.defaultedLoanCount },
|
|
46
|
+
{
|
|
47
|
+
key: 'outstandingPrincipalAmount',
|
|
48
|
+
label: 'Outstanding Pokok',
|
|
49
|
+
value: summary.credit.outstandingPrincipalAmount,
|
|
50
|
+
unit: 'IDR',
|
|
51
|
+
},
|
|
52
|
+
{ key: 'totalOverdueAmount', label: 'Total Tunggakan', value: summary.credit.totalOverdueAmount, unit: 'IDR' },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'shu',
|
|
57
|
+
title: 'Ringkasan SHU',
|
|
58
|
+
rows: [
|
|
59
|
+
{ key: 'allocationCount', label: 'Jumlah Alokasi', value: summary.shu.allocationCount },
|
|
60
|
+
{ key: 'totalAllocatedAmount', label: 'Total Alokasi', value: summary.shu.totalAllocatedAmount, unit: 'IDR' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'cross-domain',
|
|
65
|
+
title: 'Metrik Lintas Domain',
|
|
66
|
+
rows: [
|
|
67
|
+
{ key: 'memberActivationRate', label: 'Activation Rate Anggota', value: summary.crossDomain.memberActivationRate },
|
|
68
|
+
{ key: 'creditPenetrationRate', label: 'Credit Penetration Rate', value: summary.crossDomain.creditPenetrationRate },
|
|
69
|
+
{
|
|
70
|
+
key: 'overdueToOutstandingRatio',
|
|
71
|
+
label: 'Overdue/Outstanding Ratio',
|
|
72
|
+
value: summary.crossDomain.overdueToOutstandingRatio,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
key: 'savingsPerActiveMember',
|
|
76
|
+
label: 'Simpanan per Anggota Aktif',
|
|
77
|
+
value: summary.crossDomain.savingsPerActiveMember,
|
|
78
|
+
unit: 'IDR',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'shuPerActiveMember',
|
|
82
|
+
label: 'SHU per Anggota Aktif',
|
|
83
|
+
value: summary.crossDomain.shuPerActiveMember,
|
|
84
|
+
unit: 'IDR',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SavingComplianceSnapshotServiceContract, SavingComplianceStatus } from '../contracts/SavingComplianceSnapshotContract';
|
|
2
|
+
export interface SavingComplianceSnapshotService extends SavingComplianceSnapshotServiceContract {
|
|
3
|
+
}
|
|
4
|
+
export declare function deriveSavingComplianceStatus(monthlyWajibNominalIdr: number, wajibNetTotalIdr: number, hasPolicy: boolean): SavingComplianceStatus;
|