From 17c7456ed690ee51798d4a97f44324cfffcf26f3 Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Thu, 5 Mar 2026 15:21:27 -0500 Subject: [PATCH] add ledger-items endpoint support --- src/index.js | 66 +++++++++++ test/index.test.js | 276 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 341 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 109e2dc..72085b7 100644 --- a/src/index.js +++ b/src/index.js @@ -80,6 +80,45 @@ class TemplateInfo { } } +// LedgerItemPassTemplate model class +class LedgerItemPassTemplate { + constructor(data = {}) { + this.id = data.id; + this.name = data.name; + this.protocol = data.protocol; + this.platform = data.platform; + this.useCase = data.use_case; + } +} + +// LedgerItemAccessPass model class +class LedgerItemAccessPass { + constructor(data = {}) { + this.id = data.id; + this.fullName = data.full_name; + this.state = data.state; + this.metadata = data.metadata || {}; + this.unifiedAccessPassExId = data.unified_access_pass_ex_id; + this.passTemplate = data.pass_template + ? new LedgerItemPassTemplate(data.pass_template) + : null; + } +} + +// LedgerItem model class +class LedgerItem { + constructor(data = {}) { + this.id = data.id; + this.createdAt = data.created_at; + this.amount = data.amount; + this.kind = data.kind; + this.metadata = data.metadata || {}; + this.accessPass = data.access_pass + ? new LedgerItemAccessPass(data.access_pass) + : null; + } +} + // Base API wrapper to handle common functionality class BaseApi { constructor(accountId, secretKey, baseUrl = "https://api.accessgrid.com") { @@ -473,6 +512,30 @@ class ConsoleApi extends BaseApi { return response; } + + async listLedgerItems(params = {}) { + const queryParams = new URLSearchParams(); + if (params.page) queryParams.append("page", params.page); + if (params.perPage) queryParams.append("per_page", params.perPage); + if (params.startDate) queryParams.append("start_date", params.startDate); + if (params.endDate) queryParams.append("end_date", params.endDate); + + const queryString = queryParams.toString(); + const path = queryString + ? `/v1/console/ledger-items?${queryString}` + : "/v1/console/ledger-items"; + + const response = await this.request(path); + + if (response.ledger_items) { + response.ledgerItems = response.ledger_items.map( + (item) => new LedgerItem(item), + ); + delete response.ledger_items; + } + + return response; + } } // Main AccessGrid class @@ -497,6 +560,9 @@ export { Template, PassTemplatePair, TemplateInfo, + LedgerItem, + LedgerItemAccessPass, + LedgerItemPassTemplate, }; // Default export diff --git a/test/index.test.js b/test/index.test.js index e9f31bd..8befddf 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,4 +1,4 @@ -import AccessGrid, { AccessGridError, AuthenticationError, AccessCard, Template, PassTemplatePair, TemplateInfo } from '../src/index'; +import AccessGrid, { AccessGridError, AuthenticationError, AccessCard, Template, PassTemplatePair, TemplateInfo, LedgerItem, LedgerItemAccessPass, LedgerItemPassTemplate } from '../src/index'; // ══════════════════════════════════════════════════════════════════════════════ // Global mocks @@ -707,5 +707,279 @@ describe('AccessGrid SDK', () => { expect(info.name).toBe('Employee Badge'); expect(info.platform).toBe('apple'); }); + + test('LedgerItem should deserialize with nested access pass and template', () => { + const item = new LedgerItem({ + id: 'li-1', + created_at: '2025-03-01T00:00:00Z', + amount: 150, + kind: 'provision', + metadata: { note: 'test' }, + access_pass: { + id: 'ap-1', + full_name: 'Jane Doe', + state: 'active', + metadata: { dept: 'eng' }, + unified_access_pass_ex_id: 'uap-1', + pass_template: { + id: 'pt-1', + name: 'Employee Badge', + protocol: 'desfire', + platform: 'apple', + use_case: 'employee_badge' + } + } + }); + + expect(item.id).toBe('li-1'); + expect(item.createdAt).toBe('2025-03-01T00:00:00Z'); + expect(item.amount).toBe(150); + expect(item.kind).toBe('provision'); + expect(item.metadata).toEqual({ note: 'test' }); + expect(item.accessPass).toBeInstanceOf(LedgerItemAccessPass); + expect(item.accessPass.id).toBe('ap-1'); + expect(item.accessPass.fullName).toBe('Jane Doe'); + expect(item.accessPass.state).toBe('active'); + expect(item.accessPass.metadata).toEqual({ dept: 'eng' }); + expect(item.accessPass.unifiedAccessPassExId).toBe('uap-1'); + expect(item.accessPass.passTemplate).toBeInstanceOf(LedgerItemPassTemplate); + expect(item.accessPass.passTemplate.id).toBe('pt-1'); + expect(item.accessPass.passTemplate.name).toBe('Employee Badge'); + expect(item.accessPass.passTemplate.protocol).toBe('desfire'); + expect(item.accessPass.passTemplate.platform).toBe('apple'); + expect(item.accessPass.passTemplate.useCase).toBe('employee_badge'); + }); + + test('LedgerItem handles null access_pass', () => { + const item = new LedgerItem({ + id: 'li-2', + created_at: '2025-03-01T00:00:00Z', + amount: 50, + kind: 'renewal', + metadata: {}, + access_pass: null + }); + + expect(item.id).toBe('li-2'); + expect(item.accessPass).toBeNull(); + }); + + test('LedgerItem handles null pass_template on access pass', () => { + const item = new LedgerItem({ + id: 'li-3', + created_at: '2025-03-01T00:00:00Z', + amount: 75, + kind: 'provision', + metadata: {}, + access_pass: { + id: 'ap-2', + full_name: 'John Smith', + state: 'suspended', + metadata: {}, + unified_access_pass_ex_id: null, + pass_template: null + } + }); + + expect(item.accessPass).toBeInstanceOf(LedgerItemAccessPass); + expect(item.accessPass.passTemplate).toBeNull(); + }); + + test('LedgerItem handles missing access_pass key', () => { + const item = new LedgerItem({ + id: 'li-4', + created_at: '2025-03-01T00:00:00Z', + amount: 25, + kind: 'other' + }); + + expect(item.accessPass).toBeNull(); + }); + }); + + // ════════════════════════════════════════════════════════════════════════════ + // Console API — Ledger Items + // ════════════════════════════════════════════════════════════════════════════ + + describe('Console API — listLedgerItems', () => { + const mockLedgerResponse = { + ledger_items: [ + { + id: 'li-1', + created_at: '2025-03-01T12:00:00Z', + amount: 150, + kind: 'provision', + metadata: {}, + access_pass: { + id: 'ap-1', + full_name: 'Jane Doe', + state: 'active', + metadata: {}, + unified_access_pass_ex_id: 'uap-1', + pass_template: { + id: 'pt-1', + name: 'Employee Badge', + protocol: 'desfire', + platform: 'apple', + use_case: 'employee_badge' + } + } + }, + { + id: 'li-2', + created_at: '2025-03-02T12:00:00Z', + amount: 50, + kind: 'renewal', + metadata: {}, + access_pass: null + } + ], + pagination: { + current_page: 1, + per_page: 50, + total_pages: 3, + total_count: 125 + } + }; + + test('should make correct API call', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + await client.console.listLedgerItems(); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/ledger-items'), + expect.objectContaining({ + method: 'GET' + }) + ); + }); + + test('should pass pagination params', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + await client.console.listLedgerItems({ page: 2, perPage: 10 }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('page=2'), + expect.anything() + ); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('per_page=10'), + expect.anything() + ); + }); + + test('should pass date filter params', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + await client.console.listLedgerItems({ + startDate: '2025-03-01T00:00:00Z', + endDate: '2025-03-31T23:59:59Z' + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('start_date=2025-03-01T00%3A00%3A00Z'), + expect.anything() + ); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('end_date=2025-03-31T23%3A59%3A59Z'), + expect.anything() + ); + }); + + test('should not include query string when no params given', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + await client.console.listLedgerItems(); + + const calledUrl = fetch.mock.calls[0][0]; + expect(calledUrl).toMatch(/\/ledger-items(\?sig_payload=|$)/); + }); + + test('should return LedgerItem instances', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + const result = await client.console.listLedgerItems(); + + expect(result.ledgerItems).toHaveLength(2); + expect(result.ledgerItems[0]).toBeInstanceOf(LedgerItem); + expect(result.ledgerItems[1]).toBeInstanceOf(LedgerItem); + }); + + test('should remove snake_case key from response', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + const result = await client.console.listLedgerItems(); + + expect(result.ledger_items).toBeUndefined(); + }); + + test('should deserialize nested models', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + const result = await client.console.listLedgerItems(); + const item = result.ledgerItems[0]; + + expect(item.id).toBe('li-1'); + expect(item.amount).toBe(150); + expect(item.kind).toBe('provision'); + expect(item.accessPass).toBeInstanceOf(LedgerItemAccessPass); + expect(item.accessPass.id).toBe('ap-1'); + expect(item.accessPass.fullName).toBe('Jane Doe'); + expect(item.accessPass.passTemplate).toBeInstanceOf(LedgerItemPassTemplate); + expect(item.accessPass.passTemplate.id).toBe('pt-1'); + expect(item.accessPass.passTemplate.name).toBe('Employee Badge'); + }); + + test('should handle null access_pass in response', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + const result = await client.console.listLedgerItems(); + const item = result.ledgerItems[1]; + + expect(item.id).toBe('li-2'); + expect(item.accessPass).toBeNull(); + }); + + test('should preserve pagination metadata', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockLedgerResponse) + }); + + const result = await client.console.listLedgerItems(); + + expect(result.pagination).toEqual({ + current_page: 1, + per_page: 50, + total_pages: 3, + total_count: 125 + }); + }); }); });