malawi-curriculum-api 1.0.4 → 1.0.5

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 CHANGED
@@ -15,20 +15,89 @@ The official SDK for accessing the Malawi Curriculum API.
15
15
  npm install malawi-curriculum-api
16
16
  ```
17
17
 
18
- ## Quick Start
18
+ ### Authentication
19
+
20
+ Initialize the client with your API Key and optional Firebase ID Token for user-specific access (subscriptions/purchases).
19
21
 
20
22
  ```javascript
21
23
  import { MalawiCurriculumClient } from 'malawi-curriculum-api';
22
24
 
23
- const client = new MalawiCurriculumClient({
24
- apiKey: 'YOUR_API_KEY'
25
+ const client = new MalawiCurriculumClient({
26
+ apiKey: 'YOUR_API_KEY',
27
+ firebaseToken: 'USER_FIREBASE_ID_TOKEN' // Optional: for accessing paid resources
25
28
  });
26
29
 
27
- const resources = await client.getResources({
28
- level: 'MSCE',
29
- subject: 'Mathematics',
30
- type: 'past_paper'
30
+ // Update token later (e.g., on user login/refresh)
31
+ client.setFirebaseToken('NEW_TOKEN');
32
+ ```
33
+
34
+ ### Search Resources
35
+
36
+ Search with powerful filtering options. Access to filters depends on your API Key plan tier.
37
+
38
+ ```javascript
39
+ const results = await client.search({
40
+ q: 'mathematics',
41
+ level: 'MSCE', // Basic+ Plan
42
+ year: 2023, // Pro+ Plan
43
+ type: 'past_paper', // Pro+ Plan
44
+ limit: 10
45
+ });
46
+ ```
47
+
48
+ ### Secure Downloads
49
+
50
+ Request a secure, short-lived download URL. This flow enforces entitlements (subscriptions/purchases).
51
+
52
+ ```javascript
53
+ try {
54
+ // 1. Get signed URL (handles payment checks automatically)
55
+ const url = await client.download(123, 'download');
56
+
57
+ // 2. Open or download
58
+ window.location.href = url;
59
+ } catch (error) {
60
+ if (error.message.includes('402')) {
61
+ console.error('Payment Required');
62
+ }
63
+ }
64
+ ```
65
+
66
+ ### Payments
67
+
68
+ Initiate mobile money payments for subscriptions or resource purchases.
69
+
70
+ #### Developer Subscription (Pay Platform)
71
+ ```javascript
72
+ const response = await client.initiatePayment({
73
+ type: 'subscription',
74
+ tier: 'pro',
75
+ mobile: '26599123456',
76
+ operator: 'AIRTEL', // or 'TNM'
77
+ email: 'dev@example.com'
31
78
  });
79
+ console.log('Charge ID:', response.charge_id);
80
+ ```
81
+
82
+ #### Purchase Resource (User pays Developer)
83
+ ```javascript
84
+ const response = await client.initiatePayment({
85
+ type: 'purchase',
86
+ developerId: 'DEV_UUID',
87
+ resourceId: 123,
88
+ amount: 500,
89
+ mobile: '26588123456',
90
+ operator: 'TNM',
91
+ email: 'user@example.com'
92
+ });
93
+ ```
94
+
95
+ #### Verify Payment
96
+ ```javascript
97
+ const status = await client.verifyPayment(chargeId);
98
+ if (status.data.status === 'successful') {
99
+ console.log('Payment Successful');
100
+ }
32
101
  ```
33
102
 
34
103
  ## API Overview
@@ -328,6 +397,107 @@ When a resource has been priced (not free), download requests return `402 Paymen
328
397
 
329
398
  ---
330
399
 
400
+ ### Related Resources
401
+
402
+ Get resources similar to a given resource (same subject/level), prioritising the same year.
403
+
404
+ ```javascript
405
+ const related = await client.getRelatedResources(101);
406
+ console.log(related);
407
+ // Up to 5 resources from the same subject and level
408
+ ```
409
+
410
+ **Request:**
411
+ - Method: `GET`
412
+ - Endpoint: `/resources/{id}/related`
413
+ - Headers: `Authorization: Bearer API_KEY`
414
+
415
+ **Response:**
416
+ ```json
417
+ {
418
+ "success": true,
419
+ "data": [
420
+ {
421
+ "id": 102,
422
+ "title": "MSCE Mathematics Paper 2 2023",
423
+ "type": "past_paper",
424
+ "year": 2023,
425
+ "subject": "Mathematics",
426
+ "level": "MSCE",
427
+ "price_mwk": 500,
428
+ "is_free": false
429
+ }
430
+ ]
431
+ }
432
+ ```
433
+
434
+ ---
435
+
436
+ ### Bookmarks
437
+
438
+ Save resources for quick access later.
439
+
440
+ #### List Bookmarks
441
+
442
+ ```javascript
443
+ const bookmarks = await client.getBookmarks({ limit: 20 });
444
+ ```
445
+
446
+ **Request:**
447
+ - Method: `GET`
448
+ - Endpoint: `/bookmarks`
449
+ - Headers: `Authorization: Bearer API_KEY`
450
+ - Query Parameters:
451
+ - `limit` (optional): Max results (1-100, default: 50)
452
+ - `offset` (optional): Pagination offset
453
+
454
+ **Response:**
455
+ ```json
456
+ {
457
+ "success": true,
458
+ "count": 1,
459
+ "data": [
460
+ {
461
+ "id": 1,
462
+ "resource_id": 101,
463
+ "title": "MSCE Mathematics Paper 1 2023",
464
+ "type": "past_paper",
465
+ "year": 2023,
466
+ "subject": "Mathematics",
467
+ "level": "MSCE",
468
+ "price_mwk": 500,
469
+ "is_free": false,
470
+ "bookmarked_at": "2026-02-14T00:00:00.000Z"
471
+ }
472
+ ]
473
+ }
474
+ ```
475
+
476
+ #### Add Bookmark
477
+
478
+ ```javascript
479
+ const bookmark = await client.addBookmark(101);
480
+ console.log(bookmark);
481
+ // { id: 1, resource_id: 101, created_at: "..." }
482
+ ```
483
+
484
+ **Request:**
485
+ - Method: `POST`
486
+ - Endpoint: `/bookmarks`
487
+ - Body: `{ "resource_id": 101 }`
488
+
489
+ #### Remove Bookmark
490
+
491
+ ```javascript
492
+ await client.removeBookmark(101);
493
+ ```
494
+
495
+ **Request:**
496
+ - Method: `DELETE`
497
+ - Endpoint: `/bookmarks/{resourceId}`
498
+
499
+ ---
500
+
331
501
  ### Search Resources
332
502
 
333
503
  Search across all curriculum resources. Results and filter capabilities depend on your plan tier.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "malawi-curriculum-api",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Official JavaScript SDK for the Malawi Curriculum API",
5
5
  "main": "src/MalawiCurriculumClient.js",
6
6
  "type": "module",
@@ -26,13 +26,23 @@ export class MalawiCurriculumClient {
26
26
  /**
27
27
  * @param {Object} config
28
28
  * @param {string} config.apiKey - Your API Key
29
- * @param {string} [config.baseUrl] - API Base URL (default: https://malawi-curricular-api-production.up.railway.app/api/v1)
29
+ * @param {string} [config.baseUrl] - API Base URL
30
+ * @param {string} [config.firebaseToken] - Firebase ID Token (for paid resource access)
30
31
  */
31
- constructor({ apiKey, baseUrl }) {
32
+ constructor({ apiKey, baseUrl, firebaseToken }) {
32
33
  if (!apiKey) throw new Error('API Key is required');
33
34
 
34
35
  this.apiKey = apiKey;
35
36
  this.baseUrl = baseUrl || 'https://malawi-curricular-api-production.up.railway.app/api/v1';
37
+ this.firebaseToken = firebaseToken;
38
+ }
39
+
40
+ /**
41
+ * Set or update the Firebase ID Token
42
+ * @param {string} token
43
+ */
44
+ setFirebaseToken(token) {
45
+ this.firebaseToken = token;
36
46
  }
37
47
 
38
48
  /**
@@ -49,14 +59,17 @@ export class MalawiCurriculumClient {
49
59
  });
50
60
 
51
61
  const url = `${this.baseUrl}${endpoint}?${params.toString()}`;
62
+ const headers = {
63
+ 'Authorization': `Bearer ${this.apiKey}`,
64
+ 'Content-Type': 'application/json'
65
+ };
66
+
67
+ if (this.firebaseToken) {
68
+ headers['X-Firebase-Token'] = this.firebaseToken;
69
+ }
52
70
 
53
71
  try {
54
- const response = await fetch(url, {
55
- headers: {
56
- 'Authorization': `Bearer ${this.apiKey}`,
57
- 'Content-Type': 'application/json'
58
- }
59
- });
72
+ const response = await fetch(url, { headers });
60
73
 
61
74
  if (!response.ok) {
62
75
  const errorText = await response.text();
@@ -77,14 +90,19 @@ export class MalawiCurriculumClient {
77
90
  */
78
91
  async _post(endpoint, body = {}) {
79
92
  const url = `${this.baseUrl}${endpoint}`;
93
+ const headers = {
94
+ 'Authorization': `Bearer ${this.apiKey}`,
95
+ 'Content-Type': 'application/json'
96
+ };
97
+
98
+ if (this.firebaseToken) {
99
+ headers['X-Firebase-Token'] = this.firebaseToken;
100
+ }
80
101
 
81
102
  try {
82
103
  const response = await fetch(url, {
83
104
  method: 'POST',
84
- headers: {
85
- 'Authorization': `Bearer ${this.apiKey}`,
86
- 'Content-Type': 'application/json'
87
- },
105
+ headers,
88
106
  body: JSON.stringify(body)
89
107
  });
90
108
 
@@ -184,8 +202,11 @@ export class MalawiCurriculumClient {
184
202
  */
185
203
  async downloadResource(id) {
186
204
  console.warn('[DEPRECATED] downloadResource() is deprecated. Use download() for the secure token flow.');
187
- const response = await this._request(`/resources/${id}/download`);
188
- return response.download_url;
205
+ // This old endpoint is gone/legacy, but for SDK compat we redirect to new flow if possible or just fail?
206
+ // Since strict mode is on, we should probably advise new flow.
207
+ // But let's try to map it to new flow if possible?
208
+ // "downloadResource(id)" implies purpose=download.
209
+ return this.download(id, 'download');
189
210
  }
190
211
 
191
212
  /**
@@ -210,4 +231,104 @@ export class MalawiCurriculumClient {
210
231
  const response = await this._request(`/pricing/${resourceId}`);
211
232
  return response.data;
212
233
  }
234
+
235
+ /**
236
+ * Helper to make authenticated DELETE requests
237
+ * @private
238
+ */
239
+ async _delete(endpoint) {
240
+ const url = `${this.baseUrl}${endpoint}`;
241
+ const headers = {
242
+ 'Authorization': `Bearer ${this.apiKey}`,
243
+ 'Content-Type': 'application/json'
244
+ };
245
+
246
+ if (this.firebaseToken) {
247
+ headers['X-Firebase-Token'] = this.firebaseToken;
248
+ }
249
+
250
+ try {
251
+ const response = await fetch(url, {
252
+ method: 'DELETE',
253
+ headers
254
+ });
255
+
256
+ if (!response.ok) {
257
+ const errorText = await response.text();
258
+ throw new Error(`API Error ${response.status}: ${errorText}`);
259
+ }
260
+
261
+ return await response.json();
262
+ } catch (error) {
263
+ if (error.cause) console.error('Network Error Cause:', error.cause);
264
+ throw error;
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Initiate a payment (Subscription or Purchase).
270
+ * @param {Object} data - Payment details
271
+ * @param {'subscription'|'purchase'} data.type - Payment type
272
+ * @param {string} data.mobile - Mobile number
273
+ * @param {'TNM'|'AIRTEL'} data.operator - Mobile operator
274
+ * @param {string} data.email - Email address
275
+ * @param {string} [data.tier] - Subscription tier (for type='subscription')
276
+ * @param {string} [data.developerId] - Developer ID (for type='purchase')
277
+ * @param {number} [data.resourceId] - Resource ID (for type='purchase')
278
+ * @param {number} [data.amount] - Amount (for type='purchase')
279
+ * @returns {Promise<Object>} - { success, charge_id, status, message }
280
+ */
281
+ async initiatePayment(data) {
282
+ return this._post('/payments/initiate', data);
283
+ }
284
+
285
+ /**
286
+ * Verify a payment status.
287
+ * @param {string} chargeId - The charge ID returned from initiatePayment
288
+ * @returns {Promise<Object>} - { success, status, data }
289
+ */
290
+ async verifyPayment(chargeId) {
291
+ return this._get(`/payments/verify/${chargeId}`, {}, (data) => data);
292
+ }
293
+
294
+ /**
295
+ * Get related resources for a given resource
296
+ * @param {number} resourceId - ID of the source resource
297
+ * @returns {Promise<Resource[]>} Up to 5 related resources
298
+ */
299
+ async getRelatedResources(resourceId) {
300
+ const response = await this._request(`/resources/${resourceId}/related`);
301
+ return response.data || [];
302
+ }
303
+
304
+ /**
305
+ * List bookmarked resources
306
+ * @param {Object} [options]
307
+ * @param {number} [options.limit] - Max results (1-100)
308
+ * @param {number} [options.offset] - Pagination offset
309
+ * @returns {Promise<Object[]>} Bookmarked resources with metadata
310
+ */
311
+ async getBookmarks(options = {}) {
312
+ const response = await this._request('/bookmarks', options);
313
+ return response.data || [];
314
+ }
315
+
316
+ /**
317
+ * Bookmark a resource
318
+ * @param {number} resourceId - ID of the resource to bookmark
319
+ * @returns {Promise<Object>} Bookmark record
320
+ */
321
+ async addBookmark(resourceId) {
322
+ const response = await this._post('/bookmarks', { resource_id: resourceId });
323
+ return response.data;
324
+ }
325
+
326
+ /**
327
+ * Remove a bookmark
328
+ * @param {number} resourceId - ID of the resource to un-bookmark
329
+ * @returns {Promise<Object>} Confirmation
330
+ */
331
+ async removeBookmark(resourceId) {
332
+ return await this._delete(`/bookmarks/${resourceId}`);
333
+ }
213
334
  }