@loka-sms/core-integration-be 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @loka-sms/core-integration-be
2
+
3
+ NestJS helper package untuk backend modul Loka SMS yang perlu mengakses data Core via service-to-service auth (`X-App-ID` + `X-App-Secret`).
4
+
5
+ Package ini cocok untuk **worker/background job/service sync**, bukan untuk forwarding token user.
6
+
7
+ ## Flow
8
+
9
+ ```text
10
+ Backend Modul / Worker / Cron
11
+ -> Gateway:3000/api/students
12
+ X-App-ID: <id>
13
+ X-App-Secret: <secret>
14
+ X-School-ID: <school-id>
15
+ -> Gateway forward ke be-loka-core:3002/api/v1/students
16
+ -> Core ServiceAuthGuard validasi credentials
17
+ ```
18
+
19
+ Semua request tetap lewat Gateway, bukan langsung ke Core.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm install @loka-sms/core-integration-be
25
+ ```
26
+
27
+ ## Environment Variable
28
+
29
+ | Variabel | Wajib | Fallback | Keterangan |
30
+ |---|---|---|---|
31
+ | `CORE_SERVICE_URL` | ✅ | — | Base URL Gateway, contoh: `http://localhost:3000/api` |
32
+ | `CORE_APP_ID` | ✅ | `APP_ID` | Application ID yang terdaftar di Core |
33
+ | `CORE_APP_SECRET` | ✅ | `APP_SECRET` | Application secret (plain, bukan bcrypt) |
34
+
35
+ Contoh `.env`:
36
+
37
+ ```env
38
+ CORE_SERVICE_URL=http://localhost:3000/api
39
+ CORE_APP_ID=erapor-app
40
+ CORE_APP_SECRET=erapor-secret
41
+ ```
42
+
43
+ ### URL Gateway
44
+
45
+ | Lingkungan | `CORE_SERVICE_URL` |
46
+ |---|---|
47
+ | Lokal (backend di host) | `http://localhost:3000/api` |
48
+ | Docker compose | `http://be-loka-gateway:3000/api` |
49
+ | Production | `<gateway-url>/api` (dari environment variable production) |
50
+
51
+ > **Catatan**: Jangan tambah `/api/v1` di URL jika lewat Gateway. Gateway sudah rewrite `/api` ke `/api/v1` secara otomatis.
52
+
53
+ ## Setup AppModule
54
+
55
+ ```ts
56
+ import { Module } from '@nestjs/common';
57
+ import { ConfigModule } from '@nestjs/config';
58
+ import { CoreIntegrationModule } from '@loka-sms/core-integration-be';
59
+
60
+ @Module({
61
+ imports: [
62
+ ConfigModule.forRoot({ isGlobal: true }),
63
+ CoreIntegrationModule,
64
+ ],
65
+ })
66
+ export class AppModule {}
67
+ ```
68
+
69
+ Atau dengan konfigurasi eksplisit:
70
+
71
+ ```ts
72
+ CoreIntegrationModule.forRoot({
73
+ baseUrl: 'http://localhost:3000/api',
74
+ appId: 'erapor-app',
75
+ appSecret: 'erapor-secret',
76
+ timeout: 10000,
77
+ })
78
+ ```
79
+
80
+ ## Cara Pakai
81
+
82
+ ```ts
83
+ import { Injectable } from '@nestjs/common';
84
+ import { CoreIntegrationService } from '@loka-sms/core-integration-be';
85
+
86
+ @Injectable()
87
+ export class RaporSyncService {
88
+ constructor(private readonly core: CoreIntegrationService) {}
89
+
90
+ async generate(schoolId: string) {
91
+ const students = await this.core.getStudents(schoolId, { status: 'active' });
92
+ const classes = await this.core.getClasses(schoolId);
93
+ return { students, classes };
94
+ }
95
+ }
96
+ ```
97
+
98
+ Package selalu mengirim `X-App-ID`, `X-App-Secret`, dan `X-School-ID`. Tidak perlu panggil method khusus untuk set auth.
99
+
100
+ ## Method Tersedia
101
+
102
+ | Method | Core Endpoint via Gateway |
103
+ |---|---|
104
+ | `getStudents(schoolId, params)` | `GET /api/students` |
105
+ | `getStudentsByClass(classId, schoolId)` | `GET /api/students?class_id=...&status=active` |
106
+ | `getStudent(studentId, schoolId)` | `GET /api/students/:id` |
107
+ | `findStudentByEmail(email, schoolId)` | `GET /api/students?q=...&limit=1` |
108
+ | `getParentChildrenByUserId(parentUserId, schoolId)` | `GET /api/parents/by-user/:id/children` |
109
+ | `getParentChildIdsByUserId(parentUserId, schoolId)` | `GET /api/parents/by-user/:id/children` |
110
+ | `resolveParentStudentScope(parentUserId, schoolId, requestedStudentId?)` | `GET /api/parents/by-user/:id/children` |
111
+ | `getTeachers(schoolId)` | `GET /api/teachers` |
112
+ | `getTeacher(teacherId, schoolId)` | `GET /api/teachers/:id` |
113
+ | `getClasses(schoolId)` | `GET /api/classes` |
114
+ | `getSubjects(schoolId, params)` | `GET /api/subjects` |
115
+ | `getCurriculums(schoolId, params)` | `GET /api/curriculum` |
116
+ | `getAcademicCalendar(schoolId, params)` | `GET /api/academic-calendar` |
117
+ | `getRooms(schoolId, params)` | `GET /api/rooms` |
118
+ | `getKKM(subjectId, gradeLevel, schoolId)` | `GET /api/kkm` |
119
+ | `pushGradeToCore(schoolId, gradeData)` | `POST /api/grades/sync` |
120
+ | `createNotification(schoolId, payload)` | `POST /api/notifications` |
121
+
122
+ ## Generic Request
123
+
124
+ Kalau method wrapper belum tersedia:
125
+
126
+ ```ts
127
+ const data = await this.core.get('teaching-schedule', schoolId, {
128
+ params: { class_id: classId },
129
+ });
130
+
131
+ await this.core.post('notifications', schoolId, {
132
+ user_id: 'user_xxx',
133
+ type: 'rapor_ready',
134
+ title: 'Rapor siap dicek',
135
+ message: 'Nilai akhir sudah siap.',
136
+ });
137
+ ```
138
+
139
+ Path tidak perlu diawali `/api` atau `/api/v1`.
140
+
141
+ Benar:
142
+ ```ts
143
+ this.core.get('students', schoolId)
144
+ ```
145
+
146
+ Salah:
147
+ ```ts
148
+ this.core.get('api/students', schoolId)
149
+ this.core.get('api/v1/students', schoolId)
150
+ ```
151
+
152
+ ## Data Per Modul
153
+
154
+ | Modul | Ambil Dari Core | Jangan Ambil Dari Core |
155
+ |---|---|---|
156
+ | LMS | siswa, guru, kelas, mapel, KKM, kalender | nilai berjalan (milik LMS) |
157
+ | E-Rapor | siswa, kelas, guru, mapel, kurikulum, KKM, semester | nilai mentah tugas/ujian; ambil nilai final dari LMS |
158
+ | Presensi | siswa, kelas, guru, kalender, jadwal | hasil presensi operasional |
159
+ | Keuangan | siswa, orang tua, kelas | transaksi, invoice, pembayaran |
160
+ | PPDB | referensi sekolah | data pendaftar sebelum resmi jadi siswa |
161
+ | HRIS | guru, staff, struktur sekolah | payroll, cuti, KPI internal |
162
+
163
+ ## Prasyarat Core
164
+
165
+ Setiap `APP_ID` harus terdaftar di tabel `loka_sms.applications` dengan secret di-hash bcrypt.
166
+
167
+ Contoh registrasi:
168
+
169
+ ```sql
170
+ INSERT INTO loka_sms.applications (app_id, app_secret, name, service_url, status)
171
+ VALUES ('erapor-app', '<bcrypt-hash-dari-erapor-secret>', 'E-Rapor Service', 'http://erapor:3012', 'active');
172
+ ```
173
+
174
+ Endpoint Core yang akan dipanggil juga harus memiliki decorator `@AllowServiceAuth()`. Sebagian besar endpoint sudah.
175
+
176
+ ## Troubleshooting
177
+
178
+ ### Error: CoreIntegrationModule requires baseUrl or CORE_SERVICE_URL
179
+
180
+ Set env:
181
+ ```env
182
+ CORE_SERVICE_URL=http://localhost:3000/api
183
+ ```
184
+
185
+ ### Error: CoreIntegrationModule requires appId or CORE_APP_ID, APP_ID
186
+
187
+ Set env:
188
+ ```env
189
+ CORE_APP_ID=erapor-app
190
+ CORE_APP_SECRET=erapor-secret
191
+ ```
192
+
193
+ ### Error 401 / 403 dari Core
194
+
195
+ Cek:
196
+ - `APP_ID` dan `APP_SECRET` sudah terdaftar di tabel `loka_sms.applications`
197
+ - Secret di database adalah bcrypt hash
198
+ - Endpoint Core memiliki `@AllowServiceAuth()`
199
+ - Status aplikasi di database adalah `active`
200
+
201
+ ### Error 404
202
+
203
+ Cek path. Jangan tambah `/api` atau `/api/v1` di method.
204
+
205
+ ### Data kosong
206
+
207
+ Cek `schoolId` yang dikirim. Pastikan `X-School-ID` benar.
208
+
209
+ ## Catatan
210
+
211
+ - Package ini untuk service-to-service / worker mode (tanpa user JWT).
212
+ - Semua request lewat Gateway, bukan langsung ke Core.
213
+ - Endpoint Core tetap melakukan validasi credentials dan permission.
214
+ - Untuk menambah `APP_ID` baru, tim Core harus insert record di tabel `applications` dengan secret bcrypt.
@@ -0,0 +1,5 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+ import { CoreIntegrationConfig } from './types';
3
+ export declare class CoreIntegrationModule {
4
+ static forRoot(config?: CoreIntegrationConfig): DynamicModule;
5
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var CoreIntegrationModule_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.CoreIntegrationModule = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ const axios_1 = require("@nestjs/axios");
13
+ const core_integration_service_1 = require("./core-integration.service");
14
+ const types_1 = require("./types");
15
+ let CoreIntegrationModule = CoreIntegrationModule_1 = class CoreIntegrationModule {
16
+ static forRoot(config = {}) {
17
+ return {
18
+ module: CoreIntegrationModule_1,
19
+ imports: [
20
+ axios_1.HttpModule.register({
21
+ timeout: config.timeout ?? 5000,
22
+ }),
23
+ ],
24
+ providers: [
25
+ { provide: types_1.CORE_INTEGRATION_OPTIONS, useValue: config },
26
+ core_integration_service_1.CoreIntegrationService,
27
+ ],
28
+ exports: [core_integration_service_1.CoreIntegrationService],
29
+ };
30
+ }
31
+ };
32
+ exports.CoreIntegrationModule = CoreIntegrationModule;
33
+ exports.CoreIntegrationModule = CoreIntegrationModule = CoreIntegrationModule_1 = __decorate([
34
+ (0, common_1.Module)({
35
+ imports: [axios_1.HttpModule],
36
+ providers: [
37
+ { provide: types_1.CORE_INTEGRATION_OPTIONS, useValue: {} },
38
+ core_integration_service_1.CoreIntegrationService,
39
+ ],
40
+ exports: [core_integration_service_1.CoreIntegrationService],
41
+ })
42
+ ], CoreIntegrationModule);
@@ -0,0 +1,39 @@
1
+ import { HttpService } from '@nestjs/axios';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { CoreIntegrationConfig, CoreNotificationPayload, CoreRequestOptions } from './types';
4
+ export declare class CoreIntegrationService {
5
+ private readonly httpService;
6
+ private readonly configService?;
7
+ private readonly options;
8
+ private readonly logger;
9
+ private readonly baseUrl;
10
+ private readonly appId;
11
+ private readonly appSecret;
12
+ constructor(httpService: HttpService, configService?: ConfigService | undefined, options?: CoreIntegrationConfig);
13
+ private resolveRequired;
14
+ private getHeaders;
15
+ private makeUrl;
16
+ private extractStudentId;
17
+ get<T = any>(path: string, schoolId?: string, options?: CoreRequestOptions): Promise<T>;
18
+ post<T = any>(path: string, schoolId: string | undefined, body?: unknown, options?: CoreRequestOptions): Promise<T>;
19
+ put<T = any>(path: string, schoolId: string | undefined, body?: unknown, options?: CoreRequestOptions): Promise<T>;
20
+ patch<T = any>(path: string, schoolId: string | undefined, body?: unknown, options?: CoreRequestOptions): Promise<T>;
21
+ delete<T = any>(path: string, schoolId?: string, options?: CoreRequestOptions): Promise<T>;
22
+ getParentChildrenByUserId(parentUserId: string, schoolId: string): Promise<any[]>;
23
+ getParentChildIdsByUserId(parentUserId: string, schoolId: string): Promise<string[]>;
24
+ resolveParentStudentScope(parentUserId: string, schoolId: string, requestedStudentId?: string): Promise<string[]>;
25
+ getStudents(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
26
+ getStudentsByClass(classId: string, schoolId: string): Promise<any[]>;
27
+ getStudent(studentId: string, schoolId: string): Promise<any>;
28
+ findStudentByEmail(email: string, schoolId: string): Promise<any>;
29
+ getTeacher(teacherId: string, schoolId: string): Promise<any>;
30
+ getClasses(schoolId: string): Promise<any[]>;
31
+ getTeachers(schoolId: string): Promise<any[]>;
32
+ getSubjects(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
33
+ getCurriculums(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
34
+ getAcademicCalendar(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
35
+ getRooms(schoolId: string, params?: Record<string, unknown>): Promise<any[]>;
36
+ pushGradeToCore(schoolId: string, gradeData: Record<string, unknown>): Promise<any>;
37
+ getKKM(subjectId: string, gradeLevel: number, schoolId: string): Promise<number | null>;
38
+ createNotification(schoolId: string, data: CoreNotificationPayload): Promise<any>;
39
+ }
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ var CoreIntegrationService_1;
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.CoreIntegrationService = void 0;
17
+ const common_1 = require("@nestjs/common");
18
+ const axios_1 = require("@nestjs/axios");
19
+ const config_1 = require("@nestjs/config");
20
+ const rxjs_1 = require("rxjs");
21
+ const types_1 = require("./types");
22
+ let CoreIntegrationService = CoreIntegrationService_1 = class CoreIntegrationService {
23
+ constructor(httpService, configService, options = {}) {
24
+ this.httpService = httpService;
25
+ this.configService = configService;
26
+ this.options = options;
27
+ this.logger = new common_1.Logger(CoreIntegrationService_1.name);
28
+ this.baseUrl = this.resolveRequired('baseUrl', 'CORE_SERVICE_URL');
29
+ this.appId = this.resolveRequired('appId', 'CORE_APP_ID', 'APP_ID');
30
+ this.appSecret = this.resolveRequired('appSecret', 'CORE_APP_SECRET', 'APP_SECRET');
31
+ }
32
+ resolveRequired(optionKey, ...envKeys) {
33
+ const fromOption = this.options[optionKey];
34
+ if (fromOption && typeof fromOption === 'string') {
35
+ return optionKey === 'baseUrl' ? fromOption.replace(/\/+$/, '') : fromOption;
36
+ }
37
+ for (const key of envKeys) {
38
+ const fromConfig = this.configService?.get(key);
39
+ if (fromConfig)
40
+ return optionKey === 'baseUrl' ? fromConfig.replace(/\/+$/, '') : fromConfig;
41
+ const fromEnv = process.env[key];
42
+ if (fromEnv)
43
+ return optionKey === 'baseUrl' ? fromEnv.replace(/\/+$/, '') : fromEnv;
44
+ }
45
+ throw new Error(`CoreIntegrationModule requires ${optionKey} or one of: ${envKeys.join(', ')}`);
46
+ }
47
+ getHeaders(schoolId, extraHeaders) {
48
+ return {
49
+ 'X-App-ID': this.appId,
50
+ 'X-App-Secret': this.appSecret,
51
+ 'Content-Type': 'application/json',
52
+ ...(schoolId ? { 'X-School-ID': schoolId } : {}),
53
+ ...(extraHeaders ?? {}),
54
+ };
55
+ }
56
+ makeUrl(path) {
57
+ return `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
58
+ }
59
+ extractStudentId(child) {
60
+ return child?.student_id || child?.student?.id || child?.studentUserId || child?.id || null;
61
+ }
62
+ async get(path, schoolId, options = {}) {
63
+ const { headers, ...requestOptions } = options;
64
+ const res = await (0, rxjs_1.firstValueFrom)(this.httpService.get(this.makeUrl(path), {
65
+ ...requestOptions,
66
+ headers: this.getHeaders(schoolId, headers),
67
+ }));
68
+ return res.data;
69
+ }
70
+ async post(path, schoolId, body, options = {}) {
71
+ const { headers, ...requestOptions } = options;
72
+ const res = await (0, rxjs_1.firstValueFrom)(this.httpService.post(this.makeUrl(path), body, {
73
+ ...requestOptions,
74
+ headers: this.getHeaders(schoolId, headers),
75
+ }));
76
+ return res.data;
77
+ }
78
+ async put(path, schoolId, body, options = {}) {
79
+ const { headers, ...requestOptions } = options;
80
+ const res = await (0, rxjs_1.firstValueFrom)(this.httpService.put(this.makeUrl(path), body, {
81
+ ...requestOptions,
82
+ headers: this.getHeaders(schoolId, headers),
83
+ }));
84
+ return res.data;
85
+ }
86
+ async patch(path, schoolId, body, options = {}) {
87
+ const { headers, ...requestOptions } = options;
88
+ const res = await (0, rxjs_1.firstValueFrom)(this.httpService.patch(this.makeUrl(path), body, {
89
+ ...requestOptions,
90
+ headers: this.getHeaders(schoolId, headers),
91
+ }));
92
+ return res.data;
93
+ }
94
+ async delete(path, schoolId, options = {}) {
95
+ const { headers, ...requestOptions } = options;
96
+ const res = await (0, rxjs_1.firstValueFrom)(this.httpService.delete(this.makeUrl(path), {
97
+ ...requestOptions,
98
+ headers: this.getHeaders(schoolId, headers),
99
+ }));
100
+ return res.data;
101
+ }
102
+ async getParentChildrenByUserId(parentUserId, schoolId) {
103
+ try {
104
+ const data = await this.get(`parents/by-user/${parentUserId}/children`, schoolId);
105
+ return data?.children || data?.data?.children || [];
106
+ }
107
+ catch (error) {
108
+ if (error.response?.status === 404) {
109
+ return [];
110
+ }
111
+ this.logger.error(`Failed to fetch parent children: ${error.message}`);
112
+ throw new Error('Core service unavailable: unable to fetch parent children');
113
+ }
114
+ }
115
+ async getParentChildIdsByUserId(parentUserId, schoolId) {
116
+ const children = await this.getParentChildrenByUserId(parentUserId, schoolId);
117
+ return children
118
+ .map((child) => this.extractStudentId(child))
119
+ .filter((studentId) => !!studentId);
120
+ }
121
+ async resolveParentStudentScope(parentUserId, schoolId, requestedStudentId) {
122
+ const childIds = await this.getParentChildIdsByUserId(parentUserId, schoolId);
123
+ if (childIds.length === 0) {
124
+ throw new common_1.ForbiddenException('Parent has no linked children in this school');
125
+ }
126
+ if (!requestedStudentId) {
127
+ return childIds;
128
+ }
129
+ if (!childIds.includes(requestedStudentId)) {
130
+ throw new common_1.ForbiddenException('Child is not linked to this parent');
131
+ }
132
+ return [requestedStudentId];
133
+ }
134
+ async getStudents(schoolId, params = {}) {
135
+ try {
136
+ const data = await this.get('students', schoolId, { params });
137
+ return data?.data || data?.students || data || [];
138
+ }
139
+ catch (error) {
140
+ if (error.response?.status === 404) {
141
+ return [];
142
+ }
143
+ this.logger.error(`Failed to fetch students: ${error.message}`);
144
+ throw new Error('Core service unavailable: unable to fetch students');
145
+ }
146
+ }
147
+ async getStudentsByClass(classId, schoolId) {
148
+ return this.getStudents(schoolId, { class_id: classId, status: 'active' });
149
+ }
150
+ async getStudent(studentId, schoolId) {
151
+ try {
152
+ return await this.get(`students/${studentId}`, schoolId);
153
+ }
154
+ catch (error) {
155
+ this.logger.warn(`Failed to fetch student ${studentId}: ${error.message}`);
156
+ return null;
157
+ }
158
+ }
159
+ async findStudentByEmail(email, schoolId) {
160
+ try {
161
+ const data = await this.get('students', schoolId, { params: { q: email, limit: 1 } });
162
+ const list = data?.data || data?.students || data || [];
163
+ return Array.isArray(list) && list.length > 0 ? list[0] : null;
164
+ }
165
+ catch (error) {
166
+ this.logger.warn(`Failed to find student by email ${email}: ${error.message}`);
167
+ return null;
168
+ }
169
+ }
170
+ async getTeacher(teacherId, schoolId) {
171
+ try {
172
+ const data = await this.get(`teachers/${teacherId}`, schoolId);
173
+ return data?.data || data;
174
+ }
175
+ catch (error) {
176
+ this.logger.error(`Failed to fetch teacher: ${error.message}`);
177
+ return null;
178
+ }
179
+ }
180
+ async getClasses(schoolId) {
181
+ try {
182
+ const data = await this.get('classes', schoolId);
183
+ return data?.data || data || [];
184
+ }
185
+ catch (error) {
186
+ this.logger.error(`Failed to fetch classes: ${error.message}`);
187
+ throw new Error('Core service unavailable: unable to fetch classes');
188
+ }
189
+ }
190
+ async getTeachers(schoolId) {
191
+ try {
192
+ const data = await this.get('teachers', schoolId);
193
+ const body = data?.data || data || {};
194
+ return body?.data || body || [];
195
+ }
196
+ catch (error) {
197
+ this.logger.error(`Failed to fetch teachers: ${error.message}`);
198
+ throw new Error('Core service unavailable: unable to fetch teachers');
199
+ }
200
+ }
201
+ async getSubjects(schoolId, params = {}) {
202
+ const data = await this.get('subjects', schoolId, { params });
203
+ return data?.data || data || [];
204
+ }
205
+ async getCurriculums(schoolId, params = {}) {
206
+ const data = await this.get('curriculum', schoolId, { params });
207
+ return data?.data || data || [];
208
+ }
209
+ async getAcademicCalendar(schoolId, params = {}) {
210
+ const data = await this.get('academic-calendar', schoolId, { params });
211
+ return data?.data || data || [];
212
+ }
213
+ async getRooms(schoolId, params = {}) {
214
+ const data = await this.get('rooms', schoolId, { params });
215
+ return data?.data || data || [];
216
+ }
217
+ async pushGradeToCore(schoolId, gradeData) {
218
+ try {
219
+ return await this.post('grades/sync', schoolId, gradeData);
220
+ }
221
+ catch (error) {
222
+ this.logger.error(`Failed to push grade to core: ${error.message}`);
223
+ throw error;
224
+ }
225
+ }
226
+ async getKKM(subjectId, gradeLevel, schoolId) {
227
+ try {
228
+ const data = await this.get('kkm', schoolId, {
229
+ params: { subject_id: subjectId, grade_level: gradeLevel },
230
+ });
231
+ const list = data?.data || data || [];
232
+ const found = Array.isArray(list) ? list.find((k) => k.subject_id === subjectId) : null;
233
+ return found?.kkm_score ?? null;
234
+ }
235
+ catch (error) {
236
+ this.logger.warn(`Failed to fetch KKM: ${error.message}`);
237
+ return null;
238
+ }
239
+ }
240
+ async createNotification(schoolId, data) {
241
+ try {
242
+ return await this.post('notifications', schoolId, data);
243
+ }
244
+ catch (error) {
245
+ this.logger.warn(`Failed to create notification: ${error.message}`);
246
+ return null;
247
+ }
248
+ }
249
+ };
250
+ exports.CoreIntegrationService = CoreIntegrationService;
251
+ exports.CoreIntegrationService = CoreIntegrationService = CoreIntegrationService_1 = __decorate([
252
+ (0, common_1.Injectable)(),
253
+ __param(1, (0, common_1.Optional)()),
254
+ __param(2, (0, common_1.Optional)()),
255
+ __param(2, (0, common_1.Inject)(types_1.CORE_INTEGRATION_OPTIONS)),
256
+ __metadata("design:paramtypes", [axios_1.HttpService,
257
+ config_1.ConfigService, Object])
258
+ ], CoreIntegrationService);
@@ -0,0 +1,3 @@
1
+ export * from './core-integration.module';
2
+ export * from './core-integration.service';
3
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./core-integration.module"), exports);
18
+ __exportStar(require("./core-integration.service"), exports);
19
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,22 @@
1
+ export interface CoreIntegrationConfig {
2
+ baseUrl?: string;
3
+ appId?: string;
4
+ appSecret?: string;
5
+ timeout?: number;
6
+ }
7
+ export interface CoreRequestOptions {
8
+ params?: Record<string, unknown>;
9
+ headers?: Record<string, string>;
10
+ timeout?: number;
11
+ [key: string]: unknown;
12
+ }
13
+ export interface CoreNotificationPayload {
14
+ user_id: string;
15
+ type: string;
16
+ title: string;
17
+ message: string;
18
+ reference?: string;
19
+ reference_id?: string;
20
+ action_url?: string;
21
+ }
22
+ export declare const CORE_INTEGRATION_OPTIONS = "CORE_INTEGRATION_OPTIONS";
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CORE_INTEGRATION_OPTIONS = void 0;
4
+ exports.CORE_INTEGRATION_OPTIONS = 'CORE_INTEGRATION_OPTIONS';
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@loka-sms/core-integration-be",
3
+ "version": "0.0.1",
4
+ "description": "Internal NestJS client for service-to-service access to Loka SMS Core APIs",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "files": [
10
+ "dist/",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "clean": "rimraf dist",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "peerDependencies": {
19
+ "@nestjs/axios": "^4.0.0",
20
+ "@nestjs/common": "^11.0.0",
21
+ "@nestjs/config": "^4.0.0",
22
+ "rxjs": "^7.8.0"
23
+ },
24
+ "devDependencies": {
25
+ "@nestjs/axios": "^4.0.1",
26
+ "@nestjs/common": "^11.1.19",
27
+ "@nestjs/config": "^4.0.4",
28
+ "@types/node": "^22.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "rxjs": "^7.8.1",
31
+ "typescript": "^5.7.3"
32
+ },
33
+ "keywords": [
34
+ "loka",
35
+ "sms",
36
+ "core",
37
+ "nestjs",
38
+ "integration"
39
+ ]
40
+ }