diff --git a/src/index.js b/src/index.js index 6c89596..a3fa30e 100644 --- a/src/index.js +++ b/src/index.js @@ -69,6 +69,42 @@ class Template { } } +// HIDOrg model class +class HIDOrg { + constructor(data = {}) { + this.id = data.id; + this.name = data.name; + this.slug = data.slug; + this.firstName = data.first_name; + this.lastName = data.last_name; + this.phone = data.phone; + this.fullAddress = data.full_address; + this.status = data.status; + this.createdAt = data.created_at; + } +} + +// LedgerItem model class +class LedgerItem { + constructor(data = {}) { + this.id = data.id; + this.amount = data.amount; + this.kind = data.kind; + this.createdAt = data.created_at; + + if (data.access_pass) { + this.accessPass = { + exId: data.access_pass.ex_id, + passTemplate: data.access_pass.pass_template + ? { exId: data.access_pass.pass_template.ex_id } + : null, + }; + } else { + this.accessPass = null; + } + } +} + // PassTemplatePair model class class PassTemplatePair { constructor(data = {}) { @@ -395,6 +431,9 @@ class AccessCardsApi extends BaseApi { class ConsoleApi extends BaseApi { constructor(accountId, secretKey, baseUrl) { super(accountId, secretKey, baseUrl); + this.hid = { + orgs: new HIDOrgsApi(accountId, secretKey, baseUrl), + }; } _buildTemplateBody(params) { @@ -476,6 +515,50 @@ class ConsoleApi extends BaseApi { return this.getEventLogs(params); } + async iosPreflight(params) { + const body = {}; + if (params.accessPassExId) body.access_pass_ex_id = params.accessPassExId; + + const response = await this.request( + `/v1/console/card-templates/${params.cardTemplateId}/ios_preflight`, + { method: "POST", body }, + ); + + return { + provisioningCredentialIdentifier: + response.provisioning_credential_identifier, + sharingInstanceIdentifier: response.sharing_instance_identifier, + cardTemplateIdentifier: response.card_template_identifier, + environmentIdentifier: response.environment_identifier, + }; + } + + async ledgerItems(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); + + const result = {}; + if (response.ledger_items) { + result.ledgerItems = response.ledger_items.map( + (item) => new LedgerItem(item), + ); + } + if (response.pagination) { + result.pagination = response.pagination; + } + return result; + } + async listPassTemplatePairs(params = {}) { const queryParams = new URLSearchParams(); if (params.page) queryParams.append("page", params.page); @@ -499,6 +582,45 @@ class ConsoleApi extends BaseApi { } } +// HID Orgs API handling +class HIDOrgsApi extends BaseApi { + constructor(accountId, secretKey, baseUrl) { + super(accountId, secretKey, baseUrl); + } + + async create(params) { + const body = { + name: params.name, + full_address: params.fullAddress, + phone: params.phone, + first_name: params.firstName, + last_name: params.lastName, + }; + + const response = await this.request("/v1/console/hid/orgs", { + method: "POST", + body, + }); + return new HIDOrg(response); + } + + async list() { + const response = await this.request("/v1/console/hid/orgs"); + return (response.hid_orgs || []).map((org) => new HIDOrg(org)); + } + + async activate(params) { + const response = await this.request("/v1/console/hid/orgs/activate", { + method: "POST", + body: { + email: params.email, + password: params.password, + }, + }); + return new HIDOrg(response); + } +} + // Main AccessGrid class class AccessGrid { constructor(accountId, secretKey, options = {}) { @@ -521,6 +643,8 @@ export { Template, PassTemplatePair, TemplateInfo, + HIDOrg, + LedgerItem, }; // Default export diff --git a/test/index.test.js b/test/index.test.js index 3651719..36cad3b 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, HIDOrg, LedgerItem } from '../src/index'; // ══════════════════════════════════════════════════════════════════════════════ // Global mocks @@ -756,6 +756,50 @@ describe('AccessGrid SDK', () => { expect(info.platform).toBe('apple'); }); + test('HIDOrg should have correct properties', () => { + const org = new HIDOrg({ + id: 'org-1', + name: 'My Org', + slug: 'my-org', + first_name: 'Ada', + last_name: 'Lovelace', + phone: '+1-555-0000', + full_address: '1 Main St, NY NY', + status: 'active', + created_at: '2025-01-01T00:00:00Z' + }); + + expect(org.id).toBe('org-1'); + expect(org.name).toBe('My Org'); + expect(org.slug).toBe('my-org'); + expect(org.firstName).toBe('Ada'); + expect(org.lastName).toBe('Lovelace'); + expect(org.phone).toBe('+1-555-0000'); + expect(org.fullAddress).toBe('1 Main St, NY NY'); + expect(org.status).toBe('active'); + expect(org.createdAt).toBe('2025-01-01T00:00:00Z'); + }); + + test('LedgerItem should have correct properties', () => { + const item = new LedgerItem({ + id: 'li-1', + amount: '1.50', + kind: 'issuance', + created_at: '2025-01-01T00:00:00Z', + access_pass: { + ex_id: 'pass-1', + pass_template: { ex_id: 'tmpl-1' } + } + }); + + expect(item.id).toBe('li-1'); + expect(item.amount).toBe('1.50'); + expect(item.kind).toBe('issuance'); + expect(item.createdAt).toBe('2025-01-01T00:00:00Z'); + expect(item.accessPass.exId).toBe('pass-1'); + expect(item.accessPass.passTemplate.exId).toBe('tmpl-1'); + }); + test('Template should have metadata property', () => { const template = new Template({ id: 'template-123', @@ -794,6 +838,180 @@ describe('AccessGrid SDK', () => { }); }); + // ════════════════════════════════════════════════════════════════════════════ + // HID Orgs API + // ════════════════════════════════════════════════════════════════════════════ + + describe('HID Orgs API', () => { + describe('create', () => { + test('should make correct API call', async () => { + const mockResponse = { + id: 'org-1', name: 'My Org', slug: 'my-org', + first_name: 'Ada', last_name: 'Lovelace', + phone: '+1-555-0000', full_address: '1 Main St, NY NY', + status: 'pending', created_at: '2025-01-01T00:00:00Z' + }; + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse) + }); + + const org = await client.console.hid.orgs.create({ + name: 'My Org', + fullAddress: '1 Main St, NY NY', + phone: '+1-555-0000', + firstName: 'Ada', + lastName: 'Lovelace' + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/hid/orgs'), + expect.objectContaining({ method: 'POST' }) + ); + expect(org).toBeInstanceOf(HIDOrg); + expect(org.name).toBe('My Org'); + expect(org.slug).toBe('my-org'); + }); + + test('should send snake_case params', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: 'org-1' }) + }); + + await client.console.hid.orgs.create({ + name: 'My Org', + fullAddress: '1 Main St', + phone: '+1-555-0000', + firstName: 'Ada', + lastName: 'Lovelace' + }); + + const callBody = JSON.parse(fetch.mock.calls[0][1].body); + expect(callBody.name).toBe('My Org'); + expect(callBody.full_address).toBe('1 Main St'); + expect(callBody.first_name).toBe('Ada'); + expect(callBody.last_name).toBe('Lovelace'); + }); + }); + + describe('list', () => { + test('should return array of HIDOrg instances', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ + hid_orgs: [ + { id: 'org-1', name: 'Org 1', slug: 'org-1' }, + { id: 'org-2', name: 'Org 2', slug: 'org-2' } + ] + }) + }); + + const orgs = await client.console.hid.orgs.list(); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/hid/orgs'), + expect.objectContaining({ method: 'GET' }) + ); + expect(orgs).toHaveLength(2); + expect(orgs[0]).toBeInstanceOf(HIDOrg); + }); + }); + + describe('activate', () => { + test('should make correct API call', async () => { + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ id: 'org-1', name: 'My Org', status: 'active' }) + }); + + const result = await client.console.hid.orgs.activate({ + email: 'admin@example.com', + password: 'hid-password-123' + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/hid/orgs/activate'), + expect.objectContaining({ method: 'POST' }) + ); + expect(result).toBeInstanceOf(HIDOrg); + expect(result.status).toBe('active'); + }); + }); + }); + + // ════════════════════════════════════════════════════════════════════════════ + // iOS Preflight + // ════════════════════════════════════════════════════════════════════════════ + + describe('iOS Preflight', () => { + test('should make correct API call', async () => { + const mockResponse = { + provisioning_credential_identifier: 'pci-123', + sharing_instance_identifier: 'sii-456', + card_template_identifier: 'cti-789', + environment_identifier: 'env-abc' + }; + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse) + }); + + const result = await client.console.iosPreflight({ + cardTemplateId: '0xt3mp14t3', + accessPassExId: '0xp455' + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/card-templates/0xt3mp14t3/ios_preflight'), + expect.objectContaining({ method: 'POST' }) + ); + expect(result.provisioningCredentialIdentifier).toBe('pci-123'); + expect(result.sharingInstanceIdentifier).toBe('sii-456'); + expect(result.cardTemplateIdentifier).toBe('cti-789'); + expect(result.environmentIdentifier).toBe('env-abc'); + }); + }); + + // ════════════════════════════════════════════════════════════════════════════ + // Ledger Items + // ════════════════════════════════════════════════════════════════════════════ + + describe('Ledger Items', () => { + test('should make correct API call with pagination', async () => { + const mockResponse = { + ledger_items: [ + { id: 'li-1', amount: '1.50', kind: 'issuance', created_at: '2025-01-01T00:00:00Z' }, + { id: 'li-2', amount: '0.50', kind: 'activation', created_at: '2025-01-02T00:00:00Z' } + ], + pagination: { current_page: 1, total_pages: 3, total_count: 50 } + }; + global.fetch.mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve(mockResponse) + }); + + const result = await client.console.ledgerItems({ + page: 1, + perPage: 50, + startDate: '2025-01-01T00:00:00Z', + endDate: '2025-02-01T00:00:00Z' + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/console/ledger-items'), + expect.objectContaining({ method: 'GET' }) + ); + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('page=1'), + expect.anything() + ); + expect(result.ledgerItems).toHaveLength(2); + expect(result.ledgerItems[0]).toBeInstanceOf(LedgerItem); + expect(result.pagination).toBeDefined(); + }); + }); + // ════════════════════════════════════════════════════════════════════════════ // Provision with new fields // ════════════════════════════════════════════════════════════════════════════