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,8 @@
1
+ export type OfcoopErrorCode = 'RESOURCE_NOT_FOUND' | 'CONFLICT' | 'INVALID_STATE';
2
+ export declare class OfcoopDomainError extends Error {
3
+ readonly code: OfcoopErrorCode;
4
+ constructor(code: OfcoopErrorCode, message: string);
5
+ }
6
+ export declare function notFound(entity: string, id: string): OfcoopDomainError;
7
+ export declare function conflict(message: string): OfcoopDomainError;
8
+ export declare function invalidState(message: string): OfcoopDomainError;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OfcoopDomainError = void 0;
4
+ exports.notFound = notFound;
5
+ exports.conflict = conflict;
6
+ exports.invalidState = invalidState;
7
+ class OfcoopDomainError extends Error {
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.code = code;
11
+ this.name = 'OfcoopDomainError';
12
+ }
13
+ }
14
+ exports.OfcoopDomainError = OfcoopDomainError;
15
+ function notFound(entity, id) {
16
+ return new OfcoopDomainError('RESOURCE_NOT_FOUND', `${entity} not found: ${id}`);
17
+ }
18
+ function conflict(message) {
19
+ return new OfcoopDomainError('CONFLICT', message);
20
+ }
21
+ function invalidState(message) {
22
+ return new OfcoopDomainError('INVALID_STATE', message);
23
+ }
@@ -0,0 +1,9 @@
1
+ import type { MemberNumberPolicyContract, MemberNumberPolicyScope, MemberNumberPolicyServiceContract, UpsertMemberNumberPolicyInput } from '../../contracts/MemberNumberPolicyContract';
2
+ import type { OfcoopRepositories } from '../../data/repositories';
3
+ export declare class DbAdapterMemberNumberPolicyService implements MemberNumberPolicyServiceContract {
4
+ private readonly repos;
5
+ private readonly newId;
6
+ constructor(repos: OfcoopRepositories);
7
+ getActiveMemberNumberPolicy(scope: MemberNumberPolicyScope): Promise<MemberNumberPolicyContract | null>;
8
+ upsertMemberNumberPolicy(input: UpsertMemberNumberPolicyInput): Promise<MemberNumberPolicyContract>;
9
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterMemberNumberPolicyService = void 0;
4
+ const MemberNumberPolicyService_1 = require("../MemberNumberPolicyService");
5
+ const runtimeSupport_1 = require("./runtimeSupport");
6
+ class DbAdapterMemberNumberPolicyService {
7
+ constructor(repos) {
8
+ this.repos = repos;
9
+ this.newId = (0, runtimeSupport_1.makeIdFactory)('member-number-policy');
10
+ }
11
+ async getActiveMemberNumberPolicy(scope) {
12
+ return this.repos.memberNumberPolicyRepository.getActive(scope);
13
+ }
14
+ async upsertMemberNumberPolicy(input) {
15
+ (0, MemberNumberPolicyService_1.assertValidMemberNumberPolicyInput)(input);
16
+ const scope = { koperasiId: input.koperasiId, tenantId: input.tenantId, branchId: input.branchId };
17
+ const current = await this.repos.memberNumberPolicyRepository.getActive(scope);
18
+ const now = (0, runtimeSupport_1.nowIso)();
19
+ if (current) {
20
+ return this.repos.memberNumberPolicyRepository.upsert({
21
+ ...current,
22
+ ...input,
23
+ version: current.version + 1,
24
+ lastModified: now,
25
+ });
26
+ }
27
+ return this.repos.memberNumberPolicyRepository.upsert({
28
+ id: this.newId(),
29
+ ...input,
30
+ approvalRef: input.approvalRef ?? null,
31
+ version: 1,
32
+ lastModified: now,
33
+ deleted: false,
34
+ });
35
+ }
36
+ }
37
+ exports.DbAdapterMemberNumberPolicyService = DbAdapterMemberNumberPolicyService;
@@ -0,0 +1,21 @@
1
+ import type { DbAdapter } from 'ofcore';
2
+ import type { CreateMemberInput, MemberContract, MemberExitInput, MemberQueryOptions, MemberServiceContract, MemberStatus, MemberWaConsentInput, UpdateMemberInput } from '../../contracts/MemberContract';
3
+ import { type OfcoopRepositories } from '../../data/repositories';
4
+ import { type ServiceRuntimeOptions } from './runtimeSupport';
5
+ export declare class DbAdapterMemberService implements MemberServiceContract {
6
+ private readonly db;
7
+ private readonly repos;
8
+ private readonly options;
9
+ private readonly newId;
10
+ constructor(db: DbAdapter, repos: OfcoopRepositories, options?: ServiceRuntimeOptions);
11
+ private inTx;
12
+ createMember(input: CreateMemberInput): Promise<MemberContract>;
13
+ updateMember(id: string, updates: UpdateMemberInput): Promise<MemberContract>;
14
+ getMemberById(id: string): Promise<MemberContract | null>;
15
+ listMembers(options?: MemberQueryOptions): Promise<MemberContract[]>;
16
+ changeMemberStatus(id: string, status: MemberStatus): Promise<MemberContract>;
17
+ setMemberWaConsent(id: string, consent: MemberWaConsentInput): Promise<MemberContract>;
18
+ requestMemberExit(id: string, payload: MemberExitInput): Promise<MemberContract>;
19
+ settleMemberExit(id: string, payload: MemberExitInput): Promise<MemberContract>;
20
+ cancelMemberExit(id: string, payload: MemberExitInput): Promise<MemberContract>;
21
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterMemberService = void 0;
4
+ const repositories_1 = require("../../data/repositories");
5
+ const errors_1 = require("../errors");
6
+ const MemberService_1 = require("../MemberService");
7
+ const runtimeSupport_1 = require("./runtimeSupport");
8
+ class DbAdapterMemberService {
9
+ constructor(db, repos, options = {}) {
10
+ this.db = db;
11
+ this.repos = repos;
12
+ this.options = options;
13
+ this.newId = (0, runtimeSupport_1.makeIdFactory)('member');
14
+ }
15
+ async inTx(callback) {
16
+ return this.db.transaction(async (tx) => callback((0, repositories_1.createOfcoopRepositories)(tx)));
17
+ }
18
+ async createMember(input) {
19
+ (0, MemberService_1.assertValidCreateMemberInput)(input);
20
+ const existing = await this.repos.memberRepository.list({ tenantId: input.tenantId ?? null });
21
+ if (existing.some((member) => member.memberNumber === input.memberNumber && !member.deleted)) {
22
+ throw (0, errors_1.conflict)(`Member number already exists: ${input.memberNumber}`);
23
+ }
24
+ const result = await this.repos.memberRepository.create({
25
+ id: this.newId(),
26
+ memberNumber: input.memberNumber,
27
+ fullName: input.fullName,
28
+ status: 'active',
29
+ joinDate: input.joinDate,
30
+ exitDate: null,
31
+ branchId: input.branchId ?? null,
32
+ tenantId: input.tenantId ?? null,
33
+ version: 1,
34
+ lastModified: (0, runtimeSupport_1.nowIso)(),
35
+ deleted: false,
36
+ });
37
+ this.options.logger?.logInfo('[ofcoop] member created', { memberId: result.id, tenantId: result.tenantId, branchId: result.branchId });
38
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
39
+ name: 'member.created',
40
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
41
+ entityId: result.id,
42
+ tenantId: result.tenantId ?? null,
43
+ branchId: result.branchId ?? null,
44
+ payload: { memberNumber: result.memberNumber },
45
+ });
46
+ return result;
47
+ }
48
+ async updateMember(id, updates) {
49
+ const row = await this.repos.memberRepository.getById(id);
50
+ if (!row || row.deleted)
51
+ throw (0, errors_1.notFound)('Member', id);
52
+ const updated = await this.repos.memberRepository.update(id, {
53
+ ...(updates.fullName !== undefined ? { fullName: updates.fullName } : {}),
54
+ ...(updates.memberNumber !== undefined ? { memberNumber: updates.memberNumber } : {}),
55
+ version: row.version + 1,
56
+ lastModified: (0, runtimeSupport_1.nowIso)(),
57
+ });
58
+ this.options.logger?.logInfo('[ofcoop] member updated', { memberId: id });
59
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
60
+ name: 'member.updated',
61
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
62
+ entityId: updated.id,
63
+ tenantId: updated.tenantId ?? null,
64
+ branchId: updated.branchId ?? null,
65
+ payload: { memberNumber: updated.memberNumber },
66
+ });
67
+ return updated;
68
+ }
69
+ async getMemberById(id) {
70
+ const row = await this.repos.memberRepository.getById(id);
71
+ if (!row || row.deleted)
72
+ return null;
73
+ return row;
74
+ }
75
+ async listMembers(options) {
76
+ return this.repos.memberRepository.list(options);
77
+ }
78
+ async changeMemberStatus(id, status) {
79
+ const row = await this.repos.memberRepository.getById(id);
80
+ if (!row || row.deleted)
81
+ throw (0, errors_1.notFound)('Member', id);
82
+ return this.repos.memberRepository.update(id, {
83
+ status,
84
+ version: row.version + 1,
85
+ lastModified: (0, runtimeSupport_1.nowIso)(),
86
+ });
87
+ }
88
+ async setMemberWaConsent(id, consent) {
89
+ return this.inTx(async (repos) => {
90
+ const row = await repos.memberRepository.getById(id);
91
+ if (!row || row.deleted)
92
+ throw (0, errors_1.notFound)('Member', id);
93
+ await repos.memberWaConsentRepository.set(id, consent);
94
+ return repos.memberRepository.update(id, {
95
+ version: row.version + 1,
96
+ lastModified: (0, runtimeSupport_1.nowIso)(),
97
+ });
98
+ });
99
+ }
100
+ async requestMemberExit(id, payload) {
101
+ return this.inTx(async (repos) => {
102
+ const row = await repos.memberRepository.getById(id);
103
+ if (!row || row.deleted)
104
+ throw (0, errors_1.notFound)('Member', id);
105
+ await repos.memberExitStateRepository.set(id, 'requested', payload);
106
+ return repos.memberRepository.update(id, {
107
+ status: 'inactive',
108
+ version: row.version + 1,
109
+ lastModified: (0, runtimeSupport_1.nowIso)(),
110
+ });
111
+ });
112
+ }
113
+ async settleMemberExit(id, payload) {
114
+ return this.inTx(async (repos) => {
115
+ const row = await repos.memberRepository.getById(id);
116
+ if (!row || row.deleted)
117
+ throw (0, errors_1.notFound)('Member', id);
118
+ await repos.memberExitStateRepository.set(id, 'settled', payload);
119
+ return repos.memberRepository.update(id, {
120
+ status: 'exited',
121
+ exitDate: (0, runtimeSupport_1.nowIso)(),
122
+ version: row.version + 1,
123
+ lastModified: (0, runtimeSupport_1.nowIso)(),
124
+ });
125
+ });
126
+ }
127
+ async cancelMemberExit(id, payload) {
128
+ return this.inTx(async (repos) => {
129
+ const row = await repos.memberRepository.getById(id);
130
+ if (!row || row.deleted)
131
+ throw (0, errors_1.notFound)('Member', id);
132
+ await repos.memberExitStateRepository.set(id, 'canceled', payload);
133
+ return repos.memberRepository.update(id, {
134
+ status: 'active',
135
+ exitDate: null,
136
+ version: row.version + 1,
137
+ lastModified: (0, runtimeSupport_1.nowIso)(),
138
+ });
139
+ });
140
+ }
141
+ }
142
+ exports.DbAdapterMemberService = DbAdapterMemberService;
@@ -0,0 +1,9 @@
1
+ import type { SavingComplianceSnapshotContract, SavingComplianceSnapshotQueryOptions, SavingComplianceSnapshotScope, SavingComplianceSnapshotServiceContract } from '../../contracts/SavingComplianceSnapshotContract';
2
+ import type { OfcoopRepositories } from '../../data/repositories';
3
+ export declare class DbAdapterSavingComplianceSnapshotService implements SavingComplianceSnapshotServiceContract {
4
+ private readonly repos;
5
+ constructor(repos: OfcoopRepositories);
6
+ private buildSnapshot;
7
+ listComplianceSnapshots(scope: SavingComplianceSnapshotScope, options?: SavingComplianceSnapshotQueryOptions): Promise<SavingComplianceSnapshotContract[]>;
8
+ getComplianceSnapshotByMember(memberId: string, scope: SavingComplianceSnapshotScope): Promise<SavingComplianceSnapshotContract | null>;
9
+ }
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterSavingComplianceSnapshotService = void 0;
4
+ const SavingComplianceSnapshotService_1 = require("../SavingComplianceSnapshotService");
5
+ const runtimeSupport_1 = require("./runtimeSupport");
6
+ class DbAdapterSavingComplianceSnapshotService {
7
+ constructor(repos) {
8
+ this.repos = repos;
9
+ }
10
+ async buildSnapshot(member, scope, policy) {
11
+ const memberEntries = await this.repos.savingLedgerRepository.listByMember(member.id, {
12
+ includeDeleted: false,
13
+ tenantId: scope.tenantId,
14
+ branchId: scope.branchId ?? null,
15
+ });
16
+ const wajibEntries = memberEntries.filter((entry) => entry.savingType === 'wajib');
17
+ const wajibCreditTotalIdr = wajibEntries
18
+ .filter((entry) => entry.entryType === 'credit')
19
+ .reduce((sum, entry) => sum + entry.amount, 0);
20
+ const wajibDebitTotalIdr = wajibEntries
21
+ .filter((entry) => entry.entryType === 'debit')
22
+ .reduce((sum, entry) => sum + entry.amount, 0);
23
+ const wajibNetTotalIdr = wajibCreditTotalIdr - wajibDebitTotalIdr;
24
+ const monthlyWajibNominalIdr = policy?.monthlyWajibNominalIdr ?? 0;
25
+ const status = (0, SavingComplianceSnapshotService_1.deriveSavingComplianceStatus)(monthlyWajibNominalIdr, wajibNetTotalIdr, Boolean(policy));
26
+ const monthsInArrears = status === 'underpaid' ? 1 : 0;
27
+ const recommendedAction = status === 'compliant' ? 'none' : status === 'no-policy' ? 'set_active_saving_policy' : 'followup_wajib_payment';
28
+ return {
29
+ id: `snapshot-${member.id}`,
30
+ memberId: member.id,
31
+ koperasiId: scope.koperasiId,
32
+ tenantId: member.tenantId ?? null,
33
+ branchId: member.branchId ?? null,
34
+ referenceDate: (0, runtimeSupport_1.nowIso)(),
35
+ monthlyWajibNominalIdr,
36
+ wajibCreditTotalIdr,
37
+ wajibDebitTotalIdr,
38
+ wajibNetTotalIdr,
39
+ status,
40
+ monthsInArrears,
41
+ recommendedAction,
42
+ metadata: {
43
+ memberNumber: member.memberNumber,
44
+ },
45
+ version: 1,
46
+ lastModified: (0, runtimeSupport_1.nowIso)(),
47
+ deleted: false,
48
+ };
49
+ }
50
+ async listComplianceSnapshots(scope, options) {
51
+ const policy = await this.repos.savingPolicyRepository.getActive({
52
+ koperasiId: scope.koperasiId,
53
+ tenantId: scope.tenantId,
54
+ branchId: scope.branchId ?? null,
55
+ });
56
+ const members = await this.repos.memberRepository.list({
57
+ includeDeleted: options?.includeDeleted ?? false,
58
+ status: 'active',
59
+ tenantId: scope.tenantId,
60
+ branchId: scope.branchId ?? null,
61
+ });
62
+ const snapshots = await Promise.all(members
63
+ .filter((member) => (options?.memberId ? member.id === options.memberId : true))
64
+ .map((member) => this.buildSnapshot(member, scope, policy)));
65
+ return snapshots
66
+ .filter((snapshot) => (options?.status ? snapshot.status === options.status : true))
67
+ .map((snapshot) => (0, runtimeSupport_1.clone)(snapshot));
68
+ }
69
+ async getComplianceSnapshotByMember(memberId, scope) {
70
+ const snapshots = await this.listComplianceSnapshots(scope, { memberId });
71
+ return snapshots[0] ?? null;
72
+ }
73
+ }
74
+ exports.DbAdapterSavingComplianceSnapshotService = DbAdapterSavingComplianceSnapshotService;
@@ -0,0 +1,18 @@
1
+ import type { DbAdapter } from 'ofcore';
2
+ import type { SavingBalance, SavingEntryInput, SavingLedgerContract, SavingLedgerQueryOptions, SavingLedgerServiceContract } from '../../contracts/SavingLedgerContract';
3
+ import { type OfcoopRepositories } from '../../data/repositories';
4
+ import { type ServiceRuntimeOptions } from './runtimeSupport';
5
+ export declare class DbAdapterSavingLedgerService implements SavingLedgerServiceContract {
6
+ private readonly db;
7
+ private readonly repos;
8
+ private readonly options;
9
+ private readonly newId;
10
+ constructor(db: DbAdapter, repos: OfcoopRepositories, options?: ServiceRuntimeOptions);
11
+ private inTx;
12
+ private addEntry;
13
+ recordSavingCredit(input: SavingEntryInput): Promise<SavingLedgerContract>;
14
+ recordSavingDebit(input: SavingEntryInput): Promise<SavingLedgerContract>;
15
+ recordSavingWithdrawal(input: SavingEntryInput): Promise<SavingLedgerContract>;
16
+ listSavingEntriesByMember(memberId: string, options?: SavingLedgerQueryOptions): Promise<SavingLedgerContract[]>;
17
+ getSavingBalanceByMember(memberId: string): Promise<SavingBalance>;
18
+ }
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterSavingLedgerService = void 0;
4
+ const repositories_1 = require("../../data/repositories");
5
+ const errors_1 = require("../errors");
6
+ const SavingLedgerService_1 = require("../SavingLedgerService");
7
+ const runtimeSupport_1 = require("./runtimeSupport");
8
+ class DbAdapterSavingLedgerService {
9
+ constructor(db, repos, options = {}) {
10
+ this.db = db;
11
+ this.repos = repos;
12
+ this.options = options;
13
+ this.newId = (0, runtimeSupport_1.makeIdFactory)('saving-ledger');
14
+ }
15
+ async inTx(callback) {
16
+ return this.db.transaction(async (tx) => callback((0, repositories_1.createOfcoopRepositories)(tx)));
17
+ }
18
+ async addEntry(type, input) {
19
+ (0, SavingLedgerService_1.assertValidSavingEntryInput)(input);
20
+ return this.inTx(async (repos) => {
21
+ const member = await repos.memberRepository.getById(input.memberId);
22
+ if (!member || member.deleted)
23
+ throw (0, errors_1.notFound)('Member', input.memberId);
24
+ if (member.status !== 'active') {
25
+ throw (0, errors_1.conflict)(`Cannot record saving entry for inactive member: ${input.memberId}`);
26
+ }
27
+ if (input.referenceType && input.referenceId) {
28
+ const duplicate = await repos.savingLedgerRepository.findByReference({
29
+ memberId: input.memberId,
30
+ savingType: input.savingType,
31
+ entryType: type,
32
+ referenceType: input.referenceType,
33
+ referenceId: input.referenceId,
34
+ tenantId: input.tenantId ?? null,
35
+ branchId: input.branchId ?? null,
36
+ });
37
+ if (duplicate) {
38
+ this.options.logger?.logInfo('[ofcoop] saving entry idempotent hit', {
39
+ memberId: input.memberId,
40
+ referenceType: input.referenceType,
41
+ referenceId: input.referenceId,
42
+ });
43
+ return duplicate;
44
+ }
45
+ }
46
+ const created = await repos.savingLedgerRepository.create({
47
+ id: this.newId(),
48
+ memberId: input.memberId,
49
+ savingType: input.savingType,
50
+ entryType: type,
51
+ amount: input.amount,
52
+ transactionDate: input.transactionDate,
53
+ referenceType: input.referenceType ?? null,
54
+ referenceId: input.referenceId ?? null,
55
+ notes: input.notes ?? null,
56
+ branchId: input.branchId ?? null,
57
+ tenantId: input.tenantId ?? null,
58
+ version: 1,
59
+ lastModified: (0, runtimeSupport_1.nowIso)(),
60
+ deleted: false,
61
+ });
62
+ this.options.logger?.logInfo('[ofcoop] saving entry recorded', {
63
+ entryId: created.id,
64
+ memberId: created.memberId,
65
+ savingType: created.savingType,
66
+ entryType: created.entryType,
67
+ amount: created.amount,
68
+ });
69
+ await (0, runtimeSupport_1.emitDomainEvent)(this.options, {
70
+ name: 'saving.entry-recorded',
71
+ occurredAt: (0, runtimeSupport_1.nowIso)(),
72
+ entityId: created.id,
73
+ tenantId: created.tenantId ?? null,
74
+ branchId: created.branchId ?? null,
75
+ payload: {
76
+ memberId: created.memberId,
77
+ savingType: created.savingType,
78
+ entryType: created.entryType,
79
+ amount: created.amount,
80
+ },
81
+ });
82
+ return created;
83
+ });
84
+ }
85
+ async recordSavingCredit(input) {
86
+ return this.addEntry('credit', input);
87
+ }
88
+ async recordSavingDebit(input) {
89
+ return this.addEntry('debit', input);
90
+ }
91
+ async recordSavingWithdrawal(input) {
92
+ if (input.savingType !== 'sukarela') {
93
+ throw (0, errors_1.conflict)(`recordSavingWithdrawal only supports sukarela saving type, received: ${input.savingType}`);
94
+ }
95
+ const balance = await this.getSavingBalanceByMember(input.memberId);
96
+ if (input.amount > balance.sukarela) {
97
+ throw (0, errors_1.conflict)(`Insufficient sukarela balance for member: ${input.memberId}`);
98
+ }
99
+ return this.addEntry('debit', { ...input, notes: input.notes ?? 'withdrawal' });
100
+ }
101
+ async listSavingEntriesByMember(memberId, options) {
102
+ return this.repos.savingLedgerRepository.listByMember(memberId, options);
103
+ }
104
+ async getSavingBalanceByMember(memberId) {
105
+ const entries = await this.listSavingEntriesByMember(memberId);
106
+ const balance = { pokok: 0, wajib: 0, sukarela: 0, total: 0 };
107
+ for (const entry of entries) {
108
+ const sign = entry.entryType === 'credit' ? 1 : -1;
109
+ balance[entry.savingType] += sign * entry.amount;
110
+ }
111
+ balance.total = balance.pokok + balance.wajib + balance.sukarela;
112
+ return balance;
113
+ }
114
+ }
115
+ exports.DbAdapterSavingLedgerService = DbAdapterSavingLedgerService;
@@ -0,0 +1,14 @@
1
+ import type { SavingComplianceEvaluation, SavingComplianceWorklistItem, SavingPolicyContract, SavingPolicyScope, SavingPolicyServiceContract, UpsertSavingPolicyInput } from '../../contracts/SavingPolicyContract';
2
+ import type { OfcoopRepositories } from '../../data/repositories';
3
+ import { type ServiceRuntimeOptions } from './runtimeSupport';
4
+ export declare class DbAdapterSavingPolicyService implements SavingPolicyServiceContract {
5
+ private readonly repos;
6
+ private readonly options;
7
+ private readonly newId;
8
+ constructor(repos: OfcoopRepositories, options?: ServiceRuntimeOptions);
9
+ getActiveSavingPolicy(scope: SavingPolicyScope): Promise<SavingPolicyContract | null>;
10
+ upsertSavingPolicy(input: UpsertSavingPolicyInput): Promise<SavingPolicyContract>;
11
+ evaluateWajibCompliance(scope: SavingPolicyScope): Promise<SavingComplianceEvaluation>;
12
+ getComplianceWorklist(scope: SavingPolicyScope): Promise<SavingComplianceWorklistItem[]>;
13
+ private monthDiffInclusive;
14
+ }
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DbAdapterSavingPolicyService = void 0;
4
+ const SavingPolicyService_1 = require("../SavingPolicyService");
5
+ const runtimeSupport_1 = require("./runtimeSupport");
6
+ class DbAdapterSavingPolicyService {
7
+ constructor(repos, options = {}) {
8
+ this.repos = repos;
9
+ this.options = options;
10
+ this.newId = (0, runtimeSupport_1.makeIdFactory)('saving-policy');
11
+ }
12
+ async getActiveSavingPolicy(scope) {
13
+ return this.repos.savingPolicyRepository.getActive(scope);
14
+ }
15
+ async upsertSavingPolicy(input) {
16
+ (0, SavingPolicyService_1.assertValidSavingPolicyInput)(input);
17
+ const scope = { koperasiId: input.koperasiId, tenantId: input.tenantId, branchId: input.branchId };
18
+ const current = await this.repos.savingPolicyRepository.getActive(scope);
19
+ const now = (0, runtimeSupport_1.nowIso)();
20
+ if (current) {
21
+ const updated = await this.repos.savingPolicyRepository.upsert({
22
+ ...current,
23
+ ...input,
24
+ changeReason: input.changeReason ?? 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: 'saving.policy-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.savingPolicyRepository.upsert({
40
+ id: this.newId(),
41
+ ...input,
42
+ changeReason: input.changeReason ?? 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: 'saving.policy-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 evaluateWajibCompliance(scope) {
59
+ const policy = await this.getActiveSavingPolicy(scope);
60
+ const entries = await this.repos.savingLedgerRepository.listAll({
61
+ includeDeleted: false,
62
+ tenantId: scope.tenantId,
63
+ branchId: scope.branchId ?? null,
64
+ });
65
+ const wajibEntries = entries.filter((entry) => entry.savingType === 'wajib');
66
+ const totalWajibCredit = wajibEntries
67
+ .filter((entry) => entry.entryType === 'credit')
68
+ .reduce((sum, entry) => sum + entry.amount, 0);
69
+ const totalWajibDebit = wajibEntries
70
+ .filter((entry) => entry.entryType === 'debit')
71
+ .reduce((sum, entry) => sum + entry.amount, 0);
72
+ return {
73
+ summary: {
74
+ activePolicy: policy ? 1 : 0,
75
+ wajibCreditTotal: totalWajibCredit,
76
+ wajibDebitTotal: totalWajibDebit,
77
+ wajibNetTotal: (0, SavingPolicyService_1.calculateWajibNetTotal)(totalWajibCredit, totalWajibDebit),
78
+ },
79
+ referenceDate: (0, runtimeSupport_1.nowIso)(),
80
+ };
81
+ }
82
+ async getComplianceWorklist(scope) {
83
+ const policy = await this.getActiveSavingPolicy(scope);
84
+ if (!policy)
85
+ return [];
86
+ const members = await this.repos.memberRepository.list({
87
+ includeDeleted: false,
88
+ status: 'active',
89
+ tenantId: scope.tenantId,
90
+ branchId: scope.branchId ?? null,
91
+ });
92
+ const now = new Date();
93
+ return Promise.all(members.map(async (member) => {
94
+ const entries = await this.repos.savingLedgerRepository.listByMember(member.id, {
95
+ tenantId: scope.tenantId,
96
+ branchId: scope.branchId ?? null,
97
+ });
98
+ const wajibNet = entries.reduce((sum, entry) => {
99
+ if (entry.savingType !== 'wajib')
100
+ return sum;
101
+ return sum + (entry.entryType === 'credit' ? entry.amount : -entry.amount);
102
+ }, 0);
103
+ const monthsSinceJoin = Math.max(1, this.monthDiffInclusive(member.joinDate, now.toISOString().slice(0, 10)));
104
+ const expectedWajib = monthsSinceJoin * policy.monthlyWajibNominalIdr;
105
+ const arrearsAmount = Math.max(0, expectedWajib - wajibNet);
106
+ const monthsInArrears = policy.monthlyWajibNominalIdr > 0 ? Math.ceil(arrearsAmount / policy.monthlyWajibNominalIdr) : 0;
107
+ const daysInArrears = monthsInArrears * 30;
108
+ const priority = daysInArrears > 90 ? 'critical' : daysInArrears > 30 ? 'high' : daysInArrears > 7 ? 'medium' : 'low';
109
+ return {
110
+ id: `worklist-${member.id}`,
111
+ memberId: member.id,
112
+ priority,
113
+ reasonCode: 'WJ-REVIEW',
114
+ reasonText: `Review wajib compliance for member ${member.memberNumber}`,
115
+ recommendedAction: 'verify_monthly_wajib',
116
+ metadata: {
117
+ policyId: policy.id,
118
+ memberNumber: member.memberNumber,
119
+ fullName: member.fullName,
120
+ expectedWajib,
121
+ wajibNet,
122
+ arrearsAmount,
123
+ monthsInArrears,
124
+ daysInArrears,
125
+ },
126
+ };
127
+ }));
128
+ }
129
+ monthDiffInclusive(fromDate, toDate) {
130
+ const from = new Date(fromDate);
131
+ const to = new Date(toDate);
132
+ if (Number.isNaN(from.getTime()) || Number.isNaN(to.getTime()))
133
+ return 1;
134
+ const yearDiff = to.getUTCFullYear() - from.getUTCFullYear();
135
+ const monthDiff = to.getUTCMonth() - from.getUTCMonth();
136
+ return yearDiff * 12 + monthDiff + 1;
137
+ }
138
+ }
139
+ exports.DbAdapterSavingPolicyService = DbAdapterSavingPolicyService;
@@ -0,0 +1,12 @@
1
+ import type { ShuConfigContract, ShuConfigQueryOptions, ShuConfigScope, ShuConfigServiceContract, UpsertShuConfigInput } from '../../contracts/ShuConfigContract';
2
+ import type { OfcoopRepositories } from '../../data/repositories';
3
+ import { type ServiceRuntimeOptions } from './runtimeSupport';
4
+ export declare class DbAdapterShuConfigService implements ShuConfigServiceContract {
5
+ private readonly repos;
6
+ private readonly options;
7
+ private readonly newId;
8
+ constructor(repos: OfcoopRepositories, options?: ServiceRuntimeOptions);
9
+ getActiveShuConfig(scope: ShuConfigScope): Promise<ShuConfigContract | null>;
10
+ upsertShuConfig(input: UpsertShuConfigInput): Promise<ShuConfigContract>;
11
+ listShuConfigs(scope: ShuConfigScope, options?: ShuConfigQueryOptions): Promise<ShuConfigContract[]>;
12
+ }