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,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterShuConfigService = void 0;
4
+ const ShuConfigService_1 = require("../ShuConfigService");
5
+ const runtimeSupport_1 = require("./runtimeSupport");
6
+ class DbAdapterShuConfigService {
7
+ constructor(repos, options = {}) {
8
+ this.repos = repos;
9
+ this.options = options;
10
+ this.newId = (0, runtimeSupport_1.makeIdFactory)('shu-config');
11
+ }
12
+ async getActiveShuConfig(scope) {
13
+ return this.repos.shuConfigRepository.getActive(scope);
14
+ }
15
+ async upsertShuConfig(input) {
16
+ (0, ShuConfigService_1.assertValidShuConfigInput)(input);
17
+ const scope = { koperasiId: input.koperasiId, tenantId: input.tenantId, branchId: input.branchId };
18
+ const current = await this.repos.shuConfigRepository.getActive(scope);
19
+ const now = (0, runtimeSupport_1.nowIso)();
20
+ if (current) {
21
+ const updated = await this.repos.shuConfigRepository.upsert({
22
+ ...current,
23
+ ...input,
24
+ notes: input.notes ?? null,
25
+ approvalRef: input.approvalRef ?? null,
26
+ version: current.version + 1,
27
+ lastModified: now,
28
+ });
29
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
30
+ name: 'shu.config-upserted',
31
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
32
+ entityId: updated.id,
33
+ tenantId: updated.tenantId ?? null,
34
+ branchId: updated.branchId ?? null,
35
+ payload: { effectiveDate: updated.effectiveDate },
36
+ });
37
+ return updated;
38
+ }
39
+ const created = await this.repos.shuConfigRepository.upsert({
40
+ id: this.newId(),
41
+ ...input,
42
+ notes: input.notes ?? null,
43
+ approvalRef: input.approvalRef ?? null,
44
+ version: 1,
45
+ lastModified: now,
46
+ deleted: false,
47
+ });
48
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
49
+ name: 'shu.config-upserted',
50
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
51
+ entityId: created.id,
52
+ tenantId: created.tenantId ?? null,
53
+ branchId: created.branchId ?? null,
54
+ payload: { effectiveDate: created.effectiveDate },
55
+ });
56
+ return created;
57
+ }
58
+ async listShuConfigs(scope, options) {
59
+ return this.repos.shuConfigRepository.list(scope, options);
60
+ }
61
+ }
62
+ exports.DbAdapterShuConfigService = DbAdapterShuConfigService;
@@ -0,0 +1,26 @@
1
+ import type { DbAdapter } from 'ofcore';
2
+ import type { CreateShuPeriodInput, CsvExportPayload, ShuAllocationContract, ShuAllocationQueryOptions, ShuPeriodContract, ShuPeriodQueryOptions, ShuServiceContract } from '../../contracts/ShuContract';
3
+ import { type OfcoopRepositories } from '../../data/repositories';
4
+ import { type ServiceRuntimeOptions } from './runtimeSupport';
5
+ export declare class DbAdapterShuService implements ShuServiceContract {
6
+ private readonly db;
7
+ private readonly repos;
8
+ private readonly options;
9
+ private readonly newPeriodId;
10
+ private readonly newAllocationId;
11
+ constructor(db: DbAdapter, repos: OfcoopRepositories, options?: ServiceRuntimeOptions);
12
+ private inTx;
13
+ private toDateOnly;
14
+ private dateToUtcMs;
15
+ private diffDaysInclusive;
16
+ private getEligibleWindow;
17
+ private applySavingEntryToBalance;
18
+ private computeAverageModalBasis;
19
+ private allocateWithLargestRemainder;
20
+ createShuPeriod(input: CreateShuPeriodInput): Promise<ShuPeriodContract>;
21
+ finalizeShuPeriod(periodId: string): Promise<ShuPeriodContract>;
22
+ listShuPeriods(options?: ShuPeriodQueryOptions): Promise<ShuPeriodContract[]>;
23
+ generateShuAllocations(periodId: string): Promise<ShuAllocationContract[]>;
24
+ listShuAllocations(periodId: string, options?: ShuAllocationQueryOptions): Promise<ShuAllocationContract[]>;
25
+ exportShuAllocationsCsv(periodId: string): Promise<CsvExportPayload>;
26
+ }
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterShuService = void 0;
4
+ const repositories_1 = require("../../data/repositories");
5
+ const errors_1 = require("../errors");
6
+ const ShuService_1 = require("../ShuService");
7
+ const errors_2 = require("../errors");
8
+ const runtimeSupport_1 = require("./runtimeSupport");
9
+ class DbAdapterShuService {
10
+ constructor(db, repos, options = {}) {
11
+ this.db = db;
12
+ this.repos = repos;
13
+ this.options = options;
14
+ this.newPeriodId = (0, runtimeSupport_1.makeIdFactory)('shu-period');
15
+ this.newAllocationId = (0, runtimeSupport_1.makeIdFactory)('shu-allocation');
16
+ }
17
+ async inTx(callback) {
18
+ return this.db.transaction(async (tx) => callback((0, repositories_1.createOfcoopRepositories)(tx)));
19
+ }
20
+ toDateOnly(input) {
21
+ return input.slice(0, 10);
22
+ }
23
+ dateToUtcMs(input) {
24
+ return Date.parse(`${this.toDateOnly(input)}T00:00:00.000Z`);
25
+ }
26
+ diffDaysInclusive(startDate, endDate) {
27
+ const ms = this.dateToUtcMs(endDate) - this.dateToUtcMs(startDate);
28
+ return Math.floor(ms / 86400000) + 1;
29
+ }
30
+ getEligibleWindow(member, periodStart, periodEnd) {
31
+ if (member.status !== 'active' && member.status !== 'exited')
32
+ return null;
33
+ const joinedAt = this.toDateOnly(member.joinDate);
34
+ const exitedAt = member.exitDate ? this.toDateOnly(member.exitDate) : null;
35
+ if (member.status === 'exited' && !exitedAt)
36
+ return null;
37
+ const start = joinedAt > periodStart ? joinedAt : periodStart;
38
+ const naturalEnd = exitedAt && exitedAt < periodEnd ? exitedAt : periodEnd;
39
+ if (start > naturalEnd)
40
+ return null;
41
+ const activeDays = this.diffDaysInclusive(start, naturalEnd);
42
+ const totalDays = this.diffDaysInclusive(periodStart, periodEnd);
43
+ return {
44
+ start,
45
+ end: naturalEnd,
46
+ activeDays,
47
+ proRataFactor: totalDays > 0 ? activeDays / totalDays : 0,
48
+ };
49
+ }
50
+ applySavingEntryToBalance(balances, entry, allowedSukarelaReferenceTypes) {
51
+ const sign = entry.entryType === 'debit' ? -1 : 1;
52
+ if (entry.savingType === 'pokok')
53
+ balances.pokok += sign * entry.amount;
54
+ if (entry.savingType === 'wajib')
55
+ balances.wajib += sign * entry.amount;
56
+ if (entry.savingType === 'sukarela') {
57
+ if (!allowedSukarelaReferenceTypes || allowedSukarelaReferenceTypes.size === 0)
58
+ return;
59
+ const ref = (entry.referenceType ?? '').trim();
60
+ if (!allowedSukarelaReferenceTypes.has(ref))
61
+ return;
62
+ balances.sukarela += sign * entry.amount;
63
+ }
64
+ }
65
+ async computeAverageModalBasis(args) {
66
+ const rows = await args.repos.savingLedgerRepository.listByMember(args.memberId, {
67
+ includeDeleted: false,
68
+ dateTo: args.periodEnd,
69
+ tenantId: args.tenantId ?? null,
70
+ branchId: args.branchId ?? null,
71
+ });
72
+ const asc = rows
73
+ .slice()
74
+ .sort((a, b) => this.toDateOnly(a.transactionDate).localeCompare(this.toDateOnly(b.transactionDate)));
75
+ const balances = { pokok: 0, wajib: 0, sukarela: 0 };
76
+ const activeStartMs = this.dateToUtcMs(args.activeStart);
77
+ const activeEndExclusiveMs = this.dateToUtcMs(args.activeEnd) + 86400000;
78
+ const totalPeriodDays = this.diffDaysInclusive(args.periodStart, args.periodEnd);
79
+ let weightedModal = 0;
80
+ let cursorMs = this.dateToUtcMs(args.periodStart);
81
+ let index = 0;
82
+ while (index < asc.length && this.dateToUtcMs(asc[index].transactionDate) < cursorMs) {
83
+ this.applySavingEntryToBalance(balances, asc[index], args.includeSukarela ? args.allowedSukarelaReferenceTypes : null);
84
+ index += 1;
85
+ }
86
+ while (cursorMs < activeEndExclusiveMs) {
87
+ const nextEventMs = index < asc.length ? this.dateToUtcMs(asc[index].transactionDate) : activeEndExclusiveMs;
88
+ const sliceEndMs = Math.min(nextEventMs, activeEndExclusiveMs);
89
+ if (sliceEndMs > cursorMs) {
90
+ const overlapStart = Math.max(cursorMs, activeStartMs);
91
+ const overlapEnd = Math.min(sliceEndMs, activeEndExclusiveMs);
92
+ if (overlapEnd > overlapStart) {
93
+ const days = Math.floor((overlapEnd - overlapStart) / 86400000);
94
+ const modalNow = balances.pokok + balances.wajib + (args.includeSukarela ? balances.sukarela : 0);
95
+ weightedModal += modalNow * days;
96
+ }
97
+ cursorMs = sliceEndMs;
98
+ }
99
+ else {
100
+ cursorMs += 86400000;
101
+ }
102
+ while (index < asc.length && this.dateToUtcMs(asc[index].transactionDate) === sliceEndMs) {
103
+ this.applySavingEntryToBalance(balances, asc[index], args.includeSukarela ? args.allowedSukarelaReferenceTypes : null);
104
+ index += 1;
105
+ }
106
+ }
107
+ return totalPeriodDays > 0 ? weightedModal / totalPeriodDays : 0;
108
+ }
109
+ allocateWithLargestRemainder(args) {
110
+ const totalScore = args.members.reduce((sum, item) => sum + item.score, 0);
111
+ if (totalScore <= 0) {
112
+ throw (0, errors_2.invalidState)('SHU allocation requires non-zero total score; escalate to RAT decision');
113
+ }
114
+ const floorAllocations = new Map();
115
+ const remainders = args.members.map((item) => {
116
+ const raw = (args.poolAmount * item.score) / totalScore;
117
+ const floored = Math.floor(raw);
118
+ floorAllocations.set(item.memberId, floored);
119
+ return { memberId: item.memberId, remainder: raw - floored };
120
+ });
121
+ const allocated = Array.from(floorAllocations.values()).reduce((sum, value) => sum + value, 0);
122
+ let left = args.poolAmount - allocated;
123
+ remainders.sort((a, b) => {
124
+ if (b.remainder !== a.remainder)
125
+ return b.remainder - a.remainder;
126
+ return a.memberId.localeCompare(b.memberId);
127
+ });
128
+ let i = 0;
129
+ while (left > 0 && remainders.length > 0) {
130
+ const target = remainders[i % remainders.length];
131
+ floorAllocations.set(target.memberId, (floorAllocations.get(target.memberId) ?? 0) + 1);
132
+ left -= 1;
133
+ i += 1;
134
+ }
135
+ return floorAllocations;
136
+ }
137
+ async createShuPeriod(input) {
138
+ (0, ShuService_1.assertValidCreateShuPeriodInput)(input);
139
+ const created = await this.repos.shuPeriodRepository.create({
140
+ id: this.newPeriodId(),
141
+ periodCode: input.periodCode,
142
+ startDate: input.startDate,
143
+ endDate: input.endDate,
144
+ status: 'draft',
145
+ formulaSnapshot: (0, runtimeSupport_1.clone)(input.formulaSnapshot),
146
+ branchId: input.branchId ?? null,
147
+ tenantId: input.tenantId ?? null,
148
+ version: 1,
149
+ lastModified: (0, runtimeSupport_1.nowIso)(),
150
+ deleted: false,
151
+ });
152
+ this.options.logger?.logInfo('[ofcoop] shu period created', { periodId: created.id, periodCode: created.periodCode });
153
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
154
+ name: 'shu.period-created',
155
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
156
+ entityId: created.id,
157
+ tenantId: created.tenantId ?? null,
158
+ branchId: created.branchId ?? null,
159
+ payload: { periodCode: created.periodCode },
160
+ });
161
+ return created;
162
+ }
163
+ async finalizeShuPeriod(periodId) {
164
+ const row = await this.repos.shuPeriodRepository.getById(periodId);
165
+ if (!row || row.deleted)
166
+ throw (0, errors_1.notFound)('SHU period', periodId);
167
+ const updated = await this.repos.shuPeriodRepository.update(periodId, {
168
+ status: 'finalized',
169
+ version: row.version + 1,
170
+ lastModified: (0, runtimeSupport_1.nowIso)(),
171
+ });
172
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
173
+ name: 'shu.period-finalized',
174
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
175
+ entityId: updated.id,
176
+ tenantId: updated.tenantId ?? null,
177
+ branchId: updated.branchId ?? null,
178
+ payload: { status: updated.status },
179
+ });
180
+ return updated;
181
+ }
182
+ async listShuPeriods(options = {}) {
183
+ const rows = await this.repos.shuPeriodRepository.list({
184
+ includeDeleted: options.includeDeleted,
185
+ tenantId: options.tenantId,
186
+ branchId: options.branchId,
187
+ });
188
+ return rows.filter((row) => {
189
+ if (options.status && row.status !== options.status)
190
+ return false;
191
+ if (options.dateContains) {
192
+ const date = options.dateContains.slice(0, 10);
193
+ if (row.startDate > date)
194
+ return false;
195
+ if (row.endDate < date)
196
+ return false;
197
+ }
198
+ return true;
199
+ });
200
+ }
201
+ async generateShuAllocations(periodId) {
202
+ return this.inTx(async (repos) => {
203
+ const period = await repos.shuPeriodRepository.getById(periodId);
204
+ if (!period || period.deleted)
205
+ throw (0, errors_1.notFound)('SHU period', periodId);
206
+ const members = await repos.memberRepository.list({
207
+ includeDeleted: false,
208
+ tenantId: period.tenantId ?? undefined,
209
+ branchId: period.branchId ?? undefined,
210
+ });
211
+ if (members.length === 0)
212
+ return [];
213
+ const snapshot = period.formulaSnapshot;
214
+ const distributionMode = String(snapshot.distributionMode ?? 'equal-share');
215
+ const rows = [];
216
+ if (distributionMode === 'score-weighted-v1') {
217
+ const periodStart = this.toDateOnly(period.startDate);
218
+ const periodEnd = this.toDateOnly(period.endDate);
219
+ const totalShuRaw = Number(snapshot.shuTahunBukuIdr ?? snapshot.totalDistributableShuIdr ?? 0);
220
+ const cadanganPercentRaw = Number(snapshot.cadanganPercent ?? 0);
221
+ const poolAnggotaPercentRaw = Number(snapshot.poolAnggotaPercent ?? 0);
222
+ const bobotTransaksiPercentRaw = Number(snapshot.bobotTransaksiPercent ?? 0);
223
+ const bobotModalPercentRaw = Number(snapshot.bobotModalPercent ?? 0);
224
+ const includeSukarelaInModalBasis = Boolean(snapshot.includeSukarelaInModalBasis ?? false);
225
+ const sukarelaModalReferenceTypes = Array.isArray(snapshot.sukarelaModalReferenceTypes)
226
+ ? snapshot.sukarelaModalReferenceTypes.map((item) => String(item).trim()).filter((item) => item.length > 0)
227
+ : [];
228
+ const allowedSukarelaReferenceTypes = new Set(sukarelaModalReferenceTypes);
229
+ const transactionScoreByMemberId = snapshot.transactionScoreByMemberId && typeof snapshot.transactionScoreByMemberId === 'object'
230
+ ? snapshot.transactionScoreByMemberId
231
+ : {};
232
+ if (!Number.isFinite(totalShuRaw) || totalShuRaw <= 0) {
233
+ throw (0, errors_2.invalidState)('shuTahunBukuIdr must be provided for score-weighted-v1 allocation');
234
+ }
235
+ if (!Number.isFinite(cadanganPercentRaw) || cadanganPercentRaw < 0 || cadanganPercentRaw > 100) {
236
+ throw (0, errors_2.invalidState)('cadanganPercent must be in range 0-100 for score-weighted-v1 allocation');
237
+ }
238
+ if (!Number.isFinite(poolAnggotaPercentRaw) || poolAnggotaPercentRaw < 0 || poolAnggotaPercentRaw > 100) {
239
+ throw (0, errors_2.invalidState)('poolAnggotaPercent must be in range 0-100 for score-weighted-v1 allocation');
240
+ }
241
+ if (!Number.isFinite(bobotTransaksiPercentRaw) ||
242
+ !Number.isFinite(bobotModalPercentRaw) ||
243
+ bobotTransaksiPercentRaw < 0 ||
244
+ bobotModalPercentRaw < 0 ||
245
+ bobotTransaksiPercentRaw + bobotModalPercentRaw !== 100) {
246
+ throw (0, errors_2.invalidState)('bobotTransaksiPercent + bobotModalPercent must equal 100 for score-weighted-v1 allocation');
247
+ }
248
+ const shuSetelahCadangan = Math.max(0, Math.floor(totalShuRaw - (totalShuRaw * cadanganPercentRaw) / 100));
249
+ const poolShuAnggota = Math.max(0, Math.floor((shuSetelahCadangan * poolAnggotaPercentRaw) / 100));
250
+ if (poolShuAnggota <= 0)
251
+ return [];
252
+ const eligibleMembers = [];
253
+ for (const member of members) {
254
+ const window = this.getEligibleWindow(member, periodStart, periodEnd);
255
+ if (!window)
256
+ continue;
257
+ const averageModalBasis = await this.computeAverageModalBasis({
258
+ memberId: member.id,
259
+ tenantId: period.tenantId ?? null,
260
+ branchId: period.branchId ?? null,
261
+ periodStart,
262
+ periodEnd,
263
+ activeStart: window.start,
264
+ activeEnd: window.end,
265
+ includeSukarela: includeSukarelaInModalBasis,
266
+ allowedSukarelaReferenceTypes,
267
+ repos,
268
+ });
269
+ const transaksiScoreRaw = Number(transactionScoreByMemberId[member.id] ?? 0);
270
+ const transaksiScore = Number.isFinite(transaksiScoreRaw) && transaksiScoreRaw > 0 ? transaksiScoreRaw * window.proRataFactor : 0;
271
+ const modalScore = averageModalBasis > 0 ? averageModalBasis : 0;
272
+ const score = (bobotTransaksiPercentRaw / 100) * transaksiScore + (bobotModalPercentRaw / 100) * modalScore;
273
+ eligibleMembers.push({
274
+ member,
275
+ proRataFactor: window.proRataFactor,
276
+ modalScore,
277
+ transaksiScore,
278
+ score,
279
+ activeDays: window.activeDays,
280
+ });
281
+ }
282
+ const scored = eligibleMembers.filter((item) => item.score > 0);
283
+ const allocations = this.allocateWithLargestRemainder({
284
+ poolAmount: poolShuAnggota,
285
+ members: scored.map((item) => ({ memberId: item.member.id, score: item.score })),
286
+ });
287
+ for (const item of scored) {
288
+ rows.push({
289
+ id: this.newAllocationId(),
290
+ shuPeriodId: periodId,
291
+ memberId: item.member.id,
292
+ allocationAmount: allocations.get(item.member.id) ?? 0,
293
+ calculationBasis: {
294
+ mode: 'score-weighted-v1',
295
+ shuTahunBukuIdr: totalShuRaw,
296
+ cadanganPercent: cadanganPercentRaw,
297
+ shuSetelahCadangan,
298
+ poolAnggotaPercent: poolAnggotaPercentRaw,
299
+ poolShuAnggota,
300
+ bobotTransaksiPercent: bobotTransaksiPercentRaw,
301
+ bobotModalPercent: bobotModalPercentRaw,
302
+ includeSukarelaInModalBasis,
303
+ sukarelaModalReferenceTypes,
304
+ proRataFactor: item.proRataFactor,
305
+ activeDays: item.activeDays,
306
+ transaksiScore: item.transaksiScore,
307
+ modalScore: item.modalScore,
308
+ score: item.score,
309
+ },
310
+ branchId: item.member.branchId ?? period.branchId ?? null,
311
+ tenantId: item.member.tenantId ?? period.tenantId ?? null,
312
+ version: 1,
313
+ lastModified: (0, runtimeSupport_1.nowIso)(),
314
+ deleted: false,
315
+ });
316
+ }
317
+ }
318
+ else {
319
+ const activeMembers = members.filter((member) => member.status === 'active');
320
+ if (activeMembers.length === 0)
321
+ return [];
322
+ const defaultTotal = Number(snapshot.totalDistributableShuIdr || 0);
323
+ const total = Number.isFinite(defaultTotal) && defaultTotal > 0 ? defaultTotal : activeMembers.length * 100000;
324
+ const perMember = Math.floor(total / activeMembers.length);
325
+ for (const member of activeMembers) {
326
+ rows.push({
327
+ id: this.newAllocationId(),
328
+ shuPeriodId: periodId,
329
+ memberId: member.id,
330
+ allocationAmount: perMember,
331
+ calculationBasis: {
332
+ mode: 'equal-share',
333
+ memberCount: activeMembers.length,
334
+ sourceTotal: total,
335
+ },
336
+ branchId: member.branchId ?? period.branchId ?? null,
337
+ tenantId: member.tenantId ?? period.tenantId ?? null,
338
+ version: 1,
339
+ lastModified: (0, runtimeSupport_1.nowIso)(),
340
+ deleted: false,
341
+ });
342
+ }
343
+ }
344
+ const allocations = await repos.shuAllocationRepository.createMany(rows);
345
+ this.options.logger?.logInfo('[ofcoop] shu allocations generated', {
346
+ periodId,
347
+ allocations: allocations.length,
348
+ });
349
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
350
+ name: 'shu.allocations-generated',
351
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
352
+ entityId: periodId,
353
+ tenantId: period.tenantId ?? null,
354
+ branchId: period.branchId ?? null,
355
+ payload: { allocations: allocations.length },
356
+ });
357
+ return allocations;
358
+ });
359
+ }
360
+ async listShuAllocations(periodId, options) {
361
+ return this.repos.shuAllocationRepository.listByPeriod(periodId, options);
362
+ }
363
+ async exportShuAllocationsCsv(periodId) {
364
+ const rows = await this.listShuAllocations(periodId, { includeDeleted: false });
365
+ const body = [
366
+ 'allocationId,shuPeriodId,memberId,allocationAmount',
367
+ ...rows.map((row) => `${row.id},${row.shuPeriodId},${row.memberId},${row.allocationAmount}`),
368
+ ].join('\n');
369
+ return {
370
+ filename: `shu-allocations-${periodId}.csv`,
371
+ contentType: 'text/csv',
372
+ body,
373
+ };
374
+ }
375
+ }
376
+ exports.DbAdapterShuService = DbAdapterShuService;
@@ -0,0 +1,18 @@
1
+ import type { LoggerAdapter } from 'ofcore';
2
+ export interface ServiceRuntimeOptions {
3
+ logger?: LoggerAdapter;
4
+ emitEvent?: (event: OfcoopDomainEvent) => Promise<void> | void;
5
+ }
6
+ export type OfcoopDomainEventName = 'member.created' | 'member.updated' | 'saving.entry-recorded' | 'saving.policy-upserted' | 'shu.config-upserted' | 'shu.period-created' | 'shu.period-finalized' | 'shu.allocations-generated';
7
+ export interface OfcoopDomainEvent {
8
+ name: OfcoopDomainEventName;
9
+ occurredAt: string;
10
+ entityId?: string;
11
+ tenantId?: string | null;
12
+ branchId?: string | null;
13
+ payload?: Record<string, unknown>;
14
+ }
15
+ export declare function nowIso(): string;
16
+ export declare function makeIdFactory(prefix: string): () => string;
17
+ export declare function clone<T>(value: T): T;
18
+ export declare function emitDomainEvent(options: ServiceRuntimeOptions, event: OfcoopDomainEvent): Promise<void>;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.nowIso = nowIso;
4
+ exports.makeIdFactory = makeIdFactory;
5
+ exports.clone = clone;
6
+ exports.emitDomainEvent = emitDomainEvent;
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function makeIdFactory(prefix) {
11
+ let counter = 0;
12
+ return () => {
13
+ counter += 1;
14
+ return `${prefix}-${Date.now()}-${counter}`;
15
+ };
16
+ }
17
+ function clone(value) {
18
+ return JSON.parse(JSON.stringify(value));
19
+ }
20
+ async function emitDomainEvent(options, event) {
21
+ if (!options.emitEvent)
22
+ return;
23
+ try {
24
+ await options.emitEvent(event);
25
+ }
26
+ catch (error) {
27
+ options.logger?.logError(error, { message: '[ofcoop] domain event sink failed', eventName: event.name });
28
+ }
29
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ofcoop-shared-core",
3
+ "version": "0.1.0-alpha.0",
4
+ "private": false,
5
+ "description": "Cooperative shared-core baseline (member/saving/shu/policy) with ofcore primitive bridge.",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
13
+ "build": "npm run clean && tsc -p tsconfig.build.json",
14
+ "typecheck": "tsc -p tsconfig.json --noEmit",
15
+ "test": "NODE_PATH=.. npm run build && NODE_PATH=.. node --test ./tests/*.test.js",
16
+ "verify:contract": "node ./scripts/verify-surface.js",
17
+ "verify:surface": "node ./scripts/verify-surface.js",
18
+ "verify:ofcore-primitives": "node ./scripts/verify-ofcore-primitives.js",
19
+ "verify:governance": "node ./scripts/verify-governance-transport.js",
20
+ "verify:factory-boundary": "node ./scripts/verify-no-service-logic-in-factory.js",
21
+ "verify:runtime-bridge": "NODE_PATH=.. npm run build && NODE_PATH=.. node ./scripts/verify-runtime-bridge.js",
22
+ "verify:logic": "NODE_PATH=.. npm run build && NODE_PATH=.. node ./scripts/verify-residual-conformance.js",
23
+ "verify:residual-conformance": "NODE_PATH=.. npm run build && NODE_PATH=.. node ./scripts/verify-residual-conformance.js",
24
+ "ci:check": "npm run typecheck && npm run verify:contract && npm run verify:ofcore-primitives && npm run verify:governance && npm run verify:factory-boundary && npm run verify:runtime-bridge && npm run test && npm run verify:logic",
25
+ "prepublishOnly": "npm run ci:check",
26
+ "prepack": "npm run build"
27
+ },
28
+ "dependencies": {
29
+ "ofcore": "0.1.0-alpha.0"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.9.3"
33
+ },
34
+ "author": {
35
+ "name": "Agus Made",
36
+ "email": "krisnaparta@gmail.com"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }