Skip to content
Merged
60 changes: 30 additions & 30 deletions src/bankAccount.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
import { Account } from './dataGroups/Account.js';
import { SepaAccount } from './dataGroups/SepaAccount.js';
import { AccountLimit, AllowedTransactions } from './segments/HIUPD.js';

export enum AccountType {
CheckingAccount = 'CheckingAccount',
SavingsAccount = 'SavingsAccount',
FixedDepositAccount = 'FixedDepositAccount',
SecuritiesAccount = 'SecuritiesAccount',
LoanMortgageAccount = 'LoanMortgageAccount',
CreditCardAccount = 'CreditCardAccount',
InvestmentCompanyFund = 'InvestmentCompanyFund',
HomeSavingsContract = 'HomeSavingsContract',
InsurancePolicy = 'InsurancePolicy',
Miscellaneous = 'Miscellaneous',
CheckingAccount = 'CheckingAccount',
SavingsAccount = 'SavingsAccount',
FixedDepositAccount = 'FixedDepositAccount',
SecuritiesAccount = 'SecuritiesAccount',
LoanMortgageAccount = 'LoanMortgageAccount',
CreditCardAccount = 'CreditCardAccount',
InvestmentCompanyFund = 'InvestmentCompanyFund',
HomeSavingsContract = 'HomeSavingsContract',
InsurancePolicy = 'InsurancePolicy',
Miscellaneous = 'Miscellaneous',
}

