ofx-data-extractor 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -93,6 +93,12 @@ async function handleFile(event: Event) {
93
93
  - `formatDate?: string`
94
94
  - `parserMode?: 'strict' | 'lenient'`
95
95
 
96
+ Parsing guarantees:
97
+
98
+ - OFX 1.x (SGML) and 2.x (XML) are supported, including bodies without line breaks.
99
+ - Character entities (`&`, `<`, `>`, `"`, `'`, numeric references) are decoded in text values.
100
+ - `toNormalized({ dateMode })` keeps the full `DT*` value (including time) for `raw`, `iso`, `date` and `timestamp`.
101
+
96
102
  Parser mode:
97
103
 
98
104
  - `strict` (default): parsing errors throw.
@@ -1,9 +1,16 @@
1
1
  import { ExtractorConfig } from '../@types/common';
2
2
  export declare class Config {
3
3
  private readonly config;
4
+ private readonly rawDates;
4
5
  private internConfig;
5
- constructor(config: ExtractorConfig);
6
+ private readonly sanitizeConfig;
7
+ /**
8
+ * @param rawDates internal: keep `DT*` values exactly as they appear in the
9
+ * OFX instead of applying `formatDate` (used by `toNormalized`).
10
+ */
11
+ constructor(config: ExtractorConfig, rawDates?: boolean);
6
12
  getConfig(): ExtractorConfig;
13
+ withRawDates(): Config;
7
14
  sanitizeRow(row: string): string;
8
15
  getPartialJsonData(data: string): string;
9
16
  }
@@ -1,5 +1,10 @@
1
1
  export declare const ELEMENT_CLOSURE_REGEX: RegExp;
2
2
  export declare const ELEMENT_OPENING_REGEX: RegExp;
3
+ /**
4
+ * `<LEAF>value</OTHER>`: an unclosed SGML leaf immediately followed by the
5
+ * closing tag of its parent container (OFX 1.x allows this without line breaks).
6
+ */
7
+ export declare const SGML_LEAF_FOLLOWED_BY_CLOSURE_REGEX: RegExp;
3
8
  export declare const BANK_SERVICE_START = "<BANKMSGSRSV1>";
4
9
  export declare const BANK_SERVICE_END = "</BANKMSGSRSV1>";
5
10
  export declare const CREDIT_CARD_SERVICE_START = "<CREDITCARDMSGSRSV1>";
@@ -7,6 +12,12 @@ export declare const CREDIT_CARD_SERVICE_END = "</CREDITCARDMSGSRSV1>";
7
12
  export declare const START_STATEMENT_TRANSACION = "<STMTTRN>";
8
13
  export declare const FINISH_STATEMENT_TRANSACTION = "</STMTTRN>";
9
14
  export declare const QUOTE_PATTERN = "_#_";
15
+ /**
16
+ * Text values may contain characters the parser uses as structure (`{ } [ ]`).
17
+ * They are swapped for these tokens while the text is rewritten as JSON and
18
+ * restored once it has been parsed (same idea as `QUOTE_PATTERN`).
19
+ */
20
+ export declare const STRUCTURAL_CHAR_TOKENS: Readonly<Record<string, string>>;
10
21
  export declare const QUOTE_PATTERN_REGEX: RegExp;
11
22
  export declare const OPENING_TAGS_INITIALLY_IGNORED: string[];
12
23
  export declare const CLOSING_TAGS_INITIALLY_IGNORED: string[];
@@ -1,6 +1,20 @@
1
1
  import { ExtractorConfig } from '../@types/common';
2
2
  import type { StatementTransaction } from '../@types/ofx/common';
3
3
  export declare function fixJsonProblems(content: string): string;
4
+ export declare function protectStructuralChars(value: string): string;
5
+ export declare function restoreStructuralChars(value: string): string;
6
+ /** Protects `{ } [ ]` inside text nodes (what follows `>` up to the next `<`). */
7
+ export declare function protectTextNodes(content: string): string;
8
+ /**
9
+ * Decodes the XML/SGML character entities OFX allows in text fields.
10
+ * Single pass (`&amp;lt;` -> `&lt;`); unknown or invalid entities are kept as-is.
11
+ */
12
+ export declare function decodeEntities(value: string): string;
13
+ /**
14
+ * Parses the JSON built from the OFX text, decoding entities only in values
15
+ * (after parsing, so decoded text can never be mistaken for tags or JSON).
16
+ */
17
+ export declare function parseOfxJson(text: string): any;
4
18
  export declare const extractFinancialInstitutionTransactionId: (fitid: string) => string;
5
19
  export declare const isDateField: (field: string) => boolean;
6
20
  export declare const trim: (str: string) => string;
@@ -14,7 +28,10 @@ type GetDateParams = Pick<ExtractorConfig, 'formatDate'> & {
14
28
  dateString: string;
15
29
  };
16
30
  export declare function getConfiguredDate({ dateString, formatDate: format, }: GetDateParams): string;
17
- export declare function sanitize(row: string, config: ExtractorConfig): string;
31
+ export declare function applyConfiguredDates(transaction: unknown, format?: string): void;
32
+ export declare function sanitize(row: string, config: ExtractorConfig & {
33
+ rawDates?: boolean;
34
+ }): string;
18
35
  export declare function isValidNumberToConvert(field: string, value: string): boolean;
19
36
  export declare function parseTransactions(content: string): {
20
37
  oldListText: string;
@@ -1,8 +1,9 @@
1
- const L=/<\/[\w.]+>/g,B=/<[\w.]+>/g,g="<BANKMSGSRSV1>",m="</BANKMSGSRSV1>",S="<CREDITCARDMSGSRSV1>",C="</CREDITCARDMSGSRSV1>",D="<STMTTRN>",h="</STMTTRN>";const j=new RegExp("_#_","g"),Q=[g,D,S],Z=[m,C,h];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 _(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 tt(n,t){const e=q(n);return _(t,e)}function nt(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 _(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 et(n){return n%400===0?!0:n%100===0?!1:n%4===0}function rt(n,t){return n===2?et(t)?29:28:[4,6,9,11].includes(n)?30:31}function at(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 st(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(!v({year:e,month:r,day:a,hour:s,minutes:i,seconds:c}))return null;const l=Number(t[7]||"0"),f=Number(t[8]||"0"),T=l<0?l*60-f:l*60+f,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 it(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 ot(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 ct(n){return at(n)||st(n)||ot(n)||it(n)}function v(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>rt(n.month,n.year)||n.hour<0||n.hour>23||n.minutes<0||n.minutes>59||n.seconds<0||n.seconds>59)}function N(n){const t=ct(n);return!t||!v(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var p;(function(n){n.BANK="BANK",n.CREDIT_CARD="CREDIT_CARD"})(p||(p={}));const ut=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],lt=n=>n.includes(g)?p.BANK:p.CREDIT_CARD;function k(n){if(String(n.TRNAMT).startsWith("-"))return!0;const t=String(n.TRNTYPE).toLocaleLowerCase();return t==="1"||ut.includes(t)}function I(n){const t=n.replace(/(\\)/g,"\\\\").replace(L,e=>$(e,!0)).replace(B,e=>P(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 dt=n=>JSON.stringify({date:n.slice(0,12),transactionCode:n.slice(12,19),protocol:n.slice(19)}),Tt=n=>n.startsWith("DT"),U=n=>n.trim();function P(n,t=!1){return Q.includes(n)&&!t?n:n.replace(/[<]/g,`
2
- "`).replace(/[>]/g,'":{')}function $(n,t=!1){return Z.includes(n)&&!t?`
3
- ${n}`:`},
4
- `}function ft({fitId:n,fitValue:t}){return n==="separated"?dt(t):`"${t}",`}function gt(n){return n.search(",")>n.search(".")?n.replace(/[.]/g,"").replace(/[,]/g,"."):n.replace(/[,]/g,"")}function E({dateString:n,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(n)||!N(n)?n:tt(n,t)}function ht(n){let t=n.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,"_#_");const e=n.field.replace(/['"]/g,"");return e.endsWith("AMT")&&(t=gt(t)),Tt(e)&&(t=E({dateString:t,formatDate:n.formatDate})),e==="FITID"?ft({fitId:n.fitId,fitValue:t}):n.nativeTypes&&G(e,t)?`${Number(t)},`:`"${t}",`}function mt(n,t){let e=n;return n.match(/{(\w|\W)+/)&&(e=e.replace(/({(\w|\W)+)$/,r=>ht({field:e.slice(0,e.indexOf(":")),value:r,...t}))),e}function G(n,t){return n.endsWith("ID")||n.endsWith("NUM")?!1:!isNaN(Number(t))}function K(n){const t=n.indexOf(D),e=n.lastIndexOf(h)+h.length;if(t<0||e<0||t>=e)return{oldListText:n,newListText:n};const r=n.substring(t,e),a=new RegExp(D,"g"),s=new RegExp(h,"g"),i=`"STRTTRN":[${r.replace(a,"{").replace(s,"},")}]`;return{oldListText:n,newListText:n.replace(r,i)}}function O(n){if(!(n.indexOf(g)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const e=K(n.substring(n.indexOf(g),n.indexOf(m)+m.length));return{newBankStatementTransactions:e.newListText,oldBankStatementTransactions:e.oldListText}}function w(n){if(!(n.indexOf(S)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const e=n.substring(n.indexOf(S),n.indexOf(C)+C.length),r=K(e);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function St(n){const t=[];for(const e of n)t.push(...e.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(U).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function Ct(n,t){const e={};n.join("").search("<?")>-1&&(n=St(n));for(const r of n){const[a,s]=r.split(":"),i=a.replace(`
5
- `,"").toUpperCase();e[i]=t&&G(a,s)?Number(s):String(s).replace(/\?>/,"")}return e}function Nt(n){return n.reduce((t,e)=>{const r=Math.abs(+e.TRNAMT);return k(e)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class pt{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return mt(t,this.internConfig)}getPartialJsonData(t){const[e,r]=t.split("<OFX>"),a="<OFX>"+r,{sanitizeRow:s}=this;return a.replace(/<(?=[^\/])/g,`
6
- <`).replace(L,i=>$(i)).replace(B,i=>P(i)).split(`
7
- `).map(U).filter(Boolean).map(s,this).join("")}}function x(n){return n.toString()}async function F(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(x(t))}async fromBlob(t){return new d(await F(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(x(t))}static async fromBlob(t){return new d(await F(t))}}class Rt{configInstance={}}class X extends Rt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:e}=O(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${I(e)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:e}=w(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${I(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(R){s=R,e=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(R){i=R,r=[]}const c=t.includes("<STMTTRN>");if(a==="strict"&&s&&i&&c)throw s;const o=e.length?e:r,l=e.length?g:S,f=e.length?m:C,T=t.indexOf(l),u=t.indexOf(f),b=T>=0&&u>T?t.slice(T,u+f.length):"",A=b.match(/<DTSTART>([^\n<]+)/),M=b.match(/<DTEND>([^\n<]+)/),V=Nt(o||[]),H=A?.[1]?E({dateString:A[1],formatDate:this.configInstance.getConfig().formatDate}):"",Y=M?.[1]?E({dateString:M[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:H,dateEnd:Y,...V}}getContent(t){const e=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:a}=O(e),{newCreditCardStatementTransactions:s,oldCreditCardStatementTransactions:i}=w(e);let c=e;return r&&(c=c.replace(a,r)),s&&(c=c.replace(i,s)),JSON.parse(`{${I(c)}}`)}}function z(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 W(n){if(n==null)return null;const t=Number(String(n).replace(/,/g,"."));return Number.isNaN(t)?null:t}function It(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Dt(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=N(n);return a?e==="formatted"&&t.formatDate?{value:nt(a,t.formatDate)}:{value:n}:{value:n,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}const r=N(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 Et(n,t){const e=W(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 J(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 yt(n,t={}){const e=t.amountMode||"number",r=[],a=[];return J(n).forEach(i=>{const c=[],o=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:l,amountAbs:f,warning:T}=Et(i.transaction.TRNAMT,e);T&&(T.path=i.path,c.push(T),r.push(T));const u=Dt(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:k(i.transaction)?"debit":"credit",amount:l,amountAbs:f,postedAt:u.value,description:o,descriptionNormalized:It(o),fitId:z(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:c})}),{transactions:a,warnings:r}}function bt(n){const t=[],e=[],r=J(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 l=z(o.transaction.FITID);l?a.set(l,(a.get(l)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:o.path}),W(o.transaction.TRNAMT)===null&&e.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(o.transaction.TRNAMT)}'.`,severity:"error",path:o.path}),N(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 At{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new X,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new pt(t)),this}getType(){return lt(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 Ct(String(t).split(`
8
- `).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 yt(e,{...t,formatDate:t.formatDate||this.customExtractorInstance.configInstance.getConfig().formatDate})}validate(){this.beginOperation();const t=this.getResponseInternal();return bt(t)}getWarnings(){return[...this.diagnostics]}}class y{extractor;constructor(t,e){this.extractor=new At(new X),this.extractor.data(new d(t)),this.extractor.config(e||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new y(d.fromBuffer(t).getData())}static async fromBlob(t){const e=await d.fromBlob(t);return new y(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()}}export{At as Extractor,y as Ofx,X as OfxExtractor,d as Reader,F as blobToString,x as bufferToString,I as fixJsonProblems,tt as formatDate};
1
+ const U=/<\/[\w.]+>/g,v=/<[\w.]+>/g,nt=/<([\w.]+)>([^<]*)<\/([\w.]+)>/g,T="<BANKMSGSRSV1>",m="</BANKMSGSRSV1>",S="<CREDITCARDMSGSRSV1>",p="</CREDITCARDMSGSRSV1>",b="<STMTTRN>",h="</STMTTRN>";const A={"{":"_#LB#_","}":"_#RB#_","[":"_#LS#_","]":"_#RS#_"},rt=new RegExp("_#_","g"),st=[T,b,S],at=[m,p,h];function it(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function P(e,t){const n=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=e;for(const s of n)r=r.replace(s,t[s]);return r}function ot(e,t){const n=it(e);return P(t,n)}function ct(e,t){const n=String(e.getUTCFullYear()),r=String(e.getUTCMonth()+1).padStart(2,"0"),s=String(e.getUTCDate()).padStart(2,"0"),a=String(e.getUTCHours()).padStart(2,"0"),i=String(e.getUTCMinutes()).padStart(2,"0"),o=String(e.getUTCSeconds()).padStart(2,"0");return P(t,{yyyy:n,yy:n.slice(2),y:n,MM:r,M:r,dd:s,d:s,hh:a,h:a,mm:i,m:i,ss:o,s:o})}function ut(e){return e%400===0?!0:e%100===0?!1:e%4===0}function ft(e,t){return e===2?ut(t)?29:28:[4,6,9,11].includes(e)?30:31}function lt(e){if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return null;const[t,n,r]=e.split("-").map(Number);return{year:t,month:n,day:r,hour:0,minutes:0,seconds:0}}function dt(e){if(!/^\d{4}-\d{2}-\d{2}T/.test(e))return null;const t=e.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 n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=Number(t[4]),i=Number(t[5]),o=Number(t[6]||"0");if(!$({year:n,month:r,day:s,hour:a,minutes:i,seconds:o}))return null;const f=Number(t[7]||"0"),g=Number(t[8]||"0"),d=f<0?f*60-g:f*60+g,u=new Date(Date.UTC(n,r-1,s,a,i,o)-d*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 gt(e){const n=e.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(n)?{year:Number(n.slice(0,4)),month:Number(n.slice(4,6)),day:Number(n.slice(6,8)),hour:Number(n.slice(8,10)||"00"),minutes:Number(n.slice(10,12)||"00"),seconds:Number(n.slice(12,14)||"00")}:null}function Tt(e){const t=e.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=String(t[1]).length===4,i=String(t[3]).length===4;return!a&&!i?null:{year:a?n:s,month:r,day:a?s:n,hour:0,minutes:0,seconds:0}}function ht(e){return lt(e)||dt(e)||Tt(e)||gt(e)}function $(e){return!(!Number.isFinite(e.year)||!Number.isFinite(e.month)||!Number.isFinite(e.day)||!Number.isFinite(e.hour)||!Number.isFinite(e.minutes)||!Number.isFinite(e.seconds)||e.month<1||e.month>12||e.day<1||e.day>ft(e.month,e.year)||e.hour<0||e.hour>23||e.minutes<0||e.minutes>59||e.seconds<0||e.seconds>59)}function R(e){const t=ht(e);return!t||!$(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var C;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(C||(C={}));const mt=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],St=e=>e.includes(T)?C.BANK:C.CREDIT_CARD;function G(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||mt.includes(t)}function E(e){const t=e.replace(/(\\)/g,"\\\\").replace(U,n=>J(n,!0)).replace(v,n=>W(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(rt,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const pt=Object.keys(A).reduce((e,t)=>({...e,[A[t]]:t}),{});function N(e){return e.replace(/[{}\[\]]/g,t=>A[t])}function K(e){return e.replace(/_#(?:LB|RB|LS|RS)#_/g,t=>pt[t])}function Rt(e){return e.replace(/>([^<]+)/g,(t,n)=>`>${N(n)}`)}const Ct={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function Nt(e){return e.indexOf("&")<0?e:e.replace(/&(?:(amp|lt|gt|quot|apos)|#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6}));/g,(t,n,r,s)=>{if(n)return Ct[n];const a=r?Number(r):parseInt(s,16);return a<1||a>1114111||a>=55296&&a<=57343?t:String.fromCodePoint(a)})}function y(e){return JSON.parse(e,(t,n)=>typeof n=="string"?Nt(K(n)):n)}const Dt=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),X=e=>e.startsWith("DT"),z=e=>e.trim();function W(e,t=!1){return st.includes(e)&&!t?e:e.replace(/[<]/g,`
2
+ "`).replace(/[>]/g,'":{')}function J(e,t=!1){return at.includes(e)&&!t?`
3
+ ${e}`:`},
4
+ `}function It({fitId:e,fitValue:t}){if(e==="separated"){const n=JSON.parse(Dt(t));return JSON.stringify(Object.keys(n).reduce((r,s)=>({...r,[s]:N(n[s])}),{}))}return`"${N(t)}",`}function Et(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function D({dateString:e,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(e)||!R(e)?e:ot(e,t)}function yt(e,t){if(!e||typeof e!="object")return;const n=e;for(const r of Object.keys(n)){const s=n[r];X(r)&&typeof s=="string"&&(n[r]=D({dateString:s,formatDate:t}))}}function bt(e){let t=K(e.value.replace(/[{]/g,"").replace(/(},)/g,"")).replace(/["]/g,"_#_");const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=Et(t)),X(n)&&!e.rawDates&&(t=D({dateString:t,formatDate:e.formatDate})),n==="FITID"?It({fitId:e.fitId,fitValue:t}):e.nativeTypes&&H(n,t)?`${Number(t)},`:`"${N(t)}",`}function At(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>bt({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function H(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function V(e){const t=e.indexOf(b),n=e.lastIndexOf(h)+h.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(b,"g"),a=new RegExp(h,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function L(e){if(!(e.indexOf(T)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=V(e.substring(e.indexOf(T),e.indexOf(m)+m.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function F(e){if(!(e.indexOf(S)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(S),e.indexOf(p)+p.length),r=V(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function Ot(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(z).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function wt(e,t){const n={};e.join("").search("<?")>-1&&(e=Ot(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
5
+ `,"").toUpperCase();n[i]=t&&H(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function Mt(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return G(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class w{config;rawDates;internConfig={};sanitizeConfig;constructor(t,n=!1){this.config=t,this.rawDates=n,this.internConfig=t,this.sanitizeConfig={...t,rawDates:n}}getConfig(){return this.internConfig}withRawDates(){return new w(this.config,!0)}sanitizeRow(t){return At(t,this.sanitizeConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s=Rt("<OFX>"+r),{sanitizeRow:a}=this;return s.replace(nt,(i,o,c,f)=>o===f?i:`<${o}>${c}
6
+ </${f}>`).replace(/<(?=[^\/])/g,`
7
+ <`).replace(U,i=>J(i)).replace(v,i=>W(i)).split(`
8
+ `).map(z).filter(Boolean).map(a,this).join("")}}function B(e){return e.toString()}async function k(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}else r(new Error("FileReader is not available in this environment."))})}class l{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new l(t)}fromBuffer(t){return new l(B(t))}async fromBlob(t){return new l(await k(t))}static fromString(t){return new l(t)}static fromBuffer(t){return new l(B(t))}static async fromBlob(t){return new l(await k(t))}}class xt{configInstance={}}class Y extends xt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=L(this.configInstance.getPartialJsonData(t));return n?y(`{${E(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=F(this.configInstance.getPartialJsonData(t));return n?y(`{${E(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let n=[],r=[];const s=this.configInstance?.getConfig?.().parserMode||"strict";let a=null,i=null;try{n=this.getBankTransferList(t)||[]}catch(I){a=I,n=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(I){i=I,r=[]}const o=t.includes("<STMTTRN>");if(s==="strict"&&a&&i&&o)throw a;const c=n.length?n:r,f=n.length?T:S,g=n.length?m:p,d=t.indexOf(f),u=t.indexOf(g),M=d>=0&&u>d?t.slice(d,u+g.length):"",x=M.match(/<DTSTART>([^\n<]+)/),_=M.match(/<DTEND>([^\n<]+)/),Z=Mt(c||[]),tt=x?.[1]?D({dateString:x[1],formatDate:this.configInstance.getConfig().formatDate}):"",et=_?.[1]?D({dateString:_[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:tt,dateEnd:et,...Z}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=L(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=F(n);let o=n;return r&&(o=o.replace(s,r)),a&&(o=o.replace(i,a)),y(`{${E(o)}}`)}}const _t="y-M-d";function j(e){if(e==null)return"";if(typeof e=="string"||typeof e=="number")return String(e);if(typeof e=="object"){const t=e;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(e)}catch{return String(e)}}return String(e)}function Q(e){if(e==null)return null;const t=Number(String(e).replace(/,/g,"."));return Number.isNaN(t)?null:t}function Lt(e){return e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Ft(e,t){const n=t.dateMode||"formatted";if(!e)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(n==="raw"||n==="formatted"){const s=R(e);return s?n==="formatted"?{value:ct(s,t.formatDate||_t)}:{value:e}:{value:e,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}const r=R(e);return r?n==="date"?{value:r}:n==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}function Bt(e,t){const n=Q(e);return n===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(e)}'.`,severity:"warning"}}:t==="string"?{amount:String(e),amountAbs:String(Math.abs(n))}:t==="cents"?{amount:Math.round(n*100),amountAbs:Math.round(Math.abs(n)*100)}:{amount:n,amountAbs:Math.abs(n)}}function q(e){const t=[],n=e?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=e?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"bank",transaction:o,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:n,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${c}`})});const a=e?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(a?.BANKTRANLIST?.STRTTRN||a?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"credit_card",transaction:o,currency:a?.CURDEF||null,account:a?.CCACCTFROM||null,institution:n,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${c}`})}),t}function kt(e,t={}){const n=t.amountMode||"number",r=[],s=[];return q(e).forEach(i=>{const o=[],c=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:d}=Bt(i.transaction.TRNAMT,n);d&&(d.path=i.path,o.push(d),r.push(d));const u=Ft(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,o.push(u.warning),r.push(u.warning)),s.push({source:i.source,direction:G(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:c,descriptionNormalized:Lt(c),fitId:j(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:o})}),{transactions:s,warnings:r}}function Ut(e){const t=[],n=[],r=q(e);e?.OFX||n.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 s=new Map;r.forEach(c=>{const f=j(c.transaction.FITID);f?s.set(f,(s.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:c.path}),Q(c.transaction.TRNAMT)===null&&n.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(c.transaction.TRNAMT)}'.`,severity:"error",path:c.path}),R(String(c.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(c.transaction.DTPOSTED||"")}'.`,severity:"warning",path:c.path})});const a=Array.from(s.values()).filter(c=>c>1).length;a>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${a} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(c=>c.source==="bank").length,o=r.filter(c=>c.source==="credit_card").length;return{isValid:n.length===0,warnings:t,errors:n,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:o,duplicatedFitIds:a}}}class vt{customExtractor;customExtractorInstance;dataReaderInstance=new l("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new Y,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new w(t)),this}getType(){return St(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,n,r){try{return r()}catch(s){if(this.getParserMode()!=="lenient")throw s;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:s instanceof Error?s.message:String(s),path:t}),n}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return wt(String(t).split(`
9
+ `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),n=this.getContentInternal();return{...t,...n}}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()}getResponseWithRawDates(){const t=this.customExtractorInstance.configInstance;this.customExtractorInstance.setConfig(t.withRawDates());try{return this.getResponseInternal()}finally{this.customExtractorInstance.setConfig(t)}}toNormalized(t={}){this.beginOperation();const n=this.customExtractorInstance.configInstance.getConfig().formatDate,r=kt(this.getResponseWithRawDates(),{...t,formatDate:t.formatDate||n});return r.transactions.forEach(s=>yt(s.raw,n)),r}validate(){return this.beginOperation(),Ut(this.getResponseWithRawDates())}getWarnings(){return[...this.diagnostics]}}class O{extractor;constructor(t,n){this.extractor=new vt(new Y),this.extractor.data(new l(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new O(l.fromBuffer(t).getData())}static async fromBlob(t){const n=await l.fromBlob(t);return new O(n.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()}}export{vt as Extractor,O as Ofx,Y as OfxExtractor,l as Reader,k as blobToString,B as bufferToString,E as fixJsonProblems,ot as formatDate};
@@ -26,6 +26,11 @@ export declare class Extractor<T = any> implements IExtractor<T> {
26
26
  getTransactionsSummary(): TransactionsSummary;
27
27
  getContent(): OfxStructure;
28
28
  toJson(): OfxResponse;
29
+ /**
30
+ * Runs the extraction with `DT*` fields untouched (no `formatDate`), so
31
+ * date-aware consumers see the full OFX value (including hh:mm:ss).
32
+ */
33
+ private getResponseWithRawDates;
29
34
  toNormalized(options?: NormalizeOptions): NormalizedOfxData;
30
35
  validate(): ValidationReport;
31
36
  getWarnings(): OfxDiagnostic[];
@@ -1,8 +1,9 @@
1
- (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,`
2
- "`).replace(/[>]/g,'":{')}function U(n,t=!1){return Z.includes(n)&&!t?`
3
- ${n}`:`},
4
- `}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(`
5
- `,"").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,`
6
- <`).replace(h,i=>U(i)).replace(x,i=>k(i)).split(`
7
- `).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(`
8
- `).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}));
1
+ (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,B=/<[\w.]+>/g,nt=/<([\w.]+)>([^<]*)<\/([\w.]+)>/g,m="<BANKMSGSRSV1>",S="</BANKMSGSRSV1>",R="<CREDITCARDMSGSRSV1>",p="</CREDITCARDMSGSRSV1>",O="<STMTTRN>",C="</STMTTRN>",A={"{":"_#LB#_","}":"_#RB#_","[":"_#LS#_","]":"_#RS#_"},rt=new RegExp("_#_","g"),st=[m,O,R],at=[S,p,C];function it(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function k(e,t){const n=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=e;for(const s of n)r=r.replace(s,t[s]);return r}function U(e,t){const n=it(e);return k(t,n)}function ot(e,t){const n=String(e.getUTCFullYear()),r=String(e.getUTCMonth()+1).padStart(2,"0"),s=String(e.getUTCDate()).padStart(2,"0"),a=String(e.getUTCHours()).padStart(2,"0"),i=String(e.getUTCMinutes()).padStart(2,"0"),o=String(e.getUTCSeconds()).padStart(2,"0");return k(t,{yyyy:n,yy:n.slice(2),y:n,MM:r,M:r,dd:s,d:s,hh:a,h:a,mm:i,m:i,ss:o,s:o})}function ct(e){return e%400===0?!0:e%100===0?!1:e%4===0}function ut(e,t){return e===2?ct(t)?29:28:[4,6,9,11].includes(e)?30:31}function ft(e){if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return null;const[t,n,r]=e.split("-").map(Number);return{year:t,month:n,day:r,hour:0,minutes:0,seconds:0}}function lt(e){if(!/^\d{4}-\d{2}-\d{2}T/.test(e))return null;const t=e.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 n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=Number(t[4]),i=Number(t[5]),o=Number(t[6]||"0");if(!v({year:n,month:r,day:s,hour:a,minutes:i,seconds:o}))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(n,r-1,s,a,i,o)-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 dt(e){const n=e.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(n)?{year:Number(n.slice(0,4)),month:Number(n.slice(4,6)),day:Number(n.slice(6,8)),hour:Number(n.slice(8,10)||"00"),minutes:Number(n.slice(10,12)||"00"),seconds:Number(n.slice(12,14)||"00")}:null}function Tt(e){const t=e.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=String(t[1]).length===4,i=String(t[3]).length===4;return!a&&!i?null:{year:a?n:s,month:r,day:a?s:n,hour:0,minutes:0,seconds:0}}function gt(e){return ft(e)||lt(e)||Tt(e)||dt(e)}function v(e){return!(!Number.isFinite(e.year)||!Number.isFinite(e.month)||!Number.isFinite(e.day)||!Number.isFinite(e.hour)||!Number.isFinite(e.minutes)||!Number.isFinite(e.seconds)||e.month<1||e.month>12||e.day<1||e.day>ut(e.month,e.year)||e.hour<0||e.hour>23||e.minutes<0||e.minutes>59||e.seconds<0||e.seconds>59)}function N(e){const t=gt(e);return!t||!v(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var D;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(D||(D={}));const ht=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],mt=e=>e.includes(m)?D.BANK:D.CREDIT_CARD;function P(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||ht.includes(t)}function I(e){const t=e.replace(/(\\)/g,"\\\\").replace(h,n=>z(n,!0)).replace(B,n=>X(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(rt,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const St=Object.keys(A).reduce((e,t)=>({...e,[A[t]]:t}),{});function E(e){return e.replace(/[{}\[\]]/g,t=>A[t])}function $(e){return e.replace(/_#(?:LB|RB|LS|RS)#_/g,t=>St[t])}function Rt(e){return e.replace(/>([^<]+)/g,(t,n)=>`>${E(n)}`)}const pt={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function Ct(e){return e.indexOf("&")<0?e:e.replace(/&(?:(amp|lt|gt|quot|apos)|#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6}));/g,(t,n,r,s)=>{if(n)return pt[n];const a=r?Number(r):parseInt(s,16);return a<1||a>1114111||a>=55296&&a<=57343?t:String.fromCodePoint(a)})}function w(e){return JSON.parse(e,(t,n)=>typeof n=="string"?Ct($(n)):n)}const Nt=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),G=e=>e.startsWith("DT"),K=e=>e.trim();function X(e,t=!1){return st.includes(e)&&!t?e:e.replace(/[<]/g,`
2
+ "`).replace(/[>]/g,'":{')}function z(e,t=!1){return at.includes(e)&&!t?`
3
+ ${e}`:`},
4
+ `}function Dt({fitId:e,fitValue:t}){if(e==="separated"){const n=JSON.parse(Nt(t));return JSON.stringify(Object.keys(n).reduce((r,s)=>({...r,[s]:E(n[s])}),{}))}return`"${E(t)}",`}function It(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function y({dateString:e,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(e)||!N(e)?e:U(e,t)}function Et(e,t){if(!e||typeof e!="object")return;const n=e;for(const r of Object.keys(n)){const s=n[r];G(r)&&typeof s=="string"&&(n[r]=y({dateString:s,formatDate:t}))}}function yt(e){let t=$(e.value.replace(/[{]/g,"").replace(/(},)/g,"")).replace(/["]/g,"_#_");const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=It(t)),G(n)&&!e.rawDates&&(t=y({dateString:t,formatDate:e.formatDate})),n==="FITID"?Dt({fitId:e.fitId,fitValue:t}):e.nativeTypes&&W(n,t)?`${Number(t)},`:`"${E(t)}",`}function bt(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>yt({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function W(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function J(e){const t=e.indexOf(O),n=e.lastIndexOf(C)+C.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(O,"g"),a=new RegExp(C,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function H(e){if(!(e.indexOf(m)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=J(e.substring(e.indexOf(m),e.indexOf(S)+S.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function V(e){if(!(e.indexOf(R)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(R),e.indexOf(p)+p.length),r=J(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function Ot(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(K).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function At(e,t){const n={};e.join("").search("<?")>-1&&(e=Ot(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
5
+ `,"").toUpperCase();n[i]=t&&W(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function wt(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return P(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class x{config;rawDates;internConfig={};sanitizeConfig;constructor(t,n=!1){this.config=t,this.rawDates=n,this.internConfig=t,this.sanitizeConfig={...t,rawDates:n}}getConfig(){return this.internConfig}withRawDates(){return new x(this.config,!0)}sanitizeRow(t){return bt(t,this.sanitizeConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s=Rt("<OFX>"+r),{sanitizeRow:a}=this;return s.replace(nt,(i,o,c,f)=>o===f?i:`<${o}>${c}
6
+ </${f}>`).replace(/<(?=[^\/])/g,`
7
+ <`).replace(h,i=>z(i)).replace(B,i=>X(i)).split(`
8
+ `).map(K).filter(Boolean).map(a,this).join("")}}function M(e){return e.toString()}async function _(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}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(M(t))}async fromBlob(t){return new d(await _(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(M(t))}static async fromBlob(t){return new d(await _(t))}}class xt{configInstance={}}class L extends xt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=H(this.configInstance.getPartialJsonData(t));return n?w(`{${I(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=V(this.configInstance.getPartialJsonData(t));return n?w(`{${I(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let n=[],r=[];const s=this.configInstance?.getConfig?.().parserMode||"strict";let a=null,i=null;try{n=this.getBankTransferList(t)||[]}catch(F){a=F,n=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(F){i=F,r=[]}const o=t.includes("<STMTTRN>");if(s==="strict"&&a&&i&&o)throw a;const c=n.length?n:r,f=n.length?m:R,g=n.length?S:p,T=t.indexOf(f),u=t.indexOf(g),Z=T>=0&&u>T?t.slice(T,u+g.length):"",tt=Z.match(/<DTSTART>([^\n<]+)/),et=Z.match(/<DTEND>([^\n<]+)/),Ut=wt(c||[]),vt=tt?.[1]?y({dateString:tt[1],formatDate:this.configInstance.getConfig().formatDate}):"",Pt=et?.[1]?y({dateString:et[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:vt,dateEnd:Pt,...Ut}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=H(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=V(n);let o=n;return r&&(o=o.replace(s,r)),a&&(o=o.replace(i,a)),w(`{${I(o)}}`)}}const Mt="y-M-d";function j(e){if(e==null)return"";if(typeof e=="string"||typeof e=="number")return String(e);if(typeof e=="object"){const t=e;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(e)}catch{return String(e)}}return String(e)}function Y(e){if(e==null)return null;const t=Number(String(e).replace(/,/g,"."));return Number.isNaN(t)?null:t}function _t(e){return e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Lt(e,t){const n=t.dateMode||"formatted";if(!e)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(n==="raw"||n==="formatted"){const s=N(e);return s?n==="formatted"?{value:ot(s,t.formatDate||Mt)}:{value:e}:{value:e,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}const r=N(e);return r?n==="date"?{value:r}:n==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}function Ft(e,t){const n=Y(e);return n===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(e)}'.`,severity:"warning"}}:t==="string"?{amount:String(e),amountAbs:String(Math.abs(n))}:t==="cents"?{amount:Math.round(n*100),amountAbs:Math.round(Math.abs(n)*100)}:{amount:n,amountAbs:Math.abs(n)}}function Q(e){const t=[],n=e?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=e?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"bank",transaction:o,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:n,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${c}`})});const a=e?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(a?.BANKTRANLIST?.STRTTRN||a?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"credit_card",transaction:o,currency:a?.CURDEF||null,account:a?.CCACCTFROM||null,institution:n,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${c}`})}),t}function Bt(e,t={}){const n=t.amountMode||"number",r=[],s=[];return Q(e).forEach(i=>{const o=[],c=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:T}=Ft(i.transaction.TRNAMT,n);T&&(T.path=i.path,o.push(T),r.push(T));const u=Lt(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,o.push(u.warning),r.push(u.warning)),s.push({source:i.source,direction:P(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:c,descriptionNormalized:_t(c),fitId:j(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:o})}),{transactions:s,warnings:r}}function kt(e){const t=[],n=[],r=Q(e);e?.OFX||n.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 s=new Map;r.forEach(c=>{const f=j(c.transaction.FITID);f?s.set(f,(s.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:c.path}),Y(c.transaction.TRNAMT)===null&&n.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(c.transaction.TRNAMT)}'.`,severity:"error",path:c.path}),N(String(c.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(c.transaction.DTPOSTED||"")}'.`,severity:"warning",path:c.path})});const a=Array.from(s.values()).filter(c=>c>1).length;a>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${a} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(c=>c.source==="bank").length,o=r.filter(c=>c.source==="credit_card").length;return{isValid:n.length===0,warnings:t,errors:n,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:o,duplicatedFitIds:a}}}class q{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new L,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new x(t)),this}getType(){return mt(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,n,r){try{return r()}catch(s){if(this.getParserMode()!=="lenient")throw s;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:s instanceof Error?s.message:String(s),path:t}),n}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return At(String(t).split(`
9
+ `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),n=this.getContentInternal();return{...t,...n}}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()}getResponseWithRawDates(){const t=this.customExtractorInstance.configInstance;this.customExtractorInstance.setConfig(t.withRawDates());try{return this.getResponseInternal()}finally{this.customExtractorInstance.setConfig(t)}}toNormalized(t={}){this.beginOperation();const n=this.customExtractorInstance.configInstance.getConfig().formatDate,r=Bt(this.getResponseWithRawDates(),{...t,formatDate:t.formatDate||n});return r.transactions.forEach(s=>Et(s.raw,n)),r}validate(){return this.beginOperation(),kt(this.getResponseWithRawDates())}getWarnings(){return[...this.diagnostics]}}class b{extractor;constructor(t,n){this.extractor=new q(new L),this.extractor.data(new d(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new b(d.fromBuffer(t).getData())}static async fromBlob(t){const n=await d.fromBlob(t);return new b(n.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=q,l.Ofx=b,l.OfxExtractor=L,l.Reader=d,l.blobToString=_,l.bufferToString=M,l.fixJsonProblems=I,l.formatDate=U}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofx-data-extractor",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "keywords": [
5
5
  "ofx",
6
6
  "financial",
@@ -66,7 +66,7 @@
66
66
  "@rollup/plugin-typescript": "^10.0.1",
67
67
  "@types/jest": "^29.5.14",
68
68
  "@types/node": "^18.19.130",
69
- "esbuild": "^0.25.12",
69
+ "esbuild": "^0.28.1",
70
70
  "jest": "^29.7.0",
71
71
  "prettier": "^2.8.8",
72
72
  "rollup": "^3.30.0",