/* tslint:disable: no-object-literal-type-assertion no-useless-cast */ /** * Transfer Service. */ import { IAPIGatewayEvent, IDENTIFIERS as CORE, IDynamoDbEvent, IDynamoRecord, ILogger, ISnsEvent, KushkiError, } from "@kushki/core"; import { Context } from "aws-lambda"; import { PublishResponse } from "aws-sdk/clients/sns"; import { DocumentClient } from "aws-sdk/lib/dynamodb/document_client"; import { BANKS } from "constant/Banks"; import { IDENTIFIERS } from "constant/Identifiers"; import { FIREHOSE_RESOURCES, SNS_RESOURCES, TABLE_RESOURCES, } from "constant/Resources"; import { CONTAINER } from "infrastructure/Container"; import { ERRORS } from "infrastructure/ErrorEnum"; import QueryOutput = DocumentClient.QueryOutput; import { ProcessorTypeEnum } from "infrastructure/ProcessorTypeEnum"; import { PseEnum } from "infrastructure/PseEnum"; import { PseErrorEnum } from "infrastructure/PseErrorEnum"; import { TransactionColumnEnum } from "infrastructure/TransactionColumnEnum"; import { TransactionRulesEnum } from "infrastructure/TransactionRulesEnum"; import { TransactionStatusEnum } from "infrastructure/TransactionStatusEnum"; import { USER_TYPE, UserTypeEnum } from "infrastructure/UserTypeEnum"; import { inject, injectable } from "inversify"; import * as momt from "moment"; import * as moment from "moment-timezone"; import { gMerchantObject, gTransactionFetch } from "repository/Guard"; import { IDynamoGateway } from "repository/IDynamoGateway"; import { IFirehoseGateway } from "repository/IFirehoseGateway"; import { IKushkiService } from "repository/IKushkiService"; import { ILambdaGateway } from "repository/ILambdaGateway"; import QueryInput = DocumentClient.QueryInput; import { IPseGateway } from "repository/IPseGateway"; import { ISNSGateway } from "repository/ISNSGateway"; import { ITransferService } from "repository/ITransferService"; import { StatusCodeError } from "request-promise/errors"; import { forkJoin, from, iif, Observable, of, throwError } from "rxjs"; import { tag } from "rxjs-spy/operators"; import { catchError, concatMap, count, delay, flatMap, map, mergeMap, switchMap, } from "rxjs/operators"; import tsLogClass from "ts-log-class"; import { AgentRequestParameters, Ticket, Token, } from "types/agent_request_parameters"; import { ChargeSnsMessage } from "types/charge_sns_message"; import { DynamoTransactionSns } from "types/dynamo_transaction_sns"; import { InitRequest } from "types/init_request"; import { LambdaTransactionRuleResponse } from "types/lambda_transaction_rule_response"; import { MerchantObject } from "types/merchant_object"; import { PseCreateTransaction } from "types/pse_create_transaction"; import { PseCreateTransactionRequest } from "types/pse_create_transaction_request"; import { PseGetBankList } from "types/pse_get_bank_list"; import { PseGetTransactionInformationRequest } from "types/pse_get_transaction_information_request"; import { PseGetTransactionInformationResponse } from "types/pse_get_transaction_information_response"; import { StatusRequestParameters } from "types/status_request_parameters"; import { TokenRequest } from "types/token_request"; import { TransactionFetch } from "types/transaction_fetch"; import * as url from "url"; /** * Implementation */ @injectable() @tsLogClass() export class TransferService implements ITransferService { private readonly _logger: ILogger; private readonly _storage: IDynamoGateway = CONTAINER.get( IDENTIFIERS.DynamoGateway ); private readonly _gateway: IPseGateway = CONTAINER.get( IDENTIFIERS.PseGateway ); private readonly _kushki: IKushkiService = CONTAINER.get( IDENTIFIERS.KushkiService ); private readonly _snsGateway: ISNSGateway = CONTAINER.get( IDENTIFIERS.SNSGateway ); private readonly _firehose: IFirehoseGateway = CONTAINER.get< IFirehoseGateway >(IDENTIFIERS.FirehoseGateway); private readonly _lambda: ILambdaGateway = CONTAINER.get( IDENTIFIERS.LambdaGateway ); constructor(@inject(CORE.ILogger) logger: ILogger) { this._logger = logger; } private static _isTokenAgent(input: AgentRequestParameters): input is Token { return input !== undefined && "token" in input; } private static _isTicketAgent( input: AgentRequestParameters ): input is Ticket { return input !== undefined && "ticketId" in input; } private static _buildTransferObject( record: IDynamoRecord ): object { if ( record.dynamodb.NewImage === null || (record.dynamodb.NewImage.status !== TransactionStatusEnum.ApprovedTransaction && record.dynamodb.NewImage.status !== TransactionStatusEnum.DeclinedTransaction) ) return {}; let sum_taxes: number = 0; const extra_taxes: object | undefined = record.dynamodb.NewImage.amount.extraTaxes; if (extra_taxes !== undefined) Object.keys(extra_taxes).forEach((key: string) => { sum_taxes += extra_taxes[key]; }); const total_amount: number = Number(record.dynamodb.NewImage.amount.iva) + Number(record.dynamodb.NewImage.amount.subtotalIva) + Number(record.dynamodb.NewImage.amount.subtotalIva0) + Number(sum_taxes); return { transaction_id: record.dynamodb.NewImage.created.toString(), ticket_number: record.dynamodb.NewImage.ticketNumber, // TODO Change to variable currency code // TODO Add response_code and response_text (PSE errors) currency_code: "COP", taxes: JSON.stringify(extra_taxes), // TODO Change on PSE refund transaction_type: "SALE", payment_brand: "PSE", metadata: record.dynamodb.NewImage.metadata, created: momt(record.dynamodb.NewImage.created) .utc() .format("YYYY-MM-DD HH:mm:ss"), request_amount: total_amount, approved_transaction_amount: record.dynamodb.NewImage.status === TransactionStatusEnum.ApprovedTransaction ? total_amount : 0, approval_code: record.dynamodb.NewImage.trazabilityCode, iva_value: record.dynamodb.NewImage.amount.iva, subtotal_iva0: record.dynamodb.NewImage.amount.subtotalIva0, subtotal_iva: record.dynamodb.NewImage.amount.subtotalIva, transaction_status: record.dynamodb.NewImage.status === TransactionStatusEnum.ApprovedTransaction ? "APPROVAL" : "DECLINED", sync_mode: "api", processor_id: record.dynamodb.NewImage.publicMerchantId, processor_name: "ACH Processor", bank_id: record.dynamodb.NewImage.bankId, document_type: record.dynamodb.NewImage.documentType, document_number: record.dynamodb.NewImage.documentNumber, payment_description: record.dynamodb.NewImage.paymentDescription, user_type: record.dynamodb.NewImage.userType, }; } private static _buildElasticObject( record: IDynamoRecord ): object { if ( record.dynamodb.NewImage === null || (record.dynamodb.NewImage.status !== TransactionStatusEnum.ApprovedTransaction && record.dynamodb.NewImage.status !== TransactionStatusEnum.DeclinedTransaction) ) return {}; let sum_extra_taxes: number = 0; const extra_taxes: object | undefined = record.dynamodb.NewImage.amount.extraTaxes; if (extra_taxes !== undefined) Object.keys(extra_taxes).forEach((key: string) => { sum_extra_taxes += extra_taxes[key]; }); const total: number = Number(record.dynamodb.NewImage.amount.subtotalIva0) + Number(record.dynamodb.NewImage.amount.subtotalIva) + Number(record.dynamodb.NewImage.amount.iva) + Number(sum_extra_taxes); return { ticket_number: record.dynamodb.NewImage.ticketNumber, transaction_id: record.dynamodb.NewImage.created.toString(), // TODO Change to variable currency code // TODO Add response_code and response_text (PSE errors) currency_code: "COP", taxes: record.dynamodb.NewImage.amount.extraTaxes, // TODO Change on PSE refund transaction_type: "SALE", // TODO Change finding merchant on dynamo merchant_id: record.dynamodb.NewImage.publicMerchantId, payment_brand: "PSE", created: momt(record.dynamodb.NewImage.created) .utc() .toISOString(), request_amount: total, approved_transaction_amount: record.dynamodb.NewImage.status === TransactionStatusEnum.ApprovedTransaction ? total : 0, approval_code: record.dynamodb.NewImage.trazabilityCode, subtotal_iva: record.dynamodb.NewImage.amount.subtotalIva, subtotal_iva0: record.dynamodb.NewImage.amount.subtotalIva0, iva_value: record.dynamodb.NewImage.amount.iva, metadata: record.dynamodb.NewImage.metadata, transaction_status: record.dynamodb.NewImage.status === TransactionStatusEnum.ApprovedTransaction ? "APPROVAL" : "DECLINED", sync_mode: "api", processor_id: record.dynamodb.NewImage.publicMerchantId, processor_name: "ACH Processor", bank_id: record.dynamodb.NewImage.bankId, bank_name: BANKS[record.dynamodb.NewImage.bankId], document_type: record.dynamodb.NewImage.documentType, document_number: record.dynamodb.NewImage.documentNumber, payment_description: record.dynamodb.NewImage.paymentDescription, user_type: record.dynamodb.NewImage.userType === UserTypeEnum.Natural ? USER_TYPE[UserTypeEnum.Natural].name : USER_TYPE[UserTypeEnum.Juridica].name, payment_method: "transfer", }; } public getToken( event: IAPIGatewayEvent, _context: Context ): Observable { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: event.requestContext.authorizer.merchantId, // TODO publicMerchantId is merchantId in this usrv [@pmoreanoj] }) .pipe( map((result: MerchantObject | undefined) => { if (result === undefined || result === null) throw new KushkiError(ERRORS.E007); if (!("publicMerchantId" in result)) throw new KushkiError(ERRORS.E007); const token: string = this._kushki.generateToken(); return { token, bankId: event.body.bankId, amount: event.body.amount, callbackUrl: event.body.callbackUrl, userType: event.body.userType, documentType: event.body.documentType, documentNumber: event.body.documentNumber, created: Number(new Date().getTime().toString()), status: TransactionStatusEnum.RequestedToken, userIp: event.headers["X-FORWARDED-FOR"].split(",")[0].trim(), publicMerchantId: result.publicMerchantId, paymentDescription: event.body.paymentDescription === undefined || event.body.paymentDescription === null ? "-" : event.body.paymentDescription, }; }), concatMap((dynamoToken: TransactionFetch) => this._storage.put(dynamoToken, TABLE_RESOURCES.TransactionTable).pipe( map((result: boolean) => { if (!result) throw new KushkiError(ERRORS.E002); return { token: dynamoToken.token }; }) ) ) ); } public bankList( event: IAPIGatewayEvent, _context: Context ): Observable { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: event.requestContext.authorizer.merchantId, }) .pipe( map((result: object | undefined) => { if (result === undefined) throw new KushkiError(ERRORS.E007); if (!("publicMerchantId" in result)) throw new KushkiError(ERRORS.E007); return result; }), concatMap((merchant: MerchantObject) => this._gateway.getBankListRequest(merchant.entityCode, merchant.type) ), map((bankListResponse: PseGetBankList) => { const response: object[] = []; let cont: number = 0; for (const record of bankListResponse.getBankListResponseInformation) { response[cont] = { code: record.financialInstitutionCode, name: record.financialInstitutionName, }; cont += 1; } return response; }) ); } public queueWebhooks( event: IDynamoDbEvent, _context: Context ): Observable { return from(event.Records).pipe( concatMap((record: IDynamoRecord) => iif( () => record.dynamodb.NewImage !== null && (record.dynamodb.NewImage.status === TransactionStatusEnum.ApprovedTransaction || record.dynamodb.NewImage.status === TransactionStatusEnum.DeclinedTransaction) && (record.dynamodb.OldImage === null || record.dynamodb.OldImage.status !== record.dynamodb.NewImage.status), forkJoin([ this._storage.getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: record.dynamodb.NewImage.publicMerchantId, }), this._firehose.put( FIREHOSE_RESOURCES.Redshift, TransferService._buildTransferObject(record) ), this._firehose.put( FIREHOSE_RESOURCES.Elastic, TransferService._buildElasticObject(record) ), ]).pipe( switchMap((data: [object | undefined, boolean, boolean]) => { const element: object | undefined = data[0]; if (element === undefined) throw new KushkiError(ERRORS.E007); if (!gMerchantObject(element)) throw new KushkiError(ERRORS.E007); const merchant: MerchantObject = element; if (merchant.url === undefined) return from([]); return from(merchant.url); }), mergeMap((urlHook: string) => this._snsGateway.publish(SNS_RESOURCES.WebhookTopicArn, { transaction: record.dynamodb.NewImage, url: urlHook, }) ) ), of(true) ) ), tag("Transfer Service | queueWebhooks") ); } public status( event: IAPIGatewayEvent< null, StatusRequestParameters, null, { merchantId: string } >, _context: Context ): Observable { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: event.requestContext.authorizer.merchantId, }) .pipe( map((merchant: object | undefined) => this._validateMerchant(merchant)), switchMap(() => this._storage.getItem(TABLE_RESOURCES.TransactionTable, { token: event.pathParameters.token, }) ), map((result: object | undefined) => { if (result === undefined) throw new KushkiError(ERRORS.E004); if (!("bankId" in result)) throw new KushkiError(ERRORS.E004); return result; }) ); } public static getSolicitedate(): string { return moment() .tz("America/Bogota") .format("YYYY-MM-DD"); } public getTotalAmount(transaction: TransactionFetch): number { let vat_value: number = 0; vat_value += transaction.amount.iva; if (transaction.amount.extraTaxes !== undefined) vat_value = this._extraTaxes(transaction.amount.extraTaxes, vat_value); return vat_value; } public static transactionValue( transaction: TransactionFetch, vatValue: number ): number { return ( Number(transaction.amount.subtotalIva) + Number(transaction.amount.subtotalIva0) + Number(vatValue) ); } // TODO: Seguros Mundial hack, delete after siftscience on transfer - private method _validateTransactionRules /* tslint:disable:max-func-body-length */ public init( event: IAPIGatewayEvent, _context: Context ): Observable { const thirty_minutes: number = 1800000; const ticket_number: string = this._kushki.generateTicketNumber(); let vat_value: number = 0; let transaction_token: TransactionFetch; let transaction_mid: string = ""; let trazability_code: string = ""; return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: event.requestContext.authorizer.merchantId, }) .pipe( map((merchant: object | undefined) => this._validateMerchant(merchant)), switchMap(() => this._storage.getItem(TABLE_RESOURCES.TransactionTable, { token: event.body.token, }) ), map((result: object | undefined) => { if (result === undefined) throw new KushkiError(ERRORS.E004); if (!("bankId" in result)) throw new KushkiError(ERRORS.E004); const transaction: TransactionFetch = result; [transaction_token, transaction_mid] = [ transaction, transaction.publicMerchantId, ]; if (this._kushki.getTime() - transaction.created >= thirty_minutes) throw new KushkiError(ERRORS.E005); vat_value = this.getTotalAmount(transaction); return transaction; }), concatMap((transactionFetch: TransactionFetch) => forkJoin( this._validateTransactionRules( transactionFetch, event.body.metadata ), this._getMerchantObject(transactionFetch), this._lambda.invokeFunction<{ body: LambdaTransactionRuleResponse; }>(`${process.env.LAMBDA_RULE}`, { body: JSON.stringify({ merchantId: event.requestContext.authorizer.merchantId, transactionKind: "transfer", userIp: transactionFetch.userIp, documentNumber: transactionFetch.documentNumber, amount: transactionFetch.amount, }), }) ) ), concatMap( ( transaction: [ boolean, [TransactionFetch, Required], { body: LambdaTransactionRuleResponse } ] ) => { if (!transaction[0]) throw new KushkiError(ERRORS.E008); const reference_number: string[] = TransferService._getReferences( transaction[1][0], transaction[1][1], ticket_number ); return this._gateway.createTransactionRequest( this._buildCreateTransactionRequest( transaction, vat_value, ticket_number, reference_number ), transaction[1][1].type ); } ), concatMap((pseCreateTransactionResponse: PseCreateTransaction) => { trazability_code = pseCreateTransactionResponse .createTransactionPaymentResponseInformation.trazabilityCode; return this._storage.put( { ...transaction_token, trazabilityCode: pseCreateTransactionResponse .createTransactionPaymentResponseInformation.trazabilityCode, returnCode: pseCreateTransactionResponse .createTransactionPaymentResponseInformation.returnCode, bankurl: pseCreateTransactionResponse .createTransactionPaymentResponseInformation.bankurl, transactionCycle: pseCreateTransactionResponse .createTransactionPaymentResponseInformation.transactionCycle, status: TransactionStatusEnum.InitializedTransaction, ticketNumber: ticket_number, metadata: event.body.metadata, }, TABLE_RESOURCES.TransactionTable ); }), map(() => ({ redirectUrl: `${process.env.PSE_AGENT_URL}?token=${ transaction_token.token }&mid=${transaction_mid}`, trazabilityCode: trazability_code, })) ); } private static _getReferences( transaction: TransactionFetch, merchant: Required, ticketNumber: string ): string[] { let reference_number: string[]; if (merchant.type === ProcessorTypeEnum.GATEWAY) reference_number = [ `${transaction.documentType}${transaction.documentNumber}`, transaction.userIp, ticketNumber, ]; else reference_number = [ merchant.merchantType, merchant.taxId, merchant.businessType, ]; return reference_number; } public webhook( event: ISnsEvent, _context: Context ): Observable { const one_hour: number = 3600000; const server_errors: number = 500; const topic_arn: string = event.Records[0].Sns.TopicArn; const body: DynamoTransactionSns = event.Records[0].Sns.Message; return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: body.transaction.publicMerchantId, }) .pipe( concatMap((result: object | undefined) => { if (result === undefined) throw new KushkiError(ERRORS.E007); if (!gMerchantObject(result)) throw new KushkiError(ERRORS.E007); const merchant: MerchantObject = result; delete body.transaction.bankurl; delete body.transaction.callbackUrl; if ( body.transaction.status === TransactionStatusEnum.DeclinedTransaction ) delete body.transaction.ticketNumber; return this._kushki.buildWebHookRequest( body.transaction, merchant.webhookSignature, body.url ); }), catchError((err: StatusCodeError) => { if ( err.statusCode !== undefined && err.statusCode >= server_errors && this._kushki.getTime() - body.transaction.created < one_hour ) return this._snsGateway.publish(topic_arn, body); return of(true); }) ); } public charge( event: ISnsEvent, _context: Context ): Observable { const topic_arn: string = event.Records[0].Sns.TopicArn; const body_with_token: ChargeSnsMessage = event.Records[0].Sns.Message; const get_information_request: PseGetTransactionInformationRequest = { entityCode: body_with_token.entityCode, trazabilityCode: body_with_token.trazabilityCode, }; return of(1).pipe( delay(180000), switchMap(() => this._storage.getItem(TABLE_RESOURCES.TransactionTable, { token: body_with_token.token, }) ), map((transaction: object | undefined) => { if (transaction === undefined) throw new KushkiError(ERRORS.E004); if (!gTransactionFetch(transaction)) throw new KushkiError(ERRORS.E004); return transaction; }), switchMap((transactionToken: TransactionFetch) => iif( () => transactionToken.status !== TransactionStatusEnum.ApprovedTransaction && transactionToken.status !== TransactionStatusEnum.DeclinedTransaction, this._processCharge( transactionToken, get_information_request, topic_arn, body_with_token ), of(true) ) ) ); } public agent( event: IAPIGatewayEvent, _context: Context ): Observable { if (TransferService._isTicketAgent(event.queryStringParameters)) return this._agentTicket(event.queryStringParameters); if (TransferService._isTokenAgent(event.queryStringParameters)) { const time: number = new Date().getTime(); const real_time_stamp: string = time.toString(); const real_time: number = Number(real_time_stamp); const thirty_minutes: number = 1800000; return this._tokenAgent( event.queryStringParameters, real_time, thirty_minutes ); } return throwError(new KushkiError(ERRORS.E004)); } public call(event: { method: string; args: object; processorType: ProcessorTypeEnum; }): Observable { return this._gateway.call(event.method, event.args, event.processorType); } private _processCharge( transactionToken: TransactionFetch, getInformationRequest: PseGetTransactionInformationRequest, topicArn: string, bodyWithToken: ChargeSnsMessage ): Observable { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: transactionToken.publicMerchantId, }) .pipe( concatMap((result: object | undefined) => { if (result === undefined) throw new KushkiError(ERRORS.E007); if (!gMerchantObject(result)) throw new KushkiError(ERRORS.E007); return forkJoin( this._gateway.getTransactionInformation( getInformationRequest, result.type ), of(result.type) ); }), catchError((err: Error) => this._snsGateway .publish(topicArn, bodyWithToken) .pipe(switchMap(() => throwError(err))) ), concatMap((data: [PseGetTransactionInformationResponse, string]) => iif( () => data[0].getTransactionInformationResponseBody.transactionState === PseEnum.Pending, this._snsGateway.publish(topicArn, bodyWithToken), this._chargePut( transactionToken, data[0], getInformationRequest, data[1] ) ) ) ); } private _extraTaxes(extraTaxes: object, vatValue: number): number { const extra_taxes: object = extraTaxes; let total: number = vatValue; Object.keys(extra_taxes).forEach((key: string) => { total += extra_taxes[key]; }); return total; } private _tokenAgent( params: Token, realTime: number, thirtyMinutes: number ): Observable { return this._storage .getItem(TABLE_RESOURCES.TransactionTable, { token: params.token }) .pipe( map((data: object | undefined) => { if (data === undefined) throw new KushkiError(ERRORS.E004); if (!gTransactionFetch(data)) throw new KushkiError(ERRORS.E004); const transaction: TransactionFetch = data; if (realTime - transaction.created >= thirtyMinutes) throw new KushkiError(ERRORS.E005); return transaction; }), concatMap((transaction: TransactionFetch) => forkJoin( of(transaction), this._getMerchantWithPublic(transaction.publicMerchantId) ) ), concatMap((transaction: [TransactionFetch, MerchantObject]) => { const token: string = transaction[0].token; const trazability_code: string | undefined = transaction[0].trazabilityCode; const entity_code: string | undefined = transaction[1].entityCode; return this._snsGateway .publish(SNS_RESOURCES.ChargeTopicArn, { token, trazabilityCode: trazability_code, entityCode: entity_code, }) .pipe(map(() => ({ redirectUrl: transaction[0].bankurl }))); }) ); } private _getMerchantWithPublic(publicId: string): Observable { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: publicId, }) .pipe( map((data: object | undefined) => { if (data === undefined) throw new KushkiError(ERRORS.E007); if (!gMerchantObject(data)) throw new KushkiError(ERRORS.E007); return data; }) ); } private _agentTicket(params: Ticket): Observable { return this._storage .query({ IndexName: TransactionColumnEnum.TICKET_NUMBER, KeyConditionExpression: `${ TransactionColumnEnum.TICKET_NUMBER }= :ticketNumber`, ExpressionAttributeValues: { ":ticketNumber": params.ticketId, }, TableName: TABLE_RESOURCES.TransactionTable, }) .pipe( map((items: QueryOutput) => { if ( items.Items === undefined || (items.Items !== undefined && items.Items.length === 0) ) throw new KushkiError(ERRORS.E004); return items.Items[0]; }), switchMap((row: TransactionFetch) => forkJoin(this._getMerchantWithPublic(row.publicMerchantId), of(row)) ), switchMap( (data: [Required, Required]) => { const get_information_request: PseGetTransactionInformationRequest = { entityCode: data[0].entityCode, trazabilityCode: data[1].trazabilityCode, }; return forkJoin( this._gateway.getTransactionInformation( get_information_request, data[0].type ), of(data[1]), of(get_information_request), of(data[0].type) ); } ), switchMap( ( data: [ PseGetTransactionInformationResponse, TransactionFetch, PseGetTransactionInformationRequest, string ] ) => iif( () => data[0].getTransactionInformationResponseBody .transactionState === PseEnum.Pending, of([true, data[1]]), forkJoin( this._chargePut(data[1], data[0], data[2], data[3]), of(data[1]) ) ) ), map((data: [boolean, TransactionFetch]) => { const row: TransactionFetch = data[1]; const url_parse: url.UrlWithParsedQuery = url.parse( row.callbackUrl, true ); url_parse.search = undefined; url_parse.query = { ...url_parse.query, token: row.token, }; const url_response: string = url.format(url_parse); return { redirectUrl: url_response }; }) ); } private _getMerchantObject( transactionFetch: TransactionFetch ): Observable<[TransactionFetch, MerchantObject]> { return this._storage .getItem(TABLE_RESOURCES.MerchantsTable, { publicMerchantId: transactionFetch.publicMerchantId, }) .pipe( map((data: object | undefined) => { if (data === undefined) throw new KushkiError(ERRORS.E007); if (gMerchantObject(data) !== true) throw new KushkiError(ERRORS.E007); const merchant: MerchantObject = data; return <[TransactionFetch, MerchantObject]>[ transactionFetch, merchant, ]; }) ); } private _validateTransactionRules( transactionValidate: TransactionFetch, metadataTransaction: object | undefined ): Observable { let transaction_restricted: boolean = false; if (metadataTransaction !== undefined) Object.keys(metadataTransaction).forEach((key: string) => { if (key === TransactionRulesEnum.TRX_RESTRICTED) transaction_restricted = metadataTransaction[key]; }); if (!transaction_restricted) { this._logger.info( `Transaction Restricted value : ${transaction_restricted}` ); return of(true); } if (!this._validateAmountRule(transactionValidate)) return of(false); return forkJoin( this._validateDocumentIdMonth(transactionValidate), this._validateUserIpMonth(transactionValidate) ).pipe( map( (data: [boolean, boolean]) => data.filter((element: boolean) => !element).length === 0 ) ); } private _validateDocumentIdMonth( transactionValidate: TransactionFetch ): Observable { const input_data: QueryInput = { IndexName: TransactionColumnEnum.DOCUMENT_ID, KeyConditionExpression: `${ TransactionColumnEnum.DOCUMENT_ID }= :documentNumber and ( ${ TransactionColumnEnum.CREATED } between :from and :to ) `, ExpressionAttributeValues: { ":documentNumber": transactionValidate.documentNumber, ":from": new Date().getTime() - Number(process.env.RULE_DAYS_RULE) * 86400000, ":to": new Date().getTime(), }, TableName: TABLE_RESOURCES.TransactionTable, }; return this._queryTransactions(input_data, transactionValidate); } private _validateUserIpMonth( transactionValidate: TransactionFetch ): Observable { const input_data: QueryInput = { IndexName: TransactionColumnEnum.USER_IP, KeyConditionExpression: `${ TransactionColumnEnum.USER_IP }= :userIp and ( ${ TransactionColumnEnum.CREATED } between :from and :to ) `, ExpressionAttributeValues: { ":userIp": transactionValidate.userIp, ":from": new Date().getTime() - Number(process.env.RULE_DAYS_RULE) * 86400000, ":to": new Date().getTime(), }, TableName: TABLE_RESOURCES.TransactionTable, }; return this._queryTransactions(input_data, transactionValidate); } private _queryTransactions( inputData: DocumentClient.QueryInput, transactionValidate: TransactionFetch ): Observable { return this._storage.query(inputData).pipe( flatMap((items: QueryOutput) => { if ( items.Items === undefined || (items.Items !== undefined && items.Items.length === 0) ) return from([]); return from(items.Items); }), count( (items: TransactionFetch) => items.publicMerchantId === transactionValidate.publicMerchantId && items.status === TransactionStatusEnum.ApprovedTransaction ), map((val: number) => { this._logger.info( ` Trx processed : ${val} : Max trx allowed : ${ process.env.RULE_NUM_TRX }` ); return val < Number(process.env.RULE_NUM_TRX); }) ); } private _validateAmountRule(transactionValidate: TransactionFetch): boolean { let sum_taxes: number = 0; const extra_taxes: object | undefined = transactionValidate.amount.extraTaxes; this._logger.info( `Merchant Id received : ${ transactionValidate.publicMerchantId } Merchant Id configured : ${process.env.RULE_MERCHANT_ID}` ); if (transactionValidate.publicMerchantId !== process.env.RULE_MERCHANT_ID) return true; if (extra_taxes !== undefined) Object.keys(extra_taxes).forEach((key: string) => { sum_taxes += extra_taxes[key]; }); const total_amount: number = Number(transactionValidate.amount.iva) + Number(transactionValidate.amount.subtotalIva) + Number(transactionValidate.amount.subtotalIva0) + Number(sum_taxes); this._logger.info( ` Total Amount : ${total_amount} Max Amount configured : ${ process.env.RULE_MAX_AMOUNT }` ); return total_amount < Number(process.env.RULE_MAX_AMOUNT); } private _chargePut( transactionToken: TransactionFetch, data: PseGetTransactionInformationResponse, getInformationRequest: PseGetTransactionInformationRequest, processorType: string ): Observable { return this._storage .put( { ...transactionToken, status: data.getTransactionInformationResponseBody.transactionState === PseEnum.OK ? TransactionStatusEnum.ApprovedTransaction : TransactionStatusEnum.DeclinedTransaction, processorState: data.getTransactionInformationResponseBody.transactionState, responseCode: data.getTransactionInformationResponseBody.transactionState, responseText: data.getTransactionInformationResponseBody.transactionState !== PseEnum.OK ? PseErrorEnum[ data.getTransactionInformationResponseBody.returnCode ] : PseEnum.OK, }, TABLE_RESOURCES.TransactionTable ) .pipe( concatMap(() => iif( () => data.getTransactionInformationResponseBody.transactionState === PseEnum.OK, this._gateway.finalizeTransactionPayment( getInformationRequest, processorType ), of(true) ) ) ); } private _validateMerchant(merchant: object | undefined): MerchantObject { if (merchant === undefined) throw new KushkiError(ERRORS.E007); if (!("publicMerchantId" in merchant)) throw new KushkiError(ERRORS.E007); return merchant; } private _buildCreateTransactionRequest( transaction: [ boolean, [TransactionFetch, Required], { body: LambdaTransactionRuleResponse } ], vatValue: number, ticketNumber: string, referenceNumber: string[] ): PseCreateTransactionRequest { console.error("PATOTEST"); console.error(transaction); return { financialInstitutionCode: transaction[1][0].bankId, entityCode: transaction[2].body.entityCode, serviceCode: transaction[2].body.serviceCode, transactionValue: TransferService.transactionValue( transaction[1][0], vatValue ), vatValue: vatValue, ticketId: ticketNumber, entityurl: transaction[1][0].callbackUrl, userType: transaction[1][0].userType, referenceNumber: referenceNumber, soliciteDate: TransferService.getSolicitedate(), paymentDescription: transaction[1][0].paymentDescription, }; } }