export type BankAccount = Account & {
iban?: string;
customerId: string;
accountType: AccountType;
currency: string;
holder1: string;
holder2?: string;
product?: string;
limit?: AccountLimit;
allowedTransactions?: AllowedTransactions[];
export type BankAccount = SepaAccount & {
customerId: string;
accountType: AccountType;
currency: string;
holder1: string;
holder2?: string;
product?: string;
limit?: AccountLimit;
allowedTransactions?: AllowedTransactions[];
};

export function finTsAccountTypeToEnum(accountType: number): AccountType {
if (accountType >= 1 && accountType <= 9) return AccountType.CheckingAccount;
if (accountType >= 10 && accountType <= 19) return AccountType.SavingsAccount;
if (accountType >= 20 && accountType <= 29) return AccountType.FixedDepositAccount;
if (accountType >= 30 && accountType <= 39) return AccountType.SecuritiesAccount;
if (accountType >= 40 && accountType <= 49) return AccountType.LoanMortgageAccount;
if (accountType >= 50 && accountType <= 59) return AccountType.CreditCardAccount;
if (accountType >= 60 && accountType <= 69) return AccountType.InvestmentCompanyFund;
if (accountType >= 70 && accountType <= 79) return AccountType.HomeSavingsContract;
if (accountType >= 80 && accountType <= 89) return AccountType.InsurancePolicy;
return AccountType.Miscellaneous;
if (accountType >= 1 && accountType <= 9) return AccountType.CheckingAccount;
if (accountType >= 10 && accountType <= 19) return AccountType.SavingsAccount;
if (accountType >= 20 && accountType <= 29) return AccountType.FixedDepositAccount;
if (accountType >= 30 && accountType <= 39) return AccountType.SecuritiesAccount;
if (accountType >= 40 && accountType <= 49) return AccountType.LoanMortgageAccount;
if (accountType >= 50 && accountType <= 59) return AccountType.CreditCardAccount;
if (accountType >= 60 && accountType <= 69) return AccountType.InvestmentCompanyFund;
if (accountType >= 70 && accountType <= 79) return AccountType.HomeSavingsContract;
if (accountType >= 80 && accountType <= 89) return AccountType.InsurancePolicy;
return AccountType.Miscellaneous;
}
142 changes: 36 additions & 106 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { Dialog } from './dialog.js';
import { HKTAB } from './segments/HKTAB.js';
import { StatementResponse, StatementInteraction } from './interactions/statementInteraction.js';
import { AccountBalanceResponse, BalanceInteraction } from './interactions/balanceInteraction.js';
import { PortfolioResponse, PortfolioInteraction } from './interactions/portfolioInteraction.js';
import { FinTSConfig } from './config.js';
import { ClientResponse, CustomerInteraction, CustomerOrderInteraction } from './interactions/customerInteraction.js';
import { TanMediaInteraction, TanMediaResponse } from './interactions/tanMediaInteraction.js';
import { ClientResponse, CustomerOrderInteraction } from './interactions/customerInteraction.js';
import { TanMethod } from './tanMethod.js';
import { HKSAL } from './segments/HKSAL.js';
import { HKKAZ } from './segments/HKKAZ.js';
import { HKWPD } from './segments/HKWPD.js';
import { DKKKU } from './segments/DKKKU.js';
import { InitDialogInteraction, InitResponse } from './interactions/initDialogInteraction.js';
import {CreditCardStatementInteraction} from "./interactions/creditcardStatementInteraction.js";
import { InitResponse } from './interactions/initDialogInteraction.js';
import { CreditCardStatementInteraction } from './interactions/creditcardStatementInteraction.js';
import { HKIDN } from './segments/HKIDN.js';

export interface SynchronizeResponse extends InitResponse {}

/**
* A client to communicate with a bank over the FinTS protocol
*/
export class FinTSClient {
private openCustomerInteractions = new Map<string, CustomerInteraction>();
private currentDialog: Dialog | undefined;

/**
* Creates a new FinTS client
Expand Down Expand Up @@ -59,27 +58,9 @@ export class FinTSClient {
* @returns the synchronization response
*/
async synchronize(): Promise<SynchronizeResponse> {
const dialog = new Dialog(this.config);

const syncResponse = await this.initDialog(dialog, true);

if (!syncResponse.success || syncResponse.requiresTan) {
return syncResponse;
}

if (this.config.selectedTanMethod && this.config.isTransactionSupported(HKTAB.Id)) {
const tanMediaResponse = await dialog.startCustomerOrderInteraction<TanMediaResponse>(new TanMediaInteraction());

let tanMethod = this.config.selectedTanMethod;
if (tanMethod) {
tanMethod.activeTanMedia = tanMediaResponse.tanMediaList;
}

syncResponse.bankAnswers.push(...tanMediaResponse.bankAnswers);
}

await dialog.end();
return syncResponse;
this.currentDialog = new Dialog(this.config, true);
const responses = await this.currentDialog.start();
return responses.get(HKIDN.Id) as SynchronizeResponse;
}

/**
Expand All @@ -89,7 +70,8 @@ export class FinTSClient {
* @returns the synchronization response
*/
async synchronizeWithTan(tanReference: string, tan?: string): Promise<SynchronizeResponse> {
return this.continueCustomerInteractionWithTan(tanReference, tan);
const responses = await this.continueCustomerInteractionWithTan(tanReference, tan);
return responses.get(HKIDN.Id) as SynchronizeResponse;
}

/**
Expand All @@ -109,7 +91,8 @@ export class FinTSClient {
* @returns the account balance response
*/
async getAccountBalance(accountNumber: string): Promise<AccountBalanceResponse> {
return this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
const responses = await this.startCustomerOrderInteraction(new BalanceInteraction(accountNumber));
return responses.get(HKSAL.Id) as AccountBalanceResponse;
}

/**
Expand All @@ -119,7 +102,8 @@ export class FinTSClient {
* @returns the account balance response
*/
async getAccountBalanceWithTan(tanReference: string, tan?: string): Promise<AccountBalanceResponse> {
return this.continueCustomerInteractionWithTan(tanReference, tan);
const responses = await this.continueCustomerInteractionWithTan(tanReference, tan);
return responses.get(HKSAL.Id) as AccountBalanceResponse;
}

/**
Expand All @@ -141,7 +125,8 @@ export class FinTSClient {
* @returns an account statements response containing an array of statements
*/
async getAccountStatements(accountNumber: string, from?: Date, to?: Date): Promise<StatementResponse> {
return this.startCustomerOrderInteraction(new StatementInteraction(accountNumber, from, to));
const responses = await this.startCustomerOrderInteraction(new StatementInteraction(accountNumber, from, to));
return responses.get(HKKAZ.Id) as StatementResponse;
}

/**
Expand All @@ -151,7 +136,8 @@ export class FinTSClient {
* @returns an account statements response containing an array of statements
*/
async getAccountStatementsWithTan(tanReference: string, tan?: string): Promise<StatementResponse> {
return this.continueCustomerInteractionWithTan(tanReference, tan);
const responses = await this.continueCustomerInteractionWithTan(tanReference, tan);
return responses.get(HKKAZ.Id) as StatementResponse;
}

/**
Expand Down Expand Up @@ -179,9 +165,10 @@ export class FinTSClient {
priceQuality?: '1' | '2',
maxEntries?: number
): Promise<PortfolioResponse> {
return this.startCustomerOrderInteraction(
const responses = await this.startCustomerOrderInteraction(
new PortfolioInteraction(accountNumber, currency, priceQuality, maxEntries)
);
return responses.get(HKWPD.Id) as PortfolioResponse;
}

/**
Expand All @@ -192,10 +179,10 @@ export class FinTSClient {
* @returns a portfolio response containing holdings and total value
*/
async getPortfolioWithTan(tanReference: string, tan?: string): Promise<PortfolioResponse> {
return this.continueCustomerInteractionWithTan(tanReference, tan);
const responses = await this.continueCustomerInteractionWithTan(tanReference, tan);
return responses.get(HKWPD.Id) as PortfolioResponse;
}


/**
* Checks if the bank supports fetching credit card statements in general or for the given account number
* @param accountNumber when the account number is provided, checks if the account supports fetching of statements
Expand All @@ -216,7 +203,8 @@ export class FinTSClient {
* @returns an account statements response containing an array of statements
*/
async getCreditCardStatements(accountNumber: string, from?: Date): Promise<StatementResponse> {
return this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from));
const responses = await this.startCustomerOrderInteraction(new CreditCardStatementInteraction(accountNumber, from));
return responses.get(DKKKU.Id) as StatementResponse;
}

/**
Expand All @@ -226,84 +214,26 @@ export class FinTSClient {
* @returns a credit card statements response containing an array of statements
*/
async getCreditCardStatementsWithTan(tanReference: string, tan?: string): Promise<StatementResponse> {
return this.continueCustomerInteractionWithTan(tanReference, tan);
const responses = await this.continueCustomerInteractionWithTan(tanReference, tan);
return responses.get(DKKKU.Id) as StatementResponse;
}

private async startCustomerOrderInteraction<TClientResponse extends ClientResponse>(
private async startCustomerOrderInteraction(
interaction: CustomerOrderInteraction
): Promise<TClientResponse> {
const dialog = new Dialog(this.config);
const syncResponse = await this.initDialog(dialog, false, interaction);

if (!syncResponse.success || syncResponse.requiresTan) {
return syncResponse as TClientResponse;
}

const clientResponse = await dialog.startCustomerOrderInteraction<TClientResponse>(interaction);

if (clientResponse.requiresTan) {
this.openCustomerInteractions.set(clientResponse.tanReference!, interaction);
} else {
await dialog.end();
}

return clientResponse;
): Promise<Map<string, ClientResponse>> {
this.currentDialog = new Dialog(this.config, false);
this.currentDialog.addCustomerInteraction(interaction);
return await this.currentDialog.start();
}

private async continueCustomerInteractionWithTan<TClientResponse extends ClientResponse>(
private async continueCustomerInteractionWithTan(
tanReference: string,
tan?: string
): Promise<TClientResponse> {
const interaction = this.openCustomerInteractions.get(tanReference);

if (!interaction) {
throw new Error('No open customer interaction found for TAN reference: ' + tanReference);
}

const dialog = interaction.dialog!;
let responseMessage = await dialog.sendTanMessage(interaction.segId, tanReference, tan);
let clientResponse = interaction.getClientResponse(responseMessage) as TClientResponse;

this.openCustomerInteractions.delete(tanReference);

if (!clientResponse.success) {
await dialog.end();
return clientResponse;
}

if (clientResponse.requiresTan) {
this.openCustomerInteractions.set(clientResponse.tanReference!, interaction);
return clientResponse;
}

const initDialogInteraction = interaction as InitDialogInteraction;
if (initDialogInteraction.followUpInteraction) {
clientResponse = await dialog.startCustomerOrderInteraction<TClientResponse>(
initDialogInteraction.followUpInteraction
);

if (clientResponse.requiresTan) {
this.openCustomerInteractions.set(clientResponse.tanReference!, initDialogInteraction.followUpInteraction);
return clientResponse;
}
}

await dialog.end();
return clientResponse;
}

private async initDialog(
dialog: Dialog,
syncSystemId = false,
followUpInteraction?: CustomerOrderInteraction
): Promise<InitResponse> {
const interaction = new InitDialogInteraction(this.config, syncSystemId, followUpInteraction);
const initResponse = await dialog.initialize(interaction);

if (initResponse.requiresTan) {
this.openCustomerInteractions.set(initResponse.tanReference!, interaction);
): Promise<Map<string, ClientResponse>> {
if (!this.currentDialog) {
throw new Error('no customer dialog was started which can continue');
}

return initResponse;
return await this.currentDialog.continue(tanReference, tan);
}
}
32 changes: 32 additions & 0 deletions src/dataGroups/SepaAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
import { DataGroup } from './DataGroup.js';
import { Identification } from '../dataElements/Identification.js';
import { BankIdentification } from './BankIdentification.js';
import { Account } from './Account.js';
import { YesNo } from '../dataElements/YesNo.js';

export type SepaAccount = Account & {
isSepaAccount?: boolean;
iban?: string;
bic?: string;
};

export class SepaAccountGroup extends DataGroup {
constructor(name: string, minCount = 0, maxCount = 1, minVersion?: number, maxVersion?: number) {
super(
name,
[
new YesNo('isSepaAccount', 1, 1),
new AlphaNumeric('iban', 0, 1, 34),
new AlphaNumeric('bic', 0, 1, 11),
new Identification('accountNumber', 1, 1),
new Identification('subAccountId', 0, 1),
new BankIdentification('bank', 1, 1),
],
minCount,
maxCount,
minVersion,
maxVersion
);
}
}
33 changes: 33 additions & 0 deletions src/dataGroups/SepaAccountParameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { YesNo } from '../dataElements/YesNo.js';
import { Numeric } from '../dataElements/Numeric.js';
import { AlphaNumeric } from '../dataElements/AlphaNumeric.js';
import { DataGroup } from './DataGroup.js';

export type SepaAccountParameters = {
individualAccountRetrievalAllowed: boolean;
nationalAccountAllowed: boolean;
structuredPurposeAllowed: boolean;
maxEntriesAllowed?: boolean; // version 2+
reservedPurposePositions?: number; // version 3+
supportedSepaFormats?: string[]; // optional, up to 99 entries
};

export class SepaAccountParametersGroup extends DataGroup {
constructor(name: string, minCount = 1, maxCount = 1, minVersion?: number, maxVersion?: number) {
super(
name,
[
new YesNo('individualAccountRetrievalAllowed', 1, 1),
new YesNo('nationalAccountAllowed', 1, 1),
new YesNo('structuredPurposeAllowed', 1, 1),
new YesNo('maxEntriesAllowed', 1, 1, 2), // version 2+
new Numeric('reservedPurposePositions', 1, 1, 2, 3), // version 3+
new AlphaNumeric('supportedSepaFormats', 0, 99, 256), // optional, up to 99 entries
],
minCount,
maxCount,
minVersion,
maxVersion
);
}
}
Loading