-
-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(general): import and process payments files from postfinance …
…ftp server (#764)
- Loading branch information
Showing
12 changed files
with
268 additions
and
89 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,16 @@ | ||
POSTFINANCE_EMAIL_USER=[email protected] | ||
POSTFINANCE_EMAIL_PASSWORD=password | ||
POSTFINANCE_PAYMENTS_FILES_BUCKET=postfinance-payments-files | ||
POSTFINANCE_FTP_HOST=ftp.postfinance.example | ||
POSTFINANCE_FTP_PORT=21 | ||
POSTFINANCE_FTP_USER=example | ||
POSTFINANCE_FTP_RSA_PRIVATE_KEY=" | ||
-----BEGIN RSA PRIVATE KEY----- | ||
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | ||
... | ||
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | ||
-----END RSA PRIVATE KEY----- | ||
" | ||
|
||
# To work with the Twilio API locally, configure the test credentials from Twilio in your .env file | ||
TWILIO_SID=ACXXXXXXXXXXXXXXXXXXXX | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import importExchangeRatesFunction from './exchange-rate-import'; | ||
import importBalanceMailFunction from './postfinance-balance-import'; | ||
import importPostfinancePaymentsFilesFunction from './postfinance-payments-files-import'; | ||
|
||
export const importBalanceMail = importBalanceMailFunction; | ||
export const importPostfinancePaymentsFiles = importPostfinancePaymentsFilesFunction; | ||
export const importExchangeRates = importExchangeRatesFunction; |
8 changes: 8 additions & 0 deletions
8
functions/src/cron/postfinance-payments-files-import/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { onSchedule } from 'firebase-functions/v2/scheduler'; | ||
import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../config'; | ||
import { PostfinancePaymentsFileHandler } from '../../utils/PostfinancePaymentsFileHandler'; | ||
|
||
export default onSchedule('0 * * * *', async () => { | ||
const paymentsFileHandler = new PostfinancePaymentsFileHandler(POSTFINANCE_PAYMENTS_FILES_BUCKET); | ||
await paymentsFileHandler.importPaymentFiles(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import onPostfinancePaymentsFileCreatedFunction from './postfinance-payments-files'; | ||
|
||
export const onPostfinancePaymentsFileCreated = onPostfinancePaymentsFileCreatedFunction; | ||
export const onPostfinancePaymentsFileUploaded = onPostfinancePaymentsFileCreatedFunction; |
70 changes: 0 additions & 70 deletions
70
functions/src/storage/postfinance-payments-files/PostfinancePaymentsFileImporter.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
import { onObjectFinalized } from 'firebase-functions/v2/storage'; | ||
import { POSTFINANCE_PAYMENTS_FILES_BUCKET } from '../../config'; | ||
import { PostfinancePaymentsFileImporter } from './PostfinancePaymentsFileImporter'; | ||
import { PostfinancePaymentsFileHandler } from '../../utils/PostfinancePaymentsFileHandler'; | ||
|
||
export default onObjectFinalized({ bucket: POSTFINANCE_PAYMENTS_FILES_BUCKET }, async (event) => { | ||
const paymentsFileImporter = new PostfinancePaymentsFileImporter(POSTFINANCE_PAYMENTS_FILES_BUCKET); | ||
paymentsFileImporter.processPaymentFile(event.data.name); | ||
const paymentsFileHandler = new PostfinancePaymentsFileHandler(POSTFINANCE_PAYMENTS_FILES_BUCKET); | ||
await paymentsFileHandler.processPaymentFile(event.data.name); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import xmldom from '@xmldom/xmldom'; | ||
import { DateTime } from 'luxon'; | ||
import SFTPClient from 'ssh2-sftp-client'; | ||
import { withFile } from 'tmp-promise'; | ||
import xpath from 'xpath'; | ||
import { FirestoreAdmin } from '../../../shared/src/firebase/admin/FirestoreAdmin'; | ||
import { StorageAdmin } from '../../../shared/src/firebase/admin/StorageAdmin'; | ||
import { toFirebaseAdminTimestamp } from '../../../shared/src/firebase/admin/utils'; | ||
import { | ||
BankWireContribution, | ||
CONTRIBUTION_FIRESTORE_PATH, | ||
ContributionSourceKey, | ||
StatusKey, | ||
} from '../../../shared/src/types/contribution'; | ||
import { Currency } from '../../../shared/src/types/currency'; | ||
import { USER_FIRESTORE_PATH, User } from '../../../shared/src/types/user'; | ||
import { | ||
POSTFINANCE_FTP_HOST, | ||
POSTFINANCE_FTP_PORT, | ||
POSTFINANCE_FTP_RSA_PRIVATE_KEY, | ||
POSTFINANCE_FTP_USER, | ||
} from '../config'; | ||
|
||
export class PostfinancePaymentsFileHandler { | ||
private storageAdmin: StorageAdmin; | ||
private firestoreAdmin: FirestoreAdmin; | ||
private readonly bucketName; | ||
private readonly xmlSelectExpression = '//ns:BkToCstmrDbtCdtNtfctn/ns:Ntfctn/ns:Ntry/ns:NtryDtls/ns:TxDtls'; | ||
|
||
constructor(bucketName: string) { | ||
this.storageAdmin = new StorageAdmin(); | ||
this.firestoreAdmin = new FirestoreAdmin(); | ||
this.bucketName = bucketName; | ||
} | ||
|
||
/** | ||
* Imports payment files from the Postfinance SFTP server to the payments files storage bucket | ||
*/ | ||
async importPaymentFiles() { | ||
const sftp = new SFTPClient(); | ||
const bucket = this.storageAdmin.storage.bucket(this.bucketName); | ||
const files = (await bucket.getFiles())[0].map((file) => file.name); | ||
|
||
await sftp.connect({ | ||
host: POSTFINANCE_FTP_HOST, | ||
port: Number(POSTFINANCE_FTP_PORT), | ||
username: POSTFINANCE_FTP_USER, | ||
privateKey: POSTFINANCE_FTP_RSA_PRIVATE_KEY, | ||
}); | ||
const sftpFiles = await sftp.list('/yellow-net-reports'); | ||
for (let file of sftpFiles) { | ||
if (files.includes(file.name)) { | ||
console.info(`Skipped copying file ${file.name} because it already exists in ${this.bucketName} bucket`); | ||
continue; | ||
} | ||
|
||
await withFile(async ({ path }) => { | ||
await sftp.get(`/yellow-net-reports/${file.name}`, path); | ||
await this.storageAdmin.uploadFile({ | ||
bucket: bucket, | ||
sourceFilePath: path, | ||
destinationFilePath: file.name, | ||
}); | ||
console.info(`Successfully copied ${file.name} to ${this.bucketName} bucket`); | ||
}); | ||
} | ||
} | ||
|
||
/** | ||
* Processes a payment file from the payments files storage bucket | ||
* @param fileName The name of the file to process | ||
*/ | ||
processPaymentFile(fileName: string) { | ||
if (!fileName.startsWith('camt.054_P_')) { | ||
console.info(`Skipped processing ${fileName} because it does not contain relevant payment data`); | ||
return; | ||
} | ||
|
||
this.storageAdmin.storage | ||
.bucket(this.bucketName) | ||
.file(fileName) | ||
.download() | ||
.then(async (data) => { | ||
const xmlString = data[0].toString('utf8'); | ||
const xmlDoc = new xmldom.DOMParser().parseFromString(xmlString, 'text/xml'); | ||
const select = xpath.useNamespaces({ ns: 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.08' }); | ||
const nodes = select(this.xmlSelectExpression, xmlDoc) as Node[]; | ||
|
||
for (let node of nodes) { | ||
const transactionId = select('string(//ns:Refs/ns:EndToEndId)', node) as string; | ||
const referenceId = select('string(//ns:RmtInf/ns:Strd/ns:CdtrRefInf/ns:Ref)', node) as string; | ||
const userReferenceId = parseInt(referenceId.slice(7, 20)); | ||
console.info( | ||
`Processing transaction ${transactionId} with reference ${referenceId} for user ${userReferenceId}`, | ||
); | ||
|
||
const contribution: BankWireContribution = { | ||
reference_id: referenceId, | ||
transaction_id: transactionId, | ||
monthly_interval: parseInt(referenceId.slice(20, 22)), | ||
currency: (select('string(//ns:Amt/@Ccy)', node) as string).toUpperCase() as Currency, | ||
amount: parseFloat(select('string(//ns:Amt)', node) as string), | ||
amount_chf: parseFloat(select('string(//ns:Amt)', node) as string), | ||
fees_chf: 0, | ||
status: StatusKey.SUCCEEDED, | ||
created: toFirebaseAdminTimestamp(DateTime.now()), | ||
source: ContributionSourceKey.WIRE_TRANSFER, | ||
raw_content: node.toString(), | ||
}; | ||
|
||
const user = await this.firestoreAdmin.findFirst<User>(USER_FIRESTORE_PATH, (q) => | ||
q.where('payment_reference_id', '==', userReferenceId), | ||
); | ||
|
||
if (user) { | ||
await user?.ref.collection(CONTRIBUTION_FIRESTORE_PATH).doc(transactionId).set(contribution); | ||
} else { | ||
await this.firestoreAdmin.collection(CONTRIBUTION_FIRESTORE_PATH).doc(transactionId).set(contribution); | ||
} | ||
} | ||
}); | ||
} | ||
} |
Oops, something went wrong.