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.
Files changed (98) hide show
  1. package/README.md +18 -0
  2. package/dist/OfcoopCore.d.ts +60 -0
  3. package/dist/OfcoopCore.js +273 -0
  4. package/dist/contracts/MemberContract.d.ts +52 -0
  5. package/dist/contracts/MemberContract.js +2 -0
  6. package/dist/contracts/MemberNumberPolicyContract.d.ts +44 -0
  7. package/dist/contracts/MemberNumberPolicyContract.js +2 -0
  8. package/dist/contracts/SavingComplianceSnapshotContract.d.ts +34 -0
  9. package/dist/contracts/SavingComplianceSnapshotContract.js +2 -0
  10. package/dist/contracts/SavingLedgerContract.d.ts +49 -0
  11. package/dist/contracts/SavingLedgerContract.js +2 -0
  12. package/dist/contracts/SavingPolicyContract.d.ts +63 -0
  13. package/dist/contracts/SavingPolicyContract.js +2 -0
  14. package/dist/contracts/ShuConfigContract.d.ts +39 -0
  15. package/dist/contracts/ShuConfigContract.js +2 -0
  16. package/dist/contracts/ShuContract.d.ts +58 -0
  17. package/dist/contracts/ShuContract.js +2 -0
  18. package/dist/contracts/crossDomainPrimitives.d.ts +5 -0
  19. package/dist/contracts/crossDomainPrimitives.js +2 -0
  20. package/dist/data/applyPendingMigrations.d.ts +2 -0
  21. package/dist/data/applyPendingMigrations.js +30 -0
  22. package/dist/data/migrations.d.ts +2 -0
  23. package/dist/data/migrations.js +20 -0
  24. package/dist/data/repositories.d.ts +114 -0
  25. package/dist/data/repositories.js +324 -0
  26. package/dist/data/schemas.d.ts +14 -0
  27. package/dist/data/schemas.js +179 -0
  28. package/dist/index.d.ts +38 -0
  29. package/dist/index.js +54 -0
  30. package/dist/services/AuditTrailQueryService.d.ts +35 -0
  31. package/dist/services/AuditTrailQueryService.js +62 -0
  32. package/dist/services/CoopOrchestrationService.d.ts +35 -0
  33. package/dist/services/CoopOrchestrationService.js +71 -0
  34. package/dist/services/DailyOpsService.d.ts +2 -0
  35. package/dist/services/DailyOpsService.js +7 -0
  36. package/dist/services/DashboardSummaryService.d.ts +180 -0
  37. package/dist/services/DashboardSummaryService.js +211 -0
  38. package/dist/services/DashboardViewModelService.d.ts +42 -0
  39. package/dist/services/DashboardViewModelService.js +193 -0
  40. package/dist/services/Member360Service.d.ts +75 -0
  41. package/dist/services/Member360Service.js +79 -0
  42. package/dist/services/Member360ViewModelService.d.ts +33 -0
  43. package/dist/services/Member360ViewModelService.js +66 -0
  44. package/dist/services/MemberNumberPolicyService.d.ts +4 -0
  45. package/dist/services/MemberNumberPolicyService.js +18 -0
  46. package/dist/services/MemberService.d.ts +4 -0
  47. package/dist/services/MemberService.js +18 -0
  48. package/dist/services/ReportSummaryService.d.ts +81 -0
  49. package/dist/services/ReportSummaryService.js +160 -0
  50. package/dist/services/ReportViewModelService.d.ts +24 -0
  51. package/dist/services/ReportViewModelService.js +90 -0
  52. package/dist/services/SavingComplianceSnapshotService.d.ts +4 -0
  53. package/dist/services/SavingComplianceSnapshotService.js +10 -0
  54. package/dist/services/SavingLedgerService.d.ts +4 -0
  55. package/dist/services/SavingLedgerService.js +13 -0
  56. package/dist/services/SavingPolicyService.d.ts +5 -0
  57. package/dist/services/SavingPolicyService.js +20 -0
  58. package/dist/services/ShuConfigService.d.ts +4 -0
  59. package/dist/services/ShuConfigService.js +16 -0
  60. package/dist/services/ShuService.d.ts +4 -0
  61. package/dist/services/ShuService.js +14 -0
  62. package/dist/services/createActivityAuditTrailProvider.d.ts +22 -0
  63. package/dist/services/createActivityAuditTrailProvider.js +73 -0
  64. package/dist/services/createAuditTrailCompositionProvider.d.ts +13 -0
  65. package/dist/services/createAuditTrailCompositionProvider.js +21 -0
  66. package/dist/services/createDbAdapterOfcoopServices.d.ts +20 -0
  67. package/dist/services/createDbAdapterOfcoopServices.js +23 -0
  68. package/dist/services/createMember360CompositionProviders.d.ts +6 -0
  69. package/dist/services/createMember360CompositionProviders.js +43 -0
  70. package/dist/services/createOfauthCompositionProviders.d.ts +39 -0
  71. package/dist/services/createOfauthCompositionProviders.js +80 -0
  72. package/dist/services/createOfcoopCreditCompositionProviders.d.ts +59 -0
  73. package/dist/services/createOfcoopCreditCompositionProviders.js +233 -0
  74. package/dist/services/createOfcoopDashboardCompositionProviders.d.ts +18 -0
  75. package/dist/services/createOfcoopDashboardCompositionProviders.js +228 -0
  76. package/dist/services/createOfcoopDomainAuditCompositionProviders.d.ts +36 -0
  77. package/dist/services/createOfcoopDomainAuditCompositionProviders.js +117 -0
  78. package/dist/services/createOfcoopReportCompositionProviders.d.ts +10 -0
  79. package/dist/services/createOfcoopReportCompositionProviders.js +30 -0
  80. package/dist/services/errors.d.ts +8 -0
  81. package/dist/services/errors.js +23 -0
  82. package/dist/services/impl/DbAdapterMemberNumberPolicyService.d.ts +9 -0
  83. package/dist/services/impl/DbAdapterMemberNumberPolicyService.js +37 -0
  84. package/dist/services/impl/DbAdapterMemberService.d.ts +21 -0
  85. package/dist/services/impl/DbAdapterMemberService.js +142 -0
  86. package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.d.ts +9 -0
  87. package/dist/services/impl/DbAdapterSavingComplianceSnapshotService.js +74 -0
  88. package/dist/services/impl/DbAdapterSavingLedgerService.d.ts +18 -0
  89. package/dist/services/impl/DbAdapterSavingLedgerService.js +115 -0
  90. package/dist/services/impl/DbAdapterSavingPolicyService.d.ts +14 -0
  91. package/dist/services/impl/DbAdapterSavingPolicyService.js +139 -0
  92. package/dist/services/impl/DbAdapterShuConfigService.d.ts +12 -0
  93. package/dist/services/impl/DbAdapterShuConfigService.js +62 -0
  94. package/dist/services/impl/DbAdapterShuService.d.ts +26 -0
  95. package/dist/services/impl/DbAdapterShuService.js +376 -0
  96. package/dist/services/impl/runtimeSupport.d.ts +18 -0
  97. package/dist/services/impl/runtimeSupport.js +29 -0
  98. package/package.json +41 -0
@@ -0,0 +1,233 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOfcoopCreditCompositionProviders = createOfcoopCreditCompositionProviders;
4
+ function dateOnly(iso) {
5
+ return iso.slice(0, 10);
6
+ }
7
+ function dateShiftIso(baseDateIso, diffDays) {
8
+ const d = new Date(`${baseDateIso}T00:00:00.000Z`);
9
+ d.setUTCDate(d.getUTCDate() + diffDays);
10
+ return d.toISOString().slice(0, 10);
11
+ }
12
+ function toLoanStatusComposition(board) {
13
+ const out = { aktif: 0, jatuhTempo: 0, macet: 0, lunas: 0 };
14
+ for (const item of board) {
15
+ if (item.status === 'defaulted') {
16
+ out.macet += 1;
17
+ continue;
18
+ }
19
+ if (item.status === 'closed') {
20
+ out.lunas += 1;
21
+ continue;
22
+ }
23
+ if (item.overdueInstallmentCount > 0) {
24
+ out.jatuhTempo += 1;
25
+ }
26
+ else {
27
+ out.aktif += 1;
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ function toLoanAgingBuckets(overdueItems, todayDate) {
33
+ const bucket = { d0to7: 0, d8to30: 0, d31to60: 0, dOver60: 0 };
34
+ const todayMs = Date.parse(`${todayDate}T00:00:00.000Z`);
35
+ for (const item of overdueItems) {
36
+ const dueMs = Date.parse(`${dateOnly(item.dueDate)}T00:00:00.000Z`);
37
+ const ageDays = Math.max(0, Math.floor((todayMs - dueMs) / (24 * 60 * 60 * 1000)));
38
+ if (ageDays <= 7)
39
+ bucket.d0to7 += 1;
40
+ else if (ageDays <= 30)
41
+ bucket.d8to30 += 1;
42
+ else if (ageDays <= 60)
43
+ bucket.d31to60 += 1;
44
+ else
45
+ bucket.dOver60 += 1;
46
+ }
47
+ return bucket;
48
+ }
49
+ function requireTenant(scope) {
50
+ if (!scope?.tenantId)
51
+ return null;
52
+ return {
53
+ tenantId: scope.tenantId,
54
+ ...(scope.branchId ? { branchId: scope.branchId } : {}),
55
+ };
56
+ }
57
+ function createOfcoopCreditCompositionProviders(credit) {
58
+ const member360 = {
59
+ async getCreditByMember(memberId, scope) {
60
+ const loans = await credit.loanService.listLoansByMember(memberId, {
61
+ includeDeleted: false,
62
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
63
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
64
+ });
65
+ const summary = {
66
+ totalLoans: loans.length,
67
+ activeLoans: loans.filter((l) => l.status === 'active').length,
68
+ closedLoans: loans.filter((l) => l.status === 'closed').length,
69
+ defaultedLoans: loans.filter((l) => l.status === 'defaulted').length,
70
+ totalPrincipalAmount: loans.reduce((sum, l) => sum + l.principalAmount, 0),
71
+ };
72
+ const recentLoans = [...loans]
73
+ .sort((a, b) => b.startDate.localeCompare(a.startDate))
74
+ .slice(0, 5)
75
+ .map((loan) => ({
76
+ loanId: loan.id,
77
+ status: loan.status,
78
+ principalAmount: loan.principalAmount,
79
+ startDate: loan.startDate,
80
+ }));
81
+ return { summary, recentLoans };
82
+ },
83
+ async getSettlementSummaryByMember(memberId, scope) {
84
+ const scoped = requireTenant(scope);
85
+ if (!scoped) {
86
+ return {
87
+ totalOutstanding: 0,
88
+ totalOverdue: 0,
89
+ settlementStatus: 'clear',
90
+ notes: 'tenantId missing in scope',
91
+ };
92
+ }
93
+ const board = await credit.loanBoardService.listLoanBoard(scoped, { memberId });
94
+ const totalOutstanding = board.reduce((sum, item) => sum + item.outstandingAmount, 0);
95
+ const totalOverdue = board
96
+ .filter((item) => item.overdueInstallmentCount > 0)
97
+ .reduce((sum, item) => sum + item.outstandingAmount, 0);
98
+ return {
99
+ totalOutstanding,
100
+ totalOverdue,
101
+ settlementStatus: totalOverdue > 0 ? 'overdue' : totalOutstanding > 0 ? 'pending' : 'clear',
102
+ };
103
+ },
104
+ };
105
+ const dashboard = {
106
+ async getLoanStatusComposition(scope) {
107
+ const scoped = requireTenant(scope);
108
+ if (!scoped)
109
+ return { aktif: 0, jatuhTempo: 0, macet: 0, lunas: 0 };
110
+ const board = await credit.loanBoardService.listLoanBoard(scoped);
111
+ return toLoanStatusComposition(board);
112
+ },
113
+ async getLoanAgingBuckets(scope) {
114
+ const scoped = requireTenant(scope);
115
+ if (!scoped)
116
+ return { d0to7: 0, d8to30: 0, d31to60: 0, dOver60: 0 };
117
+ const todayDate = dateOnly(new Date().toISOString());
118
+ const sixtyDaysAgo = dateShiftIso(todayDate, -60);
119
+ const overdue = await credit.loanDueWindowService.listLoansDueInWindow(scoped, {
120
+ dateFrom: `1970-01-01T00:00:00.000Z`,
121
+ dateTo: `${todayDate}T23:59:59.999Z`,
122
+ });
123
+ const recentOverdue = overdue.filter((item) => item.installmentStatus === 'overdue' && dateOnly(item.dueDate) >= sixtyDaysAgo);
124
+ const olderOverdue = overdue.filter((item) => item.installmentStatus === 'overdue' && dateOnly(item.dueDate) < sixtyDaysAgo);
125
+ const bucket = toLoanAgingBuckets(recentOverdue, todayDate);
126
+ bucket.dOver60 += olderOverdue.length;
127
+ return bucket;
128
+ },
129
+ async getTotalOverdueAmount(scope) {
130
+ const scoped = requireTenant(scope);
131
+ if (!scoped)
132
+ return 0;
133
+ const overdue = await credit.loanDueWindowService.listLoansDueInWindow(scoped, {
134
+ dateFrom: `1970-01-01T00:00:00.000Z`,
135
+ dateTo: `${new Date().toISOString().slice(0, 10)}T23:59:59.999Z`,
136
+ });
137
+ return overdue
138
+ .filter((item) => item.installmentStatus === 'overdue')
139
+ .reduce((sum, item) => sum + item.outstandingAmount, 0);
140
+ },
141
+ async getAnggotaCreditSummary(memberId, scope) {
142
+ const scoped = requireTenant(scope);
143
+ if (!scoped) {
144
+ return {
145
+ activeLoans: 0,
146
+ outstandingPrincipalAmount: 0,
147
+ totalOverdueAmount: 0,
148
+ nextDueDate: null,
149
+ };
150
+ }
151
+ const board = await credit.loanBoardService.listLoanBoard(scoped, { memberId });
152
+ const activeLoans = board.filter((item) => item.status === 'active').length;
153
+ const outstandingPrincipalAmount = board.reduce((sum, item) => sum + item.outstandingAmount, 0);
154
+ const totalOverdueAmount = board
155
+ .filter((item) => item.overdueInstallmentCount > 0)
156
+ .reduce((sum, item) => sum + item.outstandingAmount, 0);
157
+ const nextDueDate = [...board]
158
+ .map((item) => item.nextDueDate)
159
+ .filter(Boolean)
160
+ .sort((a, b) => String(a).localeCompare(String(b)))[0] ?? null;
161
+ return {
162
+ activeLoans,
163
+ outstandingPrincipalAmount,
164
+ totalOverdueAmount,
165
+ nextDueDate,
166
+ };
167
+ },
168
+ async getLoanRiskMap(scope) {
169
+ const scoped = requireTenant(scope);
170
+ if (!scoped)
171
+ return [];
172
+ const todayDate = dateOnly(new Date().toISOString());
173
+ const board = await credit.loanBoardService.listLoanBoard(scoped);
174
+ const overdue = await credit.loanDueWindowService.listLoansDueInWindow(scoped, {
175
+ dateFrom: `1970-01-01T00:00:00.000Z`,
176
+ dateTo: `${todayDate}T23:59:59.999Z`,
177
+ });
178
+ const overdueAgeByLoan = new Map();
179
+ const todayMs = Date.parse(`${todayDate}T00:00:00.000Z`);
180
+ for (const item of overdue) {
181
+ if (item.installmentStatus !== 'overdue')
182
+ continue;
183
+ const dueMs = Date.parse(`${dateOnly(item.dueDate)}T00:00:00.000Z`);
184
+ const ageDays = Math.max(0, Math.floor((todayMs - dueMs) / (24 * 60 * 60 * 1000)));
185
+ const prev = overdueAgeByLoan.get(item.loanId) ?? 0;
186
+ if (ageDays > prev)
187
+ overdueAgeByLoan.set(item.loanId, ageDays);
188
+ }
189
+ return board.map((item) => {
190
+ const overdueAgeDays = overdueAgeByLoan.get(item.loanId) ?? 0;
191
+ const riskLevel = overdueAgeDays > 60 || item.outstandingAmount >= 5000000
192
+ ? 'high'
193
+ : overdueAgeDays > 30 || item.outstandingAmount >= 1000000
194
+ ? 'medium'
195
+ : 'low';
196
+ return {
197
+ loanId: item.loanId,
198
+ memberId: item.memberId,
199
+ outstandingAmount: item.outstandingAmount,
200
+ overdueAgeDays,
201
+ riskLevel,
202
+ };
203
+ });
204
+ },
205
+ };
206
+ const report = {
207
+ async getCreditSummary(_period, scope) {
208
+ const scoped = requireTenant(scope);
209
+ if (!scoped) {
210
+ return {
211
+ loanCount: 0,
212
+ activeLoanCount: 0,
213
+ closedLoanCount: 0,
214
+ defaultedLoanCount: 0,
215
+ outstandingPrincipalAmount: 0,
216
+ totalOverdueAmount: 0,
217
+ };
218
+ }
219
+ const board = await credit.loanBoardService.listLoanBoard(scoped);
220
+ return {
221
+ loanCount: board.length,
222
+ activeLoanCount: board.filter((item) => item.status === 'active').length,
223
+ closedLoanCount: board.filter((item) => item.status === 'closed').length,
224
+ defaultedLoanCount: board.filter((item) => item.status === 'defaulted').length,
225
+ outstandingPrincipalAmount: board.reduce((sum, item) => sum + item.outstandingAmount, 0),
226
+ totalOverdueAmount: board
227
+ .filter((item) => item.overdueInstallmentCount > 0)
228
+ .reduce((sum, item) => sum + item.outstandingAmount, 0),
229
+ };
230
+ },
231
+ };
232
+ return { member360, dashboard, report };
233
+ }
@@ -0,0 +1,18 @@
1
+ import type { MemberServiceContract } from '../contracts/MemberContract';
2
+ import type { SavingLedgerServiceContract } from '../contracts/SavingLedgerContract';
3
+ import type { ShuPeriodContract, ShuServiceContract } from '../contracts/ShuContract';
4
+ import type { DashboardScopeRef, DashboardSummaryProviders } from './DashboardSummaryService';
5
+ interface OfcoopDashboardCompositionServicesRef {
6
+ memberService: Pick<MemberServiceContract, 'listMembers'>;
7
+ savingLedgerService: Pick<SavingLedgerServiceContract, 'listSavingEntriesByMember' | 'getSavingBalanceByMember'>;
8
+ shuService?: Pick<ShuServiceContract, 'listShuAllocations' | 'listShuPeriods'>;
9
+ }
10
+ interface CreateOfcoopDashboardCompositionOptions {
11
+ monthsWindow?: number;
12
+ heatmapDays?: number;
13
+ referenceDate?: string;
14
+ resolveCurrentShuPeriodId?: (scope?: DashboardScopeRef) => Promise<string | null>;
15
+ }
16
+ export declare function resolveCurrentShuPeriodIdFromRows(periods: ShuPeriodContract[], referenceDateIso?: string): string | null;
17
+ export declare function createOfcoopDashboardCompositionProviders(services: OfcoopDashboardCompositionServicesRef, options?: CreateOfcoopDashboardCompositionOptions): Pick<DashboardSummaryProviders, 'getOnboardingFunnel' | 'getWajibComplianceTrend' | 'getSavingTypeCompositionByPeriod' | 'getMemberGrowthTrend' | 'getShuDistributionSummary' | 'getOperationalActivityHeatmap'>;
18
+ export {};
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveCurrentShuPeriodIdFromRows = resolveCurrentShuPeriodIdFromRows;
4
+ exports.createOfcoopDashboardCompositionProviders = createOfcoopDashboardCompositionProviders;
5
+ const DAY_MS = 24 * 60 * 60 * 1000;
6
+ function dateOnly(iso) {
7
+ return iso.slice(0, 10);
8
+ }
9
+ function monthKeyFromIso(iso) {
10
+ return iso.slice(0, 7);
11
+ }
12
+ function monthStartUtc(monthKey) {
13
+ return new Date(`${monthKey}-01T00:00:00.000Z`);
14
+ }
15
+ function monthEndUtc(monthKey) {
16
+ const start = monthStartUtc(monthKey);
17
+ return new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + 1, 0, 23, 59, 59, 999));
18
+ }
19
+ function listRecentMonthKeys(windowSize, referenceIso = new Date().toISOString()) {
20
+ const ref = new Date(referenceIso);
21
+ const out = [];
22
+ for (let i = windowSize - 1; i >= 0; i -= 1) {
23
+ const d = new Date(Date.UTC(ref.getUTCFullYear(), ref.getUTCMonth() - i, 1));
24
+ out.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`);
25
+ }
26
+ return out;
27
+ }
28
+ function toScopeQuery(scope) {
29
+ return {
30
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
31
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
32
+ };
33
+ }
34
+ function periodContainsDate(period, dateOnlyIso) {
35
+ return period.startDate <= dateOnlyIso && period.endDate >= dateOnlyIso;
36
+ }
37
+ function resolveCurrentShuPeriodIdFromRows(periods, referenceDateIso = new Date().toISOString()) {
38
+ if (periods.length === 0)
39
+ return null;
40
+ const referenceDate = dateOnly(referenceDateIso);
41
+ const activePeriods = periods.filter((row) => periodContainsDate(row, referenceDate));
42
+ if (activePeriods.length > 0) {
43
+ return [...activePeriods].sort((a, b) => b.endDate.localeCompare(a.endDate))[0]?.id ?? null;
44
+ }
45
+ const finalized = periods.filter((row) => row.status === 'finalized');
46
+ if (finalized.length > 0) {
47
+ return [...finalized].sort((a, b) => b.endDate.localeCompare(a.endDate))[0]?.id ?? null;
48
+ }
49
+ return [...periods].sort((a, b) => b.endDate.localeCompare(a.endDate))[0]?.id ?? null;
50
+ }
51
+ function memberIsActiveAtMonth(member, monthKey) {
52
+ const end = monthEndUtc(monthKey).toISOString();
53
+ const joined = member.joinDate <= end;
54
+ if (!joined)
55
+ return false;
56
+ if (member.status !== 'active')
57
+ return false;
58
+ if (member.exitDate && member.exitDate <= end)
59
+ return false;
60
+ return true;
61
+ }
62
+ function memberIsJoinedAtMonth(member, monthKey) {
63
+ const end = monthEndUtc(monthKey).toISOString();
64
+ return member.joinDate <= end;
65
+ }
66
+ async function collectEntriesByMember(savingLedgerService, memberIds, dateFromIso, dateToIso, scope) {
67
+ const rows = await Promise.all(memberIds.map((memberId) => savingLedgerService.listSavingEntriesByMember(memberId, {
68
+ includeDeleted: false,
69
+ dateFrom: dateFromIso,
70
+ dateTo: dateToIso,
71
+ ...toScopeQuery(scope),
72
+ })));
73
+ return rows.flat();
74
+ }
75
+ function createOfcoopDashboardCompositionProviders(services, options = {}) {
76
+ const monthsWindow = Math.max(1, Math.min(24, options.monthsWindow ?? 6));
77
+ const heatmapDays = Math.max(1, Math.min(180, options.heatmapDays ?? 30));
78
+ const resolveCurrentShuPeriodId = async (scope) => {
79
+ if (options.resolveCurrentShuPeriodId) {
80
+ return options.resolveCurrentShuPeriodId(scope);
81
+ }
82
+ if (!services.shuService?.listShuPeriods)
83
+ return null;
84
+ const periods = await services.shuService.listShuPeriods({
85
+ includeDeleted: false,
86
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
87
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
88
+ });
89
+ return resolveCurrentShuPeriodIdFromRows(periods, options.referenceDate);
90
+ };
91
+ return {
92
+ async getOnboardingFunnel(scope) {
93
+ const members = await services.memberService.listMembers({
94
+ includeDeleted: false,
95
+ ...toScopeQuery(scope),
96
+ });
97
+ const balances = await Promise.all(members.map((member) => services.savingLedgerService.getSavingBalanceByMember(member.id)));
98
+ const verifikasiPokok = balances.filter((b) => b.pokok > 0).length;
99
+ const aktif = members.filter((member, idx) => member.status === 'active' && (balances[idx]?.pokok ?? 0) > 0).length;
100
+ return {
101
+ daftar: members.length,
102
+ verifikasiPokok,
103
+ aktif,
104
+ };
105
+ },
106
+ async getWajibComplianceTrend(scope) {
107
+ const monthKeys = listRecentMonthKeys(monthsWindow);
108
+ const members = await services.memberService.listMembers({
109
+ includeDeleted: false,
110
+ status: 'active',
111
+ ...toScopeQuery(scope),
112
+ });
113
+ const memberIds = members.map((member) => member.id);
114
+ if (memberIds.length === 0) {
115
+ return monthKeys.map((month) => ({ month, patuh: 0, menunggak: 0 }));
116
+ }
117
+ const firstMonth = monthKeys[0];
118
+ const lastMonth = monthKeys[monthKeys.length - 1];
119
+ const entries = await collectEntriesByMember(services.savingLedgerService, memberIds, `${firstMonth}-01T00:00:00.000Z`, monthEndUtc(lastMonth).toISOString(), scope);
120
+ const wajibCreditMonthSet = new Set(entries
121
+ .filter((entry) => entry.savingType === 'wajib' && entry.entryType === 'credit' && entry.amount > 0)
122
+ .map((entry) => `${entry.memberId}:${monthKeyFromIso(entry.transactionDate)}`));
123
+ return monthKeys.map((month) => {
124
+ let patuh = 0;
125
+ let menunggak = 0;
126
+ for (const memberId of memberIds) {
127
+ if (wajibCreditMonthSet.has(`${memberId}:${month}`))
128
+ patuh += 1;
129
+ else
130
+ menunggak += 1;
131
+ }
132
+ return { month, patuh, menunggak };
133
+ });
134
+ },
135
+ async getSavingTypeCompositionByPeriod(scope) {
136
+ const monthKeys = listRecentMonthKeys(monthsWindow);
137
+ const members = await services.memberService.listMembers({
138
+ includeDeleted: false,
139
+ ...toScopeQuery(scope),
140
+ });
141
+ const memberIds = members.map((member) => member.id);
142
+ if (memberIds.length === 0) {
143
+ return monthKeys.map((period) => ({ period, pokok: 0, wajib: 0, sukarela: 0 }));
144
+ }
145
+ const firstMonth = monthKeys[0];
146
+ const lastMonth = monthKeys[monthKeys.length - 1];
147
+ const entries = await collectEntriesByMember(services.savingLedgerService, memberIds, `${firstMonth}-01T00:00:00.000Z`, monthEndUtc(lastMonth).toISOString(), scope);
148
+ const buckets = new Map();
149
+ for (const month of monthKeys) {
150
+ buckets.set(month, { period: month, pokok: 0, wajib: 0, sukarela: 0 });
151
+ }
152
+ for (const entry of entries) {
153
+ const month = monthKeyFromIso(entry.transactionDate);
154
+ const bucket = buckets.get(month);
155
+ if (!bucket)
156
+ continue;
157
+ const sign = entry.entryType === 'credit' ? 1 : -1;
158
+ if (entry.savingType === 'pokok')
159
+ bucket.pokok += sign * entry.amount;
160
+ if (entry.savingType === 'wajib')
161
+ bucket.wajib += sign * entry.amount;
162
+ if (entry.savingType === 'sukarela')
163
+ bucket.sukarela += sign * entry.amount;
164
+ }
165
+ return monthKeys.map((month) => buckets.get(month) ?? { period: month, pokok: 0, wajib: 0, sukarela: 0 });
166
+ },
167
+ async getMemberGrowthTrend(scope) {
168
+ const monthKeys = listRecentMonthKeys(monthsWindow);
169
+ const members = await services.memberService.listMembers({
170
+ includeDeleted: false,
171
+ ...toScopeQuery(scope),
172
+ });
173
+ return monthKeys.map((month) => {
174
+ const joined = members.filter((member) => memberIsJoinedAtMonth(member, month)).length;
175
+ const active = members.filter((member) => memberIsActiveAtMonth(member, month)).length;
176
+ return {
177
+ month,
178
+ active,
179
+ nonActive: Math.max(0, joined - active),
180
+ };
181
+ });
182
+ },
183
+ async getShuDistributionSummary(scope) {
184
+ if (!services.shuService)
185
+ return [];
186
+ const periodId = await resolveCurrentShuPeriodId(scope);
187
+ if (!periodId)
188
+ return [];
189
+ const allocations = await services.shuService.listShuAllocations(periodId, { includeDeleted: false });
190
+ const scoped = allocations.filter((row) => {
191
+ if (scope?.tenantId && row.tenantId !== scope.tenantId)
192
+ return false;
193
+ if (scope?.branchId && row.branchId !== scope.branchId)
194
+ return false;
195
+ return true;
196
+ });
197
+ return [...scoped]
198
+ .sort((a, b) => b.allocationAmount - a.allocationAmount)
199
+ .slice(0, 10)
200
+ .map((row) => ({
201
+ memberId: row.memberId,
202
+ allocationAmount: row.allocationAmount,
203
+ }));
204
+ },
205
+ async getOperationalActivityHeatmap(scope) {
206
+ const members = await services.memberService.listMembers({
207
+ includeDeleted: false,
208
+ ...toScopeQuery(scope),
209
+ });
210
+ const memberIds = members.map((member) => member.id);
211
+ if (memberIds.length === 0)
212
+ return [];
213
+ const toDateIso = new Date().toISOString();
214
+ const fromDateIso = new Date(Date.now() - (heatmapDays - 1) * DAY_MS).toISOString();
215
+ const entries = await collectEntriesByMember(services.savingLedgerService, memberIds, fromDateIso, toDateIso, scope);
216
+ const map = new Map();
217
+ for (const entry of entries) {
218
+ const day = dateOnly(entry.transactionDate);
219
+ const hour = new Date(entry.transactionDate).getUTCHours();
220
+ const key = `${day}:${hour}`;
221
+ const prev = map.get(key) ?? { day, hour, total: 0 };
222
+ prev.total += 1;
223
+ map.set(key, prev);
224
+ }
225
+ return Array.from(map.values()).sort((a, b) => (a.day === b.day ? a.hour - b.hour : a.day.localeCompare(b.day)));
226
+ },
227
+ };
228
+ }
@@ -0,0 +1,36 @@
1
+ import type { AuditTrailEvent, AuditTrailQueryInput } from './AuditTrailQueryService';
2
+ import type { Member360Providers } from './Member360Service';
3
+ export declare const OFCOOP_AUDIT_EVENT_CATALOG: readonly ["member.created", "member.updated", "saving.entry-recorded", "saving.policy-upserted", "shu.config-upserted", "shu.period-created", "shu.period-finalized", "shu.allocations-generated"];
4
+ export declare const OFCOOP_CREDIT_AUDIT_EVENT_CATALOG: readonly ["loan.created", "loan.closed", "installment.schedule-generated", "installment.payment-recorded", "credit-policy.created", "credit-policy.updated"];
5
+ interface DomainAuditEventRef {
6
+ eventId: string;
7
+ name: string;
8
+ occurredAt: string;
9
+ entityId?: string;
10
+ tenantId?: string | null;
11
+ branchId?: string | null;
12
+ payload?: Record<string, unknown>;
13
+ }
14
+ interface DomainAuditServiceRef {
15
+ queryEvents: (query: {
16
+ memberId?: string;
17
+ eventNames?: string[];
18
+ fromTimestamp?: string;
19
+ toTimestamp?: string;
20
+ tenantId?: string | null;
21
+ branchId?: string | null;
22
+ limit?: number;
23
+ }) => Promise<DomainAuditEventRef[]>;
24
+ }
25
+ interface OfcoopDomainAuditServicesRef {
26
+ ofcoop?: DomainAuditServiceRef;
27
+ ofcoopCredit?: DomainAuditServiceRef;
28
+ }
29
+ export declare function createOfcoopDomainAuditCompositionProviders(services: OfcoopDomainAuditServicesRef): {
30
+ member360: Pick<Member360Providers, 'getAuditByMember'>;
31
+ auditTrailProvider: {
32
+ listEvents: (query: AuditTrailQueryInput) => Promise<AuditTrailEvent[]>;
33
+ };
34
+ eventCatalog: string[];
35
+ };
36
+ export {};
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OFCOOP_CREDIT_AUDIT_EVENT_CATALOG = exports.OFCOOP_AUDIT_EVENT_CATALOG = void 0;
4
+ exports.createOfcoopDomainAuditCompositionProviders = createOfcoopDomainAuditCompositionProviders;
5
+ exports.OFCOOP_AUDIT_EVENT_CATALOG = [
6
+ 'member.created',
7
+ 'member.updated',
8
+ 'saving.entry-recorded',
9
+ 'saving.policy-upserted',
10
+ 'shu.config-upserted',
11
+ 'shu.period-created',
12
+ 'shu.period-finalized',
13
+ 'shu.allocations-generated',
14
+ ];
15
+ exports.OFCOOP_CREDIT_AUDIT_EVENT_CATALOG = [
16
+ 'loan.created',
17
+ 'loan.closed',
18
+ 'installment.schedule-generated',
19
+ 'installment.payment-recorded',
20
+ 'credit-policy.created',
21
+ 'credit-policy.updated',
22
+ ];
23
+ function isOfcoopEventType(value) {
24
+ return exports.OFCOOP_AUDIT_EVENT_CATALOG.includes(value);
25
+ }
26
+ function isOfcoopCreditEventType(value) {
27
+ return exports.OFCOOP_CREDIT_AUDIT_EVENT_CATALOG.includes(value);
28
+ }
29
+ function toMember360Audit(event) {
30
+ return {
31
+ eventId: event.eventId,
32
+ eventType: event.name,
33
+ occurredAt: event.occurredAt,
34
+ message: event.entityId ? `entity:${event.entityId}` : event.name,
35
+ };
36
+ }
37
+ function extractMemberId(event) {
38
+ const value = event.payload?.memberId;
39
+ return typeof value === 'string' && value.trim() ? value : null;
40
+ }
41
+ function toAuditTrailEvent(event, sourceDomain, fallbackMemberId) {
42
+ return {
43
+ eventId: event.eventId,
44
+ sourceDomain,
45
+ eventType: event.name,
46
+ occurredAt: event.occurredAt,
47
+ memberId: extractMemberId(event) ?? fallbackMemberId ?? undefined,
48
+ tenantId: event.tenantId ?? undefined,
49
+ branchId: event.branchId ?? undefined,
50
+ summary: event.entityId ? `entity:${event.entityId}` : event.name,
51
+ };
52
+ }
53
+ function sortByOccurredAtDesc(rows) {
54
+ return [...rows].sort((a, b) => b.occurredAt.localeCompare(a.occurredAt));
55
+ }
56
+ const OFCOOP_COMPOSITION_AUDIT_EVENT_CATALOG = [
57
+ ...exports.OFCOOP_AUDIT_EVENT_CATALOG,
58
+ ...exports.OFCOOP_CREDIT_AUDIT_EVENT_CATALOG,
59
+ ];
60
+ function createOfcoopDomainAuditCompositionProviders(services) {
61
+ const member360 = {
62
+ async getAuditByMember(memberId, scope) {
63
+ const [ofcoopRows, ofcoopCreditRows] = await Promise.all([
64
+ services.ofcoop?.queryEvents({
65
+ memberId,
66
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
67
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
68
+ limit: 100,
69
+ }) ?? Promise.resolve([]),
70
+ services.ofcoopCredit?.queryEvents({
71
+ memberId,
72
+ ...(scope?.tenantId ? { tenantId: scope.tenantId } : {}),
73
+ ...(scope?.branchId ? { branchId: scope.branchId } : {}),
74
+ limit: 100,
75
+ }) ?? Promise.resolve([]),
76
+ ]);
77
+ return sortByOccurredAtDesc([...ofcoopRows, ...ofcoopCreditRows]).slice(0, 100).map(toMember360Audit);
78
+ },
79
+ };
80
+ const auditTrailProvider = {
81
+ async listEvents(query) {
82
+ const requestedTypes = query.eventTypes ?? [];
83
+ const ofcoopEventNames = requestedTypes.length > 0 ? requestedTypes.filter(isOfcoopEventType) : undefined;
84
+ const ofcoopCreditEventNames = requestedTypes.length > 0 ? requestedTypes.filter(isOfcoopCreditEventType) : undefined;
85
+ const [ofcoopRows, ofcoopCreditRows] = await Promise.all([
86
+ services.ofcoop?.queryEvents({
87
+ ...(query.memberId ? { memberId: query.memberId } : {}),
88
+ ...(query.fromTimestamp ? { fromTimestamp: query.fromTimestamp } : {}),
89
+ ...(query.toTimestamp ? { toTimestamp: query.toTimestamp } : {}),
90
+ ...(query.scope?.tenantId ? { tenantId: query.scope.tenantId } : {}),
91
+ ...(query.scope?.branchId ? { branchId: query.scope.branchId } : {}),
92
+ ...(ofcoopEventNames ? { eventNames: ofcoopEventNames } : {}),
93
+ ...(query.limit ? { limit: query.limit } : {}),
94
+ }) ?? Promise.resolve([]),
95
+ services.ofcoopCredit?.queryEvents({
96
+ ...(query.memberId ? { memberId: query.memberId } : {}),
97
+ ...(query.fromTimestamp ? { fromTimestamp: query.fromTimestamp } : {}),
98
+ ...(query.toTimestamp ? { toTimestamp: query.toTimestamp } : {}),
99
+ ...(query.scope?.tenantId ? { tenantId: query.scope.tenantId } : {}),
100
+ ...(query.scope?.branchId ? { branchId: query.scope.branchId } : {}),
101
+ ...(ofcoopCreditEventNames ? { eventNames: ofcoopCreditEventNames } : {}),
102
+ ...(query.limit ? { limit: query.limit } : {}),
103
+ }) ?? Promise.resolve([]),
104
+ ]);
105
+ const mapped = [
106
+ ...ofcoopRows.map((row) => toAuditTrailEvent(row, 'ofcoop', query.memberId)),
107
+ ...ofcoopCreditRows.map((row) => toAuditTrailEvent(row, 'ofcoop-credit', query.memberId)),
108
+ ];
109
+ return sortByOccurredAtDesc(mapped);
110
+ },
111
+ };
112
+ return {
113
+ member360,
114
+ auditTrailProvider,
115
+ eventCatalog: [...OFCOOP_COMPOSITION_AUDIT_EVENT_CATALOG],
116
+ };
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { ShuServiceContract } from '../contracts/ShuContract';
2
+ import type { ReportPeriod, ReportScopeRef, ReportSummaryProviders } from './ReportSummaryService';
3
+ interface OfcoopReportCompositionServicesRef {
4
+ shuService: Pick<ShuServiceContract, 'listShuAllocations'>;
5
+ }
6
+ interface CreateOfcoopReportCompositionOptions {
7
+ resolveShuPeriodIdsForRange: (period: ReportPeriod, scope?: ReportScopeRef) => Promise<string[]>;
8
+ }
9
+ export declare function createOfcoopReportCompositionProviders(services: OfcoopReportCompositionServicesRef, options: CreateOfcoopReportCompositionOptions): Pick<ReportSummaryProviders, 'getShuSummary'>;
10
+ export {};
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOfcoopReportCompositionProviders = createOfcoopReportCompositionProviders;
4
+ function createOfcoopReportCompositionProviders(services, options) {
5
+ return {
6
+ async getShuSummary(period, scope) {
7
+ const periodIds = await options.resolveShuPeriodIdsForRange(period, scope);
8
+ if (!periodIds.length) {
9
+ return {
10
+ allocationCount: 0,
11
+ totalAllocatedAmount: 0,
12
+ };
13
+ }
14
+ const allocationsByPeriod = await Promise.all(periodIds.map((periodId) => services.shuService.listShuAllocations(periodId, { includeDeleted: false })));
15
+ const allocations = allocationsByPeriod
16
+ .flat()
17
+ .filter((row) => {
18
+ if (scope?.tenantId && row.tenantId !== scope.tenantId)
19
+ return false;
20
+ if (scope?.branchId && row.branchId !== scope.branchId)
21
+ return false;
22
+ return true;
23
+ });
24
+ return {
25
+ allocationCount: allocations.length,
26
+ totalAllocatedAmount: allocations.reduce((sum, row) => sum + row.allocationAmount, 0),
27
+ };
28
+ },
29
+ };
30
+ }