ofx-data-extractor
Version:
A module written in TypeScript that provides a utility to extract data from an OFX file in Node.js and Browser
9 lines (8 loc) • 16.4 kB
JavaScript
(function(l,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(l=typeof globalThis<"u"?globalThis:l||self,h(l.ofx={}))})(this,(function(l){"use strict";const h=/<\/[\w.]+>/g,x=/<[\w.]+>/g,m="<BANKMSGSRSV1>",S="</BANKMSGSRSV1>",C="<CREDITCARDMSGSRSV1>",N="</CREDITCARDMSGSRSV1>",y="<STMTTRN>",p="</STMTTRN>",j=new RegExp("_#_","g"),Q=[m,y,C],Z=[S,N,p];function q(n){const t=n.slice(0,4),e=n.slice(4,6),r=n.slice(6,8),a=n.slice(8,10),s=n.slice(10,12),i=n.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:e,M:e,dd:r,d:r,hh:a,h:a,mm:s,m:s,ss:i,s:i}}function F(n,t){const e=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=n;for(const a of e)r=r.replace(a,t[a]);return r}function L(n,t){const e=q(n);return F(t,e)}function tt(n,t){const e=String(n.getUTCFullYear()),r=String(n.getUTCMonth()+1).padStart(2,"0"),a=String(n.getUTCDate()).padStart(2,"0"),s=String(n.getUTCHours()).padStart(2,"0"),i=String(n.getUTCMinutes()).padStart(2,"0"),c=String(n.getUTCSeconds()).padStart(2,"0");return F(t,{yyyy:e,yy:e.slice(2),y:e,MM:r,M:r,dd:a,d:a,hh:s,h:s,mm:i,m:i,ss:c,s:c})}function nt(n){return n%400===0?!0:n%100===0?!1:n%4===0}function et(n,t){return n===2?nt(t)?29:28:[4,6,9,11].includes(n)?30:31}function rt(n){if(!/^\d{4}-\d{2}-\d{2}$/.test(n))return null;const[t,e,r]=n.split("-").map(Number);return{year:t,month:e,day:r,hour:0,minutes:0,seconds:0}}function at(n){if(!/^\d{4}-\d{2}-\d{2}T/.test(n))return null;const t=n.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):?(\d{2})(?::?(\d{2}))?(?:\.\d+)?(?:([+-]\d{2}):?(\d{2})|Z)?$/);if(!t)return null;const e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=Number(t[4]),i=Number(t[5]),c=Number(t[6]||"0");if(!B({year:e,month:r,day:a,hour:s,minutes:i,seconds:c}))return null;const f=Number(t[7]||"0"),g=Number(t[8]||"0"),T=f<0?f*60-g:f*60+g,u=new Date(Date.UTC(e,r-1,a,s,i,c)-T*6e4);return Number.isNaN(u.getTime())?null:{year:u.getUTCFullYear(),month:u.getUTCMonth()+1,day:u.getUTCDate(),hour:u.getUTCHours(),minutes:u.getUTCMinutes(),seconds:u.getUTCSeconds()}}function st(n){const e=n.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(e)?{year:Number(e.slice(0,4)),month:Number(e.slice(4,6)),day:Number(e.slice(6,8)),hour:Number(e.slice(8,10)||"00"),minutes:Number(e.slice(10,12)||"00"),seconds:Number(e.slice(12,14)||"00")}:null}function it(n){const t=n.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=String(t[1]).length===4,i=String(t[3]).length===4;return!s&&!i?null:{year:s?e:a,month:r,day:s?a:e,hour:0,minutes:0,seconds:0}}function ot(n){return rt(n)||at(n)||it(n)||st(n)}function B(n){return!(!Number.isFinite(n.year)||!Number.isFinite(n.month)||!Number.isFinite(n.day)||!Number.isFinite(n.hour)||!Number.isFinite(n.minutes)||!Number.isFinite(n.seconds)||n.month<1||n.month>12||n.day<1||n.day>et(n.month,n.year)||n.hour<0||n.hour>23||n.minutes<0||n.minutes>59||n.seconds<0||n.seconds>59)}function R(n){const t=ot(n);return!t||!B(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var I;(function(n){n.BANK="BANK",n.CREDIT_CARD="CREDIT_CARD"})(I||(I={}));const ct=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],ut=n=>n.includes(m)?I.BANK:I.CREDIT_CARD;function _(n){if(String(n.TRNAMT).startsWith("-"))return!0;const t=String(n.TRNTYPE).toLocaleLowerCase();return t==="1"||ct.includes(t)}function D(n){const t=n.replace(/(\\)/g,"\\\\").replace(h,e=>U(e,!0)).replace(x,e=>k(e,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(j,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const lt=n=>JSON.stringify({date:n.slice(0,12),transactionCode:n.slice(12,19),protocol:n.slice(19)}),dt=n=>n.startsWith("DT"),v=n=>n.trim();function k(n,t=!1){return Q.includes(n)&&!t?n:n.replace(/[<]/g,`
"`).replace(/[>]/g,'":{')}function U(n,t=!1){return Z.includes(n)&&!t?`
${n}`:`},
`}function ft({fitId:n,fitValue:t}){return n==="separated"?lt(t):`"${t}",`}function Tt(n){return n.search(",")>n.search(".")?n.replace(/[.]/g,"").replace(/[,]/g,"."):n.replace(/[,]/g,"")}function b({dateString:n,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(n)||!R(n)?n:L(n,t)}function gt(n){let t=n.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,"_#_");const e=n.field.replace(/['"]/g,"");return e.endsWith("AMT")&&(t=Tt(t)),dt(e)&&(t=b({dateString:t,formatDate:n.formatDate})),e==="FITID"?ft({fitId:n.fitId,fitValue:t}):n.nativeTypes&&P(e,t)?`${Number(t)},`:`"${t}",`}function ht(n,t){let e=n;return n.match(/{(\w|\W)+/)&&(e=e.replace(/({(\w|\W)+)$/,r=>gt({field:e.slice(0,e.indexOf(":")),value:r,...t}))),e}function P(n,t){return n.endsWith("ID")||n.endsWith("NUM")?!1:!isNaN(Number(t))}function $(n){const t=n.indexOf(y),e=n.lastIndexOf(p)+p.length;if(t<0||e<0||t>=e)return{oldListText:n,newListText:n};const r=n.substring(t,e),a=new RegExp(y,"g"),s=new RegExp(p,"g"),i=`"STRTTRN":[${r.replace(a,"{").replace(s,"},")}]`;return{oldListText:n,newListText:n.replace(r,i)}}function G(n){if(!(n.indexOf(m)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const e=$(n.substring(n.indexOf(m),n.indexOf(S)+S.length));return{newBankStatementTransactions:e.newListText,oldBankStatementTransactions:e.oldListText}}function K(n){if(!(n.indexOf(C)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const e=n.substring(n.indexOf(C),n.indexOf(N)+N.length),r=$(e);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function mt(n){const t=[];for(const e of n)t.push(...e.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(v).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function St(n,t){const e={};n.join("").search("<?")>-1&&(n=mt(n));for(const r of n){const[a,s]=r.split(":"),i=a.replace(`
`,"").toUpperCase();e[i]=t&&P(a,s)?Number(s):String(s).replace(/\?>/,"")}return e}function Ct(n){return n.reduce((t,e)=>{const r=Math.abs(+e.TRNAMT);return _(e)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class Nt{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return ht(t,this.internConfig)}getPartialJsonData(t){const[e,r]=t.split("<OFX>"),a="<OFX>"+r,{sanitizeRow:s}=this;return a.replace(/<(?=[^\/])/g,`
<`).replace(h,i=>U(i)).replace(x,i=>k(i)).split(`
`).map(v).filter(Boolean).map(s,this).join("")}}function A(n){return n.toString()}async function O(n){return await new Promise((e,r)=>{if(typeof window<"u"&&window.FileReader){const a=new window.FileReader;a.onload=s=>e(s.target.result),a.onerror=s=>r(s.target.error),a.readAsText(n)}else r(new Error("FileReader is not available in this environment."))})}class d{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new d(t)}fromBuffer(t){return new d(A(t))}async fromBlob(t){return new d(await O(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(A(t))}static async fromBlob(t){return new d(await O(t))}}class pt{configInstance={}}class M extends pt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:e}=G(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${D(e)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:e}=K(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${D(e)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let e=[],r=[];const a=this.configInstance?.getConfig?.().parserMode||"strict";let s=null,i=null;try{e=this.getBankTransferList(t)||[]}catch(w){s=w,e=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(w){i=w,r=[]}const c=t.includes("<STMTTRN>");if(a==="strict"&&s&&i&&c)throw s;const o=e.length?e:r,f=e.length?m:C,g=e.length?S:N,T=t.indexOf(f),u=t.indexOf(g),V=T>=0&&u>T?t.slice(T,u+g.length):"",H=V.match(/<DTSTART>([^\n<]+)/),Y=V.match(/<DTEND>([^\n<]+)/),bt=Ct(o||[]),At=H?.[1]?b({dateString:H[1],formatDate:this.configInstance.getConfig().formatDate}):"",Ot=Y?.[1]?b({dateString:Y[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:At,dateEnd:Ot,...bt}}getContent(t){const e=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:a}=G(e),{newCreditCardStatementTransactions:s,oldCreditCardStatementTransactions:i}=K(e);let c=e;return r&&(c=c.replace(a,r)),s&&(c=c.replace(i,s)),JSON.parse(`{${D(c)}}`)}}function X(n){if(n==null)return"";if(typeof n=="string"||typeof n=="number")return String(n);if(typeof n=="object"){const t=n;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(n)}catch{return String(n)}}return String(n)}function z(n){if(n==null)return null;const t=Number(String(n).replace(/,/g,"."));return Number.isNaN(t)?null:t}function Rt(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function It(n,t){const e=t.dateMode||"formatted";if(!n)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(e==="raw"||e==="formatted"){const a=R(n);return a?e==="formatted"&&t.formatDate?{value:tt(a,t.formatDate)}:{value:n}:{value:n,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}const r=R(n);return r?e==="date"?{value:r}:e==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}function Dt(n,t){const e=z(n);return e===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(n)}'.`,severity:"warning"}}:t==="string"?{amount:String(n),amountAbs:String(Math.abs(e))}:t==="cents"?{amount:Math.round(e*100),amountAbs:Math.round(Math.abs(e)*100)}:{amount:e,amountAbs:Math.abs(e)}}function W(n){const t=[],e=n?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=n?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"bank",transaction:c,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:e,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${o}`})});const s=n?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(s?.BANKTRANLIST?.STRTTRN||s?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"credit_card",transaction:c,currency:s?.CURDEF||null,account:s?.CCACCTFROM||null,institution:e,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${o}`})}),t}function Et(n,t={}){const e=t.amountMode||"number",r=[],a=[];return W(n).forEach(i=>{const c=[],o=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:T}=Dt(i.transaction.TRNAMT,e);T&&(T.path=i.path,c.push(T),r.push(T));const u=It(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,c.push(u.warning),r.push(u.warning)),a.push({source:i.source,direction:_(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:o,descriptionNormalized:Rt(o),fitId:X(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:c})}),{transactions:a,warnings:r}}function yt(n){const t=[],e=[],r=W(n);n?.OFX||e.push({code:"MISSING_OFX_BLOCK",message:"Missing OFX root block.",severity:"error",path:"OFX"}),r.length||t.push({code:"NO_TRANSACTIONS_FOUND",message:"No transactions were found in bank or credit card statements.",severity:"warning",path:"OFX"});const a=new Map;r.forEach(o=>{const f=X(o.transaction.FITID);f?a.set(f,(a.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:o.path}),z(o.transaction.TRNAMT)===null&&e.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(o.transaction.TRNAMT)}'.`,severity:"error",path:o.path}),R(String(o.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(o.transaction.DTPOSTED||"")}'.`,severity:"warning",path:o.path})});const s=Array.from(a.values()).filter(o=>o>1).length;s>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${s} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(o=>o.source==="bank").length,c=r.filter(o=>o.source==="credit_card").length;return{isValid:e.length===0,warnings:t,errors:e,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:c,duplicatedFitIds:s}}}class J{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new M,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new Nt(t)),this}getType(){return ut(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,e,r){try{return r()}catch(a){if(this.getParserMode()!=="lenient")throw a;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:a instanceof Error?a.message:String(a),path:t}),e}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return St(String(t).split(`
`).filter(e=>!!e.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),e=this.getContentInternal();return{...t,...e}}getHeaders(){return this.beginOperation(),this.getHeadersInternal()}getBankTransferList(){return this.beginOperation(),this.safelyExecute("getBankTransferList",[],()=>this.customExtractorInstance.getBankTransferList(this.dataReaderInstance.getData()))}getCreditCardTransferList(){return this.beginOperation(),this.safelyExecute("getCreditCardTransferList",[],()=>this.customExtractorInstance.getCreditCardTransferList(this.dataReaderInstance.getData()))}getTransactionsSummary(){return this.beginOperation(),this.safelyExecute("getTransactionsSummary",{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0,dateStart:"",dateEnd:""},()=>this.customExtractorInstance.getTransactionsSummary(this.dataReaderInstance.getData()))}getContent(){return this.beginOperation(),this.getContentInternal()}toJson(){return this.beginOperation(),this.getResponseInternal()}toNormalized(t={}){this.beginOperation();const e=this.getResponseInternal();return Et(e,{...t,formatDate:t.formatDate||this.customExtractorInstance.configInstance.getConfig().formatDate})}validate(){this.beginOperation();const t=this.getResponseInternal();return yt(t)}getWarnings(){return[...this.diagnostics]}}class E{extractor;constructor(t,e){this.extractor=new J(new M),this.extractor.data(new d(t)),this.extractor.config(e||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new E(d.fromBuffer(t).getData())}static async fromBlob(t){const e=await d.fromBlob(t);return new E(e.getData())}config(t){return this.extractor.config(t),this}getHeaders(){return this.extractor.getHeaders()}getBankTransferList(){return this.extractor.getBankTransferList()}getCreditCardTransferList(){return this.extractor.getCreditCardTransferList()}getTransactionsSummary(){return this.extractor.getTransactionsSummary()}getContent(){return this.extractor.getContent()}toJson(){return this.extractor.toJson()}toNormalized(t){return this.extractor.toNormalized(t)}validate(){return this.extractor.validate()}getWarnings(){return this.extractor.getWarnings()}}l.Extractor=J,l.Ofx=E,l.OfxExtractor=M,l.Reader=d,l.blobToString=O,l.bufferToString=A,l.fixJsonProblems=D,l.formatDate=L}));