import 'reflect-metadata' ; import axios , { AxiosRequestConfig , AxiosInstance } from 'axios' ; import { classToPlain , plainToClass } from 'class-transformer' ; import { validateOrReject } from 'class-validator' ; import FormData from 'form-data' ; import { ReadStream } from 'fs' ; import qs from 'qs' ; import { Account , AccountBalance , ApproveTransaction , CreateAccountParams , RedeemTransaction , RedeemTransactionResult , Transaction , TransactionResult , TransactionType } from './Account' ; import { Device , DeviceUpdateParams , DeviceCreateParams } from './Device' ; import { ApproveIssue , Issue , IssueWithStatus } from './Issue' ; import { Redemption , Transfer } from './Transfer' ; import { AccountItem , CodeName } from './Items' ; import { FuelType , DeviceType } from './FuelType' ; import { Organisation } from './Organisation' ; import { Beneficiary , BeneficiaryCreateParams , BeneficiaryUpdateParams } from './Beneficiary' ; import { HttpException } from '@nestjs/common/exceptions/http.exception' ; export type AccessTokens = { expiryDate : Date ; accessToken : string ; refreshToken : string ; } ; export class IRECAPIClient { private config : AxiosRequestConfig ; private interceptorId = NaN ; private axiosInstance : AxiosInstance ; public constructor ( private readonly endPointUrl , private clientId , private clientSecret , private onTokensRefreshed ? , private accessTokens ? ) { this . axiosInstance = axios . create ( { baseURL : endPointUrl , timeout : 30000 } ) ; this . enableInterceptor ( ) ; this . enableErrorHandler ( ) ; if ( accessTokens ) { this . applyAccessToken ( accessTokens . accessToken ) ; } } public async login ( userName , password ) { const url = `${ this . endPointUrl }/api/token` ; this . disableInterceptor ( ) ; const response = await this . axiosInstance . post < { expires_in : number ; access_token : string ; refresh_token : string ; } > ( url , qs . stringify ( { grant_type : 'password' , username : userName , password , client_id : this . clientId , client_secret : this . clientSecret , scope : '' } ) ) ; this . enableInterceptor ( ) ; this . applyTokens ( response . data . access_token , response . data . refresh_token , response . data . expires_in ) ; return this . accessTokens ; } public get account ( ) { const accountManagementUrl = `${ this . endPointUrl }/api/irec/v1/account-management` ; return { create : async ( account ) : Promise < void > => { const accountParams = account instanceof CreateAccountParams ? account : plainToClass ( CreateAccountParams , account ) ; await validateOrReject ( accountParams ) ; const url = `${ accountManagementUrl }/create` ; await this . axiosInstance . post ( url , classToPlain ( accountParams ) , this . config ) ; } , getAll : async ( ) : Promise < Account [ ] > => { const response = await this . axiosInstance . get < unknown [ ] > ( accountManagementUrl , this . config ) ; return response . data . map ( ( account ) => plainToClass ( Account , account ) ) ; } , get : async ( code ) : Promise < Account > => { const url = `${ accountManagementUrl }/${ code }` ; const response = await this . axiosInstance . get < unknown > ( url , this . config ) ; return plainToClass ( Account , response . data ) ; } , getBalance : async ( code ) : Promise < AccountBalance [ ] > => { const url = `${ accountManagementUrl }/${ code }/balance` ; const response = await this . axiosInstance . get < unknown [ ] > ( url , this . config ) ; return response . data . map ( ( account ) => plainToClass ( AccountBalance , account ) ) ; } , getTransactions : async ( code ) : Promise < Array < Transaction | RedeemTransaction > > => { const url = `${ accountManagementUrl }/${ code }/transactions` ; const response = await this . axiosInstance . get < any [ ] > ( url , this . config ) ; return response . data . map ( ( transaction ) => { if ( transaction . transaction_type . code === TransactionType . Redemption ) { return plainToClass ( RedeemTransaction , transaction ) ; } return plainToClass ( Transaction , transaction ) ; } ) ; } , getItems : async ( code ) : Promise < AccountItem [ ] > => { const url = `${ accountManagementUrl }/${ code }/items` ; const response = await this . axiosInstance . get < unknown [ ] > ( url , this . config ) ; return response . data . map ( ( item ) => plainToClass ( AccountItem , item ) ) ; } } ; } public get organisation ( ) { const organisationUrl = `${ this . endPointUrl }/api/irec/v1/organisation` ; return { get : async ( ) : Promise < Organisation > => { const response = await this . axiosInstance . get < unknown > ( organisationUrl , this . config ) ; return plainToClass ( Organisation , response . data ) ; } , getRegistrants : async ( ) : Promise < CodeName [ ] > => { const response = await this . axiosInstance . get < unknown [ ] > ( `${ organisationUrl }/registrants` , this . config ) ; return response . data . map ( ( org ) => plainToClass ( CodeName , org ) ) ; } , getIssuers : async ( ) : Promise < CodeName [ ] > => { const response = await this . axiosInstance . get < unknown [ ] > ( `${ organisationUrl }/issuers` , this . config ) ; return response . data . map ( ( org ) => plainToClass ( CodeName , org ) ) ; } } ; } public get issue ( ) { const issueManagementUrl = `${ this . endPointUrl }/api/irec/v1/issue-management` ; const setState = async ( code , action , notes ? ) => { const url = `${ issueManagementUrl }/${ code }/${ action }` ; await this . axiosInstance . put ( url , { notes } , this . config ) ; } ; return { create : async ( issue ) : Promise < IssueWithStatus > => { const issueParams = issue instanceof Issue ? issue : plainToClass ( Issue , issue ) ; await validateOrReject ( issue ) ; const url = `${ issueManagementUrl }/create` ; const response = await this . axiosInstance . post < IssueWithStatus > ( url , classToPlain ( issueParams ) , this . config ) ; return response . data ; } , update : async ( code , issue ) : Promise < void > => { await validateOrReject ( issue , { skipMissingProperties : true } ) ; const url = `${ issueManagementUrl }/${ code }/edit` ; await this . axiosInstance . put ( url , classToPlain ( issue ) , this . config ) ; } , get : async ( code ) : Promise < IssueWithStatus > => { const url = `${ issueManagementUrl }/${ code }` ; const response = await this . axiosInstance . get < unknown > ( url , this . config ) ; return plainToClass ( IssueWithStatus , response . data ) ; } , submit : async ( code , notes ? ) : Promise < void > => { await setState ( code , 'submit' , notes ) ; } , verify : async ( code , notes ? ) : Promise < void > => { await setState ( code , 'verify' , notes ) ; } , refer : async ( code , notes ? ) : Promise < void > => { await setState ( code , 'refer' , notes ) ; } , reject : async ( code , notes ? ) : Promise < void > => { await setState ( code , 'reject' , notes ) ; } , withdraw : async ( code , notes ? ) : Promise < void > => { await setState ( code , 'withdraw' , notes ) ; } , approve : async ( code , approve ) : Promise < ApproveTransaction > => { const appr = approve instanceof ApproveIssue ? approve : plainToClass ( ApproveIssue , approve ) ; await validateOrReject ( approve ) ; const url = `${ issueManagementUrl }/${ code }/approve` ; const response = await this . axiosInstance . put < any > ( url , classToPlain ( appr ) , this . config ) ; const asset = response . data . asset ; return plainToClass ( ApproveTransaction , { ... response . data . transaction , asset } ) ; } , getStatus : async ( code ) : Promise < IssueWithStatus > => { const url = `${ issueManagementUrl }/${ code }` ; const response = await this . axiosInstance . get < unknown > ( url , this . config ) ; return plainToClass ( IssueWithStatus , response . data ) ; } } ; } public get beneficiary ( ) { const beneficiaryManagementUrl = `${ this . endPointUrl }/api/irec/v1/beneficiaries` ; return { create : async ( params ) : Promise < Beneficiary > => { const beneficiaryParams = params instanceof BeneficiaryCreateParams ? params : plainToClass ( BeneficiaryCreateParams , params ) ; await validateOrReject ( beneficiaryParams ) ; const url = `${ beneficiaryManagementUrl }/create` ; const response = await this . axiosInstance . post < unknown > ( url , classToPlain ( beneficiaryParams ) , this . config ) ; return plainToClass ( Beneficiary , response . data ) ; } , update : async ( id , params ) : Promise < Beneficiary > => { const beneficiaryParams = params instanceof BeneficiaryUpdateParams ? params : plainToClass ( BeneficiaryUpdateParams , params ) ; await validateOrReject ( beneficiaryParams ) ; const url = `${ beneficiaryManagementUrl }/${ id }` ; const response = await this . axiosInstance . put < unknown > ( url , classToPlain ( beneficiaryParams ) , this . config ) ; return plainToClass ( Beneficiary , response . data ) ; } , get : async ( id ) : Promise < Beneficiary > => { const url = `${ beneficiaryManagementUrl }/${ id }` ; const response = await this . axiosInstance . get < unknown > ( url , this . config ) ; return plainToClass ( Beneficiary , response . data ) ; } , getAll : async ( ) : Promise < Beneficiary [ ] > => { const response = await this . axiosInstance . get < unknown [ ] > ( beneficiaryManagementUrl , this . config ) ; return response . data . map ( ( b ) => plainToClass ( Beneficiary , b ) ) ; } } ; } public get file ( ) { const fileManagementUrl = `${ this . endPointUrl }/api/irec/v1/file-management` ; return { upload : async ( files ) : Promise < string [ ] > => { const url = `${ fileManagementUrl }/upload` ; const data = new FormData ( ) ; files . forEach ( ( file ) => data . append ( 'files' , file ) ) ; const headers = data . getHeaders ( ) ; const response = await this . axiosInstance . post < { file_uids : string [ ] ; } > ( url , data , { headers : { ... headers , ... this . config . headers } } ) ; return response . data . file_uids ; } , download : async ( code ) : Promise < string > => { const url = `${ fileManagementUrl }/${ code }/download` ; const response = await this . axiosInstance . get < { url : string ; } > ( url , this . config ) ; return response . data . url ; } } ; } public get device ( ) { const deviceManagementUrl = `${ this . endPointUrl }/api/irec/v1/device-management` ; return { create : async ( device ) : Promise < Device > => { const dev = device instanceof DeviceCreateParams ? device : plainToClass ( DeviceCreateParams , device ) ; await validateOrReject ( dev ) ; const url = `${ deviceManagementUrl }/create` ; const response = await this . axiosInstance . post < { device : Device ; } > ( url , classToPlain ( dev ) , this . config ) ; return plainToClass ( Device , response . data ?. device ) ; } , edit : async ( code , device ) : Promise < Device > => { const dev = device instanceof DeviceUpdateParams ? device : plainToClass ( DeviceUpdateParams , device ) ; await validateOrReject ( dev , { skipMissingProperties : true } ) ; const url = `${ deviceManagementUrl }/${ code }/edit` ; const response = await this . axiosInstance . put < { device : Device ; } > ( url , classToPlain ( dev ) , this . config ) ; return plainToClass ( Device , response . data ?. device ) ; } , getAll : async ( ) : Promise < Device [ ] > => { const response = await this . axiosInstance . get < unknown [ ] > ( deviceManagementUrl , this . config ) ; return response . data . map ( ( device ) => plainToClass ( Device , device ) ) ; } , get : async ( code ) : Promise < Device > => { const url = `${ deviceManagementUrl }/${ code }` ; const response = await this . axiosInstance . get < unknown > ( url , this . config ) ; return plainToClass ( Device , response . data ) ; } , submit : async ( code , { notes , fileIds } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/submit` ; await this . axiosInstance . put < unknown > ( url , { notes , file_data : fileIds ?. map ( ( id ) => ( { file_uid : id } ) ) ?? [ ] } , this . config ) ; } , verify : async ( code , { notes , fileIds } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/verify` ; await this . axiosInstance . put < unknown > ( url , { notes , file_data : fileIds ?. map ( ( id ) => ( { file_uid : id } ) ) ?? [ ] } , this . config ) ; } , approve : async ( code , { notes , fileIds } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/approve` ; await this . axiosInstance . put < unknown > ( url , { notes , file_data : fileIds ?. map ( ( id ) => ( { file_uid : id } ) ) ?? [ ] } , this . config ) ; } , refer : async ( code , { notes , fileIds } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/refer` ; await this . axiosInstance . put < unknown > ( url , { notes , file_data : fileIds ?. map ( ( id ) => ( { file_uid : id } ) ) ?? [ ] } , this . config ) ; } , reject : async ( code , { notes , fileIds } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/reject` ; await this . axiosInstance . put < unknown > ( url , { notes , file_data : fileIds ?. map ( ( id ) => ( { file_uid : id } ) ) ?? [ ] } , this . config ) ; } , withdraw : async ( code , { notes } = { } ) : Promise < void > => { const url = `${ deviceManagementUrl }/${ code }/withdraw` ; await this . axiosInstance . put < unknown > ( url , { notes } , this . config ) ; } } ; } public get fuel ( ) { const fuelUrl = `${ this . endPointUrl }/api/irec/v1/fuels` ; return { getFuelTypes : async ( ) : Promise < FuelType [ ] > => { const url = `${ fuelUrl }/fuel` ; const response = await this . axiosInstance . get < unknown [ ] > ( url , this . config ) ; return response . data . map ( ( fuel ) => plainToClass ( FuelType , fuel ) ) ; } , getDeviceTypes : async ( ) : Promise < DeviceType [ ] > => { const url = `${ fuelUrl }/type` ; const response = await this . axiosInstance . get < unknown [ ] > ( url , this . config ) ; return response . data . map ( ( fuelType ) => plainToClass ( DeviceType , fuelType ) ) ; } } ; } public async transfer ( transfer ) { const t = transfer instanceof Transfer ? transfer : plainToClass ( Transfer , transfer ) ; await validateOrReject ( t ) ; const url = `${ this . endPointUrl }/api/irec/v1/transfer-management` ; const response = await this . axiosInstance . post < { transaction : any ; } > ( url , classToPlain ( t ) , this . config ) ; return plainToClass ( TransactionResult , response . data . transaction ) ; } public async redeem ( redemption ) { const r = redemption instanceof Redemption ? redemption : plainToClass ( Redemption , redemption ) ; await validateOrReject ( redemption ) ; const url = `${ this . endPointUrl }/api/irec/v1/redemption-management` ; const response = await this . axiosInstance . post < { transaction : any ; } > ( url , classToPlain ( r ) , this . config ) ; return plainToClass ( RedeemTransactionResult , response . data . transaction ) ; } private applyTokens ( accessToken , refreshToken , expiresIn ) { this . accessTokens = { expiryDate : new Date ( new Date ( ) . getTime ( ) + expiresIn * 1000 ) , accessToken , refreshToken } ; this . applyAccessToken ( accessToken ) ; } private applyAccessToken ( accessToken ) { this . config = { headers : { Authorization : `Bearer ${ accessToken }` } } ; } private async refreshAccessTokens ( ) { const url = `${ this . endPointUrl }/api/token` ; this . disableInterceptor ( ) ; const response = await this . axiosInstance . post < { expires_in : number ; access_token : string ; refresh_token : string ; } > ( url , qs . stringify ( { grant_type : 'refresh_token' , refresh_token : this . accessTokens . refreshToken , client_id : this . clientId , client_secret : this . clientSecret } ) ) ; this . enableInterceptor ( ) ; this . applyTokens ( response . data . access_token , response . data . refresh_token , response . data . expires_in ) ; if ( this . onTokensRefreshed ) { await this . onTokensRefreshed ( this . accessTokens ) ; } } private async ensureNotExpired ( ) { if ( ! this . accessTokens ) { throw new Error ( 'Access token was not set' ) ; } if ( this . accessTokens . expiryDate <= new Date ( ) && this . accessTokens . refreshToken ) { await this . refreshAccessTokens ( ) ; } } private enableInterceptor ( ) { if ( ! Number . isNaN ( this . interceptorId ) ) { return ; } this . interceptorId = this . axiosInstance . interceptors . request . use ( async ( config ) => { console . log ( `${ config . method } ${ config . url } ${ JSON . stringify ( config . data ) ?? '' }` ) ; await this . ensureNotExpired ( ) ; config . headers = { ... config . headers , ... this . config . headers } ; return config ; } ) ; } private disableInterceptor ( ) { if ( Number . isNaN ( this . interceptorId ) ) { return ; } this . axiosInstance . interceptors . request . eject ( this . interceptorId ) ; this . interceptorId = NaN ; } private enableErrorHandler ( ) { this . axiosInstance . interceptors . response . use ( ( res ) => res , ( err ) => { const status = err ?. response ?. data ?. status ?? err ?. response ?. status ?? 500 ; return Promise . reject ( new HttpException ( 'IREC API Error:' + ( err ?. response ?. data ?. msg ?? err ?. response ?. data ?. title ?? err . message ) , status ) ) ; } ) ; } }