@localpay/verification-engine 0.1.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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/core/bank-fetch.service.d.ts +51 -0
- package/dist/core/bank-fetch.service.d.ts.map +1 -0
- package/dist/core/bank-fetch.service.js +147 -0
- package/dist/core/bank-fetch.service.js.map +1 -0
- package/dist/core/verification-engine.d.ts +68 -0
- package/dist/core/verification-engine.d.ts.map +1 -0
- package/dist/core/verification-engine.js +188 -0
- package/dist/core/verification-engine.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/ethiopia/abyssinia.parser.d.ts +19 -0
- package/dist/parsers/ethiopia/abyssinia.parser.d.ts.map +1 -0
- package/dist/parsers/ethiopia/abyssinia.parser.js +79 -0
- package/dist/parsers/ethiopia/abyssinia.parser.js.map +1 -0
- package/dist/parsers/ethiopia/cbe.parser.d.ts +25 -0
- package/dist/parsers/ethiopia/cbe.parser.d.ts.map +1 -0
- package/dist/parsers/ethiopia/cbe.parser.js +209 -0
- package/dist/parsers/ethiopia/cbe.parser.js.map +1 -0
- package/dist/parsers/ethiopia/ebirr.parser.d.ts +19 -0
- package/dist/parsers/ethiopia/ebirr.parser.d.ts.map +1 -0
- package/dist/parsers/ethiopia/ebirr.parser.js +75 -0
- package/dist/parsers/ethiopia/ebirr.parser.js.map +1 -0
- package/dist/parsers/ethiopia/index.d.ts +3 -0
- package/dist/parsers/ethiopia/index.d.ts.map +1 -0
- package/dist/parsers/ethiopia/index.js +14 -0
- package/dist/parsers/ethiopia/index.js.map +1 -0
- package/dist/parsers/ethiopia/telebirr.parser.d.ts +19 -0
- package/dist/parsers/ethiopia/telebirr.parser.d.ts.map +1 -0
- package/dist/parsers/ethiopia/telebirr.parser.js +123 -0
- package/dist/parsers/ethiopia/telebirr.parser.js.map +1 -0
- package/dist/parsers/index.d.ts +13 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +20 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/kenya/index.d.ts +8 -0
- package/dist/parsers/kenya/index.d.ts.map +1 -0
- package/dist/parsers/kenya/index.js +10 -0
- package/dist/parsers/kenya/index.js.map +1 -0
- package/dist/shared/date-parser.util.d.ts +16 -0
- package/dist/shared/date-parser.util.d.ts.map +1 -0
- package/dist/shared/date-parser.util.js +68 -0
- package/dist/shared/date-parser.util.js.map +1 -0
- package/dist/shared/parser.interface.d.ts +41 -0
- package/dist/shared/parser.interface.d.ts.map +1 -0
- package/dist/shared/parser.interface.js +3 -0
- package/dist/shared/parser.interface.js.map +1 -0
- package/dist/shared/proxy.types.d.ts +27 -0
- package/dist/shared/proxy.types.d.ts.map +1 -0
- package/dist/shared/proxy.types.js +9 -0
- package/dist/shared/proxy.types.js.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LocalPay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# @localpay/verification-engine
|
|
2
|
+
|
|
3
|
+
Bank receipt verification engine for Ethiopian and East African banks.
|
|
4
|
+
|
|
5
|
+
Supports: **CBE**, **Telebirr**, **Bank of Abyssinia**, **E-Birr**
|
|
6
|
+
|
|
7
|
+
- Works in any Node.js project
|
|
8
|
+
- Proxy support via a simple interface (you provide the config, we route the traffic)
|
|
9
|
+
- Amount matching built into verification
|
|
10
|
+
- Supports receipt links, SMS text, transaction references, and OCR images
|
|
11
|
+
- Extensible — add new bank parsers by implementing one interface
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @localpay/verification-engine
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { VerificationEngine } from "@localpay/verification-engine";
|
|
27
|
+
|
|
28
|
+
const engine = new VerificationEngine();
|
|
29
|
+
|
|
30
|
+
const result = await engine.verify({
|
|
31
|
+
bank: "CBE",
|
|
32
|
+
amount: 500,
|
|
33
|
+
verMethod: "LINK",
|
|
34
|
+
rawProof: "https://apps.cbe.com.et:100/?id=FT26093JCD3218872366",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (result.status === "SUCCESS") {
|
|
38
|
+
console.log(result.receipt.receipt.transactionNumber); // FT26093JCD32
|
|
39
|
+
console.log(result.receipt.receipt.amount); // "500"
|
|
40
|
+
console.log(result.receipt.receipt.receiverAccount); // account number
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Supported banks
|
|
47
|
+
|
|
48
|
+
| parserKey | Bank | Methods |
|
|
49
|
+
| ----------- | --------------------------- | ------------------------------- |
|
|
50
|
+
| `CBE` | Commercial Bank of Ethiopia | LINK, SMS, TRANSACTION_REF, OCR |
|
|
51
|
+
| `TELEBIRR` | Telebirr | LINK, SMS, TRANSACTION_REF, OCR |
|
|
52
|
+
| `ABYSSINIA` | Bank of Abyssinia | LINK, SMS, TRANSACTION_REF, OCR |
|
|
53
|
+
| `EBIRR` | E-Birr | LINK, SMS, TRANSACTION_REF, OCR |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Verification methods
|
|
58
|
+
|
|
59
|
+
| Method | Description |
|
|
60
|
+
| ----------------- | --------------------------------------------------- |
|
|
61
|
+
| `LINK` | Pass the receipt URL directly |
|
|
62
|
+
| `SMS` | Pass the raw SMS body — the parser extracts the URL |
|
|
63
|
+
| `TRANSACTION_REF` | Pass only the transaction/reference number |
|
|
64
|
+
| `OCR` | Pass an image path or `Buffer`; OCR runs internally |
|
|
65
|
+
| `SCREENSHOT` | Alias of `OCR` for screenshot image verification |
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const byReference = await engine.verify({
|
|
69
|
+
bank: "TELEBIRR",
|
|
70
|
+
amount: 250,
|
|
71
|
+
verMethod: "TRANSACTION_REF",
|
|
72
|
+
rawProof: "CG7X9A2B",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const byScreenshot = await engine.verify({
|
|
76
|
+
bank: "TELEBIRR",
|
|
77
|
+
amount: 250,
|
|
78
|
+
verMethod: "OCR",
|
|
79
|
+
rawProof: "/tmp/receipt-screenshot.png",
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The engine compares `payload.amount` with the parsed receipt amount. Use
|
|
84
|
+
`amountTolerance` when small rounding differences are expected.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await engine.verify({
|
|
88
|
+
bank: "CBE",
|
|
89
|
+
amount: 500,
|
|
90
|
+
amountTolerance: 0.05,
|
|
91
|
+
verMethod: "LINK",
|
|
92
|
+
rawProof: "https://apps.cbe.com.et:100/?id=...",
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Proxy support
|
|
99
|
+
|
|
100
|
+
Some banks block foreign IPs (e.g. Telebirr). Implement the `ProxyResolver`
|
|
101
|
+
interface to provide per-country proxy config from your data store. The built-in
|
|
102
|
+
parsers route HTTP and browser-backed fetches through this fetch service.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import {
|
|
106
|
+
VerificationEngine,
|
|
107
|
+
ProxyResolver,
|
|
108
|
+
ProxyConfig,
|
|
109
|
+
ProxyType,
|
|
110
|
+
} from "@localpay/verification-engine";
|
|
111
|
+
|
|
112
|
+
class MyProxyResolver implements ProxyResolver {
|
|
113
|
+
async resolve(countryCode: string): Promise<ProxyConfig | null> {
|
|
114
|
+
// Read from your database, config file, environment variable, etc.
|
|
115
|
+
const row = await db.countryProxy.findUnique({ where: { countryCode } });
|
|
116
|
+
if (!row?.proxyEnabled) return null;
|
|
117
|
+
return {
|
|
118
|
+
enabled: true,
|
|
119
|
+
url: row.proxyUrl,
|
|
120
|
+
type: row.proxyType as ProxyType,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const engine = new VerificationEngine({
|
|
126
|
+
proxyResolver: new MyProxyResolver(),
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Adding a new bank
|
|
133
|
+
|
|
134
|
+
1. Implement the `ParserAndExtractor` interface:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import {
|
|
138
|
+
ParserAndExtractor,
|
|
139
|
+
ParserFetchContext,
|
|
140
|
+
RawReceipt,
|
|
141
|
+
} from "@localpay/verification-engine";
|
|
142
|
+
|
|
143
|
+
export class MyBankParser implements ParserAndExtractor {
|
|
144
|
+
extract(text: string, accountNumber?: string): { link: string } {
|
|
145
|
+
// Extract the receipt URL from SMS/OCR text
|
|
146
|
+
const match = text.match(/my-bank\.com\/receipt\/([A-Z0-9]+)/i);
|
|
147
|
+
if (!match) return { link: "" };
|
|
148
|
+
return { link: `https://my-bank.com/receipt/${match[1]}` };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
transactionRef(transactionRef: string): { link: string } {
|
|
152
|
+
return { link: `https://my-bank.com/receipt/${transactionRef}` };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async fetch(
|
|
156
|
+
link: string,
|
|
157
|
+
context?: ParserFetchContext,
|
|
158
|
+
): Promise<{ page: any }> {
|
|
159
|
+
const response = context
|
|
160
|
+
? await context.fetcher.fetch(link, context.countryCode)
|
|
161
|
+
: await fetch(link).then(async (res) => ({ data: await res.text() }));
|
|
162
|
+
|
|
163
|
+
return { page: response.data };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async receiptParser(
|
|
167
|
+
page: any,
|
|
168
|
+
): Promise<{ bank: string; receipt: RawReceipt }> {
|
|
169
|
+
// Parse the page and return structured data
|
|
170
|
+
return {
|
|
171
|
+
bank: "MY_BANK",
|
|
172
|
+
receipt: {
|
|
173
|
+
transactionNumber: "...",
|
|
174
|
+
date: "...",
|
|
175
|
+
amount: "...",
|
|
176
|
+
receiverAccount: "...",
|
|
177
|
+
receiverName: "...",
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
2. Pass it to the engine:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { PARSER_REGISTRY } from "@localpay/verification-engine";
|
|
188
|
+
import { MyBankParser } from "./my-bank.parser";
|
|
189
|
+
|
|
190
|
+
const engine = new VerificationEngine({
|
|
191
|
+
parsers: {
|
|
192
|
+
...PARSER_REGISTRY,
|
|
193
|
+
MY_BANK: new MyBankParser(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## NestJS integration
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { Injectable } from "@nestjs/common";
|
|
204
|
+
import {
|
|
205
|
+
VerificationEngine,
|
|
206
|
+
ProxyResolver,
|
|
207
|
+
} from "@localpay/verification-engine";
|
|
208
|
+
import { PrismaService } from "./prisma.service";
|
|
209
|
+
|
|
210
|
+
@Injectable()
|
|
211
|
+
export class VerificationService {
|
|
212
|
+
private readonly engine: VerificationEngine;
|
|
213
|
+
|
|
214
|
+
constructor(private readonly prisma: PrismaService) {
|
|
215
|
+
const proxyResolver: ProxyResolver = {
|
|
216
|
+
resolve: async (countryCode) => {
|
|
217
|
+
const row = await prisma.countryProxy.findUnique({
|
|
218
|
+
where: { countryCode },
|
|
219
|
+
});
|
|
220
|
+
if (!row?.proxyEnabled) return null;
|
|
221
|
+
return { enabled: true, url: row.proxyUrl, type: row.proxyType as any };
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
this.engine = new VerificationEngine({ proxyResolver });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
verify(payload: Parameters<VerificationEngine["verify"]>[0]) {
|
|
229
|
+
return this.engine.verify(payload);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Publishing
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Build and test
|
|
240
|
+
npm test
|
|
241
|
+
|
|
242
|
+
# Publish (private)
|
|
243
|
+
npm publish --access restricted
|
|
244
|
+
|
|
245
|
+
# Make public later
|
|
246
|
+
npm publish --access public
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`puppeteer` is an optional peer dependency used by parsers that need a
|
|
250
|
+
browser-rendered receipt page. HTTP/PDF-only parsers do not load it. Install it
|
|
251
|
+
in host apps that verify Telebirr or Bank of Abyssinia browser-rendered receipts.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ProxyResolver } from "../shared/proxy.types";
|
|
2
|
+
export interface BankFetchOptions {
|
|
3
|
+
responseType?: "arraybuffer" | "json" | "text";
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
validateStatus?: (status: number) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface BrowserFetchOptions {
|
|
9
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
10
|
+
waitForSelector?: string;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* FetchError is thrown when all retry attempts are exhausted.
|
|
15
|
+
* Catch this in your service layer to handle network failures gracefully.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FetchError extends Error {
|
|
18
|
+
readonly url: string;
|
|
19
|
+
readonly countryCode: string;
|
|
20
|
+
readonly cause: unknown;
|
|
21
|
+
constructor(url: string, countryCode: string, cause: unknown);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* BankFetchService — single HTTP gateway for all bank parsers.
|
|
25
|
+
*
|
|
26
|
+
* Parsers never call axios directly. They call this service, which
|
|
27
|
+
* handles proxy routing, retries, and timeouts transparently.
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* const fetcher = new BankFetchService(myProxyResolver);
|
|
31
|
+
* const { data } = await fetcher.fetch(url, 'ET', { responseType: 'text' });
|
|
32
|
+
*
|
|
33
|
+
* @param proxyResolver Implement ProxyResolver in your host app to provide
|
|
34
|
+
* proxy config from your database or config store.
|
|
35
|
+
* Pass null to disable proxy support entirely.
|
|
36
|
+
*/
|
|
37
|
+
export declare class BankFetchService {
|
|
38
|
+
private readonly proxyResolver;
|
|
39
|
+
private readonly MAX_RETRIES;
|
|
40
|
+
private readonly DEFAULT_TIMEOUT_MS;
|
|
41
|
+
constructor(proxyResolver?: ProxyResolver | null);
|
|
42
|
+
fetch(url: string, countryCode: string, options?: BankFetchOptions): Promise<{
|
|
43
|
+
data: any;
|
|
44
|
+
status: number;
|
|
45
|
+
headers: any;
|
|
46
|
+
}>;
|
|
47
|
+
fetchBrowserPage(url: string, countryCode: string, options?: BrowserFetchOptions): Promise<{
|
|
48
|
+
data: string;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=bank-fetch.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bank-fetch.service.d.ts","sourceRoot":"","sources":["../../src/core/bank-fetch.service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAa,MAAM,uBAAuB,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CAAC,EAAE,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC9C;AAED,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,GAAG,kBAAkB,GAAG,cAAc,GAAG,cAAc,CAAC;IAC1E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;aAEjB,GAAG,EAAE,MAAM;aACX,WAAW,EAAE,MAAM;aACnB,KAAK,EAAE,OAAO;gBAFd,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO;CAOjC;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,gBAAgB;IAIf,OAAO,CAAC,QAAQ,CAAC,aAAa;IAH1C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAK;IACjC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAU;gBAEhB,aAAa,GAAE,aAAa,GAAG,IAAW;IAEjE,KAAK,CACT,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,GAAG,CAAA;KAAE,CAAC;IA0CjD,gBAAgB,CACpB,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAiC7B"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.BankFetchService = exports.FetchError = void 0;
|
|
40
|
+
const axios_1 = __importDefault(require("axios"));
|
|
41
|
+
const https_proxy_agent_1 = require("https-proxy-agent");
|
|
42
|
+
const socks_proxy_agent_1 = require("socks-proxy-agent");
|
|
43
|
+
const proxy_types_1 = require("../shared/proxy.types");
|
|
44
|
+
/**
|
|
45
|
+
* FetchError is thrown when all retry attempts are exhausted.
|
|
46
|
+
* Catch this in your service layer to handle network failures gracefully.
|
|
47
|
+
*/
|
|
48
|
+
class FetchError extends Error {
|
|
49
|
+
constructor(url, countryCode, cause) {
|
|
50
|
+
const reason = cause instanceof Error ? cause.message : "Unknown network error";
|
|
51
|
+
super(`Fetch failed for ${countryCode} — ${url}: ${reason}`);
|
|
52
|
+
this.url = url;
|
|
53
|
+
this.countryCode = countryCode;
|
|
54
|
+
this.cause = cause;
|
|
55
|
+
this.name = "FetchError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.FetchError = FetchError;
|
|
59
|
+
/**
|
|
60
|
+
* BankFetchService — single HTTP gateway for all bank parsers.
|
|
61
|
+
*
|
|
62
|
+
* Parsers never call axios directly. They call this service, which
|
|
63
|
+
* handles proxy routing, retries, and timeouts transparently.
|
|
64
|
+
*
|
|
65
|
+
* Usage:
|
|
66
|
+
* const fetcher = new BankFetchService(myProxyResolver);
|
|
67
|
+
* const { data } = await fetcher.fetch(url, 'ET', { responseType: 'text' });
|
|
68
|
+
*
|
|
69
|
+
* @param proxyResolver Implement ProxyResolver in your host app to provide
|
|
70
|
+
* proxy config from your database or config store.
|
|
71
|
+
* Pass null to disable proxy support entirely.
|
|
72
|
+
*/
|
|
73
|
+
class BankFetchService {
|
|
74
|
+
constructor(proxyResolver = null) {
|
|
75
|
+
this.proxyResolver = proxyResolver;
|
|
76
|
+
this.MAX_RETRIES = 2;
|
|
77
|
+
this.DEFAULT_TIMEOUT_MS = 10000;
|
|
78
|
+
}
|
|
79
|
+
async fetch(url, countryCode, options = {}) {
|
|
80
|
+
const proxy = (await this.proxyResolver?.resolve(countryCode)) ?? null;
|
|
81
|
+
const timeoutMs = options.timeoutMs ?? this.DEFAULT_TIMEOUT_MS;
|
|
82
|
+
let lastError;
|
|
83
|
+
for (let attempt = 1; attempt <= this.MAX_RETRIES; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
const config = {
|
|
86
|
+
url,
|
|
87
|
+
method: "GET",
|
|
88
|
+
responseType: options.responseType ?? "text",
|
|
89
|
+
headers: options.headers ?? {},
|
|
90
|
+
timeout: timeoutMs,
|
|
91
|
+
validateStatus: options.validateStatus,
|
|
92
|
+
};
|
|
93
|
+
if (proxy?.enabled && proxy.url) {
|
|
94
|
+
config.httpsAgent =
|
|
95
|
+
proxy.type === proxy_types_1.ProxyType.SOCKS5
|
|
96
|
+
? new socks_proxy_agent_1.SocksProxyAgent(proxy.url)
|
|
97
|
+
: new https_proxy_agent_1.HttpsProxyAgent(proxy.url);
|
|
98
|
+
config.proxy = false;
|
|
99
|
+
}
|
|
100
|
+
const response = await (0, axios_1.default)(config);
|
|
101
|
+
return {
|
|
102
|
+
data: response.data,
|
|
103
|
+
status: response.status,
|
|
104
|
+
headers: response.headers,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
lastError = err;
|
|
109
|
+
if (attempt < this.MAX_RETRIES) {
|
|
110
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
throw new FetchError(url, countryCode, lastError);
|
|
115
|
+
}
|
|
116
|
+
async fetchBrowserPage(url, countryCode, options = {}) {
|
|
117
|
+
const proxy = (await this.proxyResolver?.resolve(countryCode)) ?? null;
|
|
118
|
+
const timeoutMs = options.timeoutMs ?? 60000;
|
|
119
|
+
const args = ["--no-sandbox", "--disable-setuid-sandbox"];
|
|
120
|
+
if (proxy?.enabled && proxy.url) {
|
|
121
|
+
args.push(`--proxy-server=${proxy.url}`);
|
|
122
|
+
}
|
|
123
|
+
const puppeteer = await Promise.resolve().then(() => __importStar(require("puppeteer")));
|
|
124
|
+
const browser = await puppeteer.default.launch({
|
|
125
|
+
headless: true,
|
|
126
|
+
args,
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
const page = await browser.newPage();
|
|
130
|
+
await page.goto(url, {
|
|
131
|
+
waitUntil: options.waitUntil ?? "domcontentloaded",
|
|
132
|
+
timeout: timeoutMs,
|
|
133
|
+
});
|
|
134
|
+
if (options.waitForSelector) {
|
|
135
|
+
await page
|
|
136
|
+
.waitForSelector(options.waitForSelector, { timeout: timeoutMs })
|
|
137
|
+
.catch(() => { });
|
|
138
|
+
}
|
|
139
|
+
return { data: await page.content() };
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await browser.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.BankFetchService = BankFetchService;
|
|
147
|
+
//# sourceMappingURL=bank-fetch.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bank-fetch.service.js","sourceRoot":"","sources":["../../src/core/bank-fetch.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ParserRegistry } from "../shared/parser.interface";
|
|
2
|
+
import { ProxyResolver } from "../shared/proxy.types";
|
|
3
|
+
export type VerificationMethod = "LINK" | "SMS" | "TRANSACTION_REF" | "OCR" | "SCREENSHOT";
|
|
4
|
+
export type RawProof = string | Buffer;
|
|
5
|
+
export interface VerifyPayload {
|
|
6
|
+
/** parserKey — e.g. 'CBE', 'TELEBIRR', 'ABYSSINIA', 'EBIRR' */
|
|
7
|
+
bank: string;
|
|
8
|
+
amount: number;
|
|
9
|
+
verMethod: VerificationMethod;
|
|
10
|
+
rawProof: RawProof;
|
|
11
|
+
accountNumber?: string;
|
|
12
|
+
countryCode?: string;
|
|
13
|
+
amountTolerance?: number;
|
|
14
|
+
}
|
|
15
|
+
export type VerifyResult = {
|
|
16
|
+
status: "SUCCESS";
|
|
17
|
+
receipt: {
|
|
18
|
+
bank: string;
|
|
19
|
+
receipt: {
|
|
20
|
+
transactionNumber: string;
|
|
21
|
+
date: string;
|
|
22
|
+
amount: string;
|
|
23
|
+
receiverAccount: string;
|
|
24
|
+
receiverName: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
} | {
|
|
28
|
+
status: "FAIL";
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* VerificationEngine — the main entry point for the package.
|
|
33
|
+
*
|
|
34
|
+
* Plain TypeScript class. No NestJS, no Prisma, no framework.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
*
|
|
38
|
+
* // Minimal — no proxy support
|
|
39
|
+
* const engine = new VerificationEngine();
|
|
40
|
+
*
|
|
41
|
+
* // With proxy support — implement ProxyResolver in your app
|
|
42
|
+
* const engine = new VerificationEngine({ proxyResolver: myResolver });
|
|
43
|
+
*
|
|
44
|
+
* // With custom parsers (extend or override)
|
|
45
|
+
* const engine = new VerificationEngine({ parsers: { ...PARSER_REGISTRY, MY_BANK: new MyParser() } });
|
|
46
|
+
*
|
|
47
|
+
* const result = await engine.verify({
|
|
48
|
+
* bank: 'CBE',
|
|
49
|
+
* amount: 500,
|
|
50
|
+
* verMethod: 'LINK',
|
|
51
|
+
* rawProof: 'https://apps.cbe.com.et:100/?id=FT26093JCD3218872366',
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
export declare class VerificationEngine {
|
|
55
|
+
private readonly parsers;
|
|
56
|
+
private readonly fetcher;
|
|
57
|
+
private readonly ocrReader;
|
|
58
|
+
constructor(options?: {
|
|
59
|
+
proxyResolver?: ProxyResolver | null;
|
|
60
|
+
ocrReader?: (input: RawProof) => Promise<string>;
|
|
61
|
+
/** Override or extend the default parser registry */
|
|
62
|
+
parsers?: ParserRegistry;
|
|
63
|
+
});
|
|
64
|
+
verify(payload: VerifyPayload): Promise<VerifyResult>;
|
|
65
|
+
/** List all registered parser keys */
|
|
66
|
+
getSupportedBanks(): string[];
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=verification-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verification-engine.d.ts","sourceRoot":"","sources":["../../src/core/verification-engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGtD,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,KAAK,GACL,iBAAiB,GACjB,KAAK,GACL,YAAY,CAAC;AAEjB,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvC,MAAM,WAAW,aAAa;IAC5B,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,YAAY,GACpB;IACE,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE;YACP,iBAAiB,EAAE,MAAM,CAAC;YAC1B,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,EAAE,MAAM,CAAC;YACf,eAAe,EAAE,MAAM,CAAC;YACxB,YAAY,EAAE,MAAM,CAAC;SACtB,CAAC;KACH,CAAC;CACH,GACD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuC;gBAErD,OAAO,CAAC,EAAE;QACpB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;QACrC,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;QACjD,qDAAqD;QACrD,OAAO,CAAC,EAAE,cAAc,CAAC;KAC1B;IAMK,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;IAwG3D,sCAAsC;IACtC,iBAAiB,IAAI,MAAM,EAAE;CAG9B"}
|