Skip to content

Commit 284db9c

Browse files
authored
Merge pull request #275 from maxmind/kevin/report-transaction
Add support for the Report Transaction API
2 parents 9182d94 + 387b52e commit 284db9c

File tree

8 files changed

+238
-5
lines changed

8 files changed

+238
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
1.7.0 (XXXX-XX-XX)
5+
------------------
6+
7+
* Added support for the Report Transaction API.
8+
49
1.6.0 (2020-04-06)
510
------------------
611

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Node.js API for MaxMind minFraud Score, Insights, and Factors
22

33
## Description
4-
This package provides an API for the [MaxMind minFraud Score, Insights, and
5-
Factors web services](https://dev.maxmind.com/minfraud/).
4+
This package provides an API for the [MaxMind minFraud Score, Insights,
5+
Factors, and Report Transaction web services](https://dev.maxmind.com/minfraud/).
66

77
## Requirements
88

@@ -75,6 +75,49 @@ of:
7575
{
7676
code: string
7777
error: string
78+
url: string
79+
}
80+
```
81+
82+
### Reporting a transaction using the Report Transactions API
83+
84+
MaxMind encourages the use of this API, as data received through this channel
85+
is continually used to improve the accuracy of our fraud detection algorithms.
86+
87+
To use the Report Transactions API, create a new `TransactionReport` object. An
88+
IP address and a valid tag are required key values. Additional key values may
89+
also be set, as documented below.
90+
91+
See the API documentation for more details.
92+
93+
```js
94+
const transactionReport = new minFraud.TransactionReport({
95+
ipAddress: '8.8.8.8',
96+
tag: minFraud.Constants.Tag.NOT_FRAUD,
97+
98+
// The following key/values are not mandatory but are encouraged
99+
chargebackCode: 'the string provided by your payment processor indicating
100+
the reason for the chargeback',
101+
maxmindId: '12345678',
102+
minfraudId: '58fa38d8-4b87-458b-a22b-f00eda1aa20d',
103+
notes: 'some notes',
104+
transactionId: 'the transaction ID you originally passed to minFraud',
105+
});
106+
107+
client.reportTransaction(transactionReport).then(() => ...);
108+
```
109+
110+
111+
If the request succeeds, no data is returned in the Promise.
112+
113+
If the request fails, an error object will be returned in the catch in the
114+
form of:
115+
116+
```js
117+
{
118+
code: string
119+
error: string
120+
url: string
78121
}
79122
```
80123

src/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,10 @@ export enum Processor {
143143
Wirecard = 'wirecard',
144144
Worldpay = 'worldpay',
145145
}
146+
147+
export enum Tag {
148+
CHARGEBACK = 'chargeback',
149+
NOT_FRAUD = 'not_fraud',
150+
SPAM_OR_ABUSE = 'spam_or_abuse',
151+
SUSPECTED_FRAUD = 'suspected_fraud',
152+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Payment from './request/payment';
1111
import Shipping from './request/shipping';
1212
import ShoppingCartItem from './request/shopping-cart-item';
1313
import Transaction from './request/transaction';
14+
import TransactionReport from './request/transaction-report';
1415
import Client from './webServiceClient';
1516

1617
export {
@@ -28,4 +29,5 @@ export {
2829
Shipping,
2930
ShoppingCartItem,
3031
Transaction,
32+
TransactionReport,
3133
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Tag } from '../constants';
2+
import { ArgumentError } from '../errors';
3+
import TransactionReport from './transaction-report';
4+
5+
describe('Device()', () => {
6+
it('throws an error if TransactionReport.ipAddress is not valid', () => {
7+
const report = () =>
8+
new TransactionReport({
9+
ipAddress: 'foo',
10+
tag: Tag.CHARGEBACK,
11+
});
12+
expect(report).toThrowError(ArgumentError);
13+
expect(report).toThrowError('transactionReport.ipAddress');
14+
});
15+
16+
it('throws an error if TransactionReport.tag is not valid', () => {
17+
const report = () =>
18+
new TransactionReport({
19+
ipAddress: '1.1.1.1',
20+
// @ts-ignore
21+
tag: 'foobar',
22+
});
23+
expect(report).toThrowError(ArgumentError);
24+
expect(report).toThrowError('transactionReport.tag');
25+
});
26+
27+
it('constructs', () => {
28+
expect(() => {
29+
const device = new TransactionReport({
30+
ipAddress: '1.1.1.1',
31+
tag: Tag.CHARGEBACK,
32+
});
33+
}).not.toThrow();
34+
});
35+
});

src/request/transaction-report.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { isIP } from 'net';
2+
import * as snakecaseKeys from 'snakecase-keys';
3+
import { Tag } from '../constants';
4+
import { ArgumentError } from '../errors';
5+
6+
interface TransactionReportProps {
7+
/**
8+
* A string which is provided by your payment processor indicating the
9+
* reason for the chargeback.
10+
*/
11+
chargebackCode?: string;
12+
/**
13+
* The IP address of the customer placing the order.
14+
*/
15+
ipAddress: string;
16+
/**
17+
* A unique eight character string identifying a minFraud Standard or
18+
* Premium request. These IDs are returned in the maxmindID field of a
19+
* response for a successful minFraud request. This field is not required,
20+
* but you are encouraged to provide it, if possible.
21+
*/
22+
maxmindId?: string;
23+
/**
24+
* A UUID that identifies a minFraud Score, minFraud Insights, or minFraud
25+
* Factors request. This ID is returned at /id in the response. This field
26+
* is not required, but you are encouraged to provide it if the request was
27+
* made to one of these services.
28+
*/
29+
minfraudId?: string;
30+
/**
31+
* Your notes on the fraud tag associated with the transaction. We manually
32+
* review many reported transactions to improve our scoring for you so any
33+
* additional details to help us understand context are helpful.
34+
*/
35+
notes?: string;
36+
/**
37+
* A Tag enum indicating the likelihood that a transaction may be
38+
* fraudulent.
39+
*/
40+
tag: Tag;
41+
/**
42+
* The transaction ID you originally passed to minFraud. This field is not
43+
* required, but you are encouraged to provide it or the transaction’s
44+
* maxmindId or minfraudId.
45+
*/
46+
transactionId?: string;
47+
}
48+
49+
export default class TransactionReport {
50+
/** @inheritDoc TransactionReportProps.chargebackCode */
51+
public chargebackCode?: string;
52+
/** @inheritDoc TransactionReportProps.ipAddress */
53+
public ipAddress: string;
54+
/** @inheritDoc TransactionReportProps.maxmindId */
55+
public maxmindId?: string;
56+
/** @inheritDoc TransactionReportProps.minfraudId */
57+
public minfraudId?: string;
58+
/** @inheritDoc TransactionReportProps.notes */
59+
public notes?: string;
60+
/** @inheritDoc TransactionReportProps.tag */
61+
public tag: Tag;
62+
/** @inheritDoc TransactionReportProps.transactionId */
63+
public transactionId?: string;
64+
65+
public constructor(transactionReport: TransactionReportProps) {
66+
if (isIP(transactionReport.ipAddress) === 0) {
67+
throw new ArgumentError(
68+
'`transactionReport.ipAddress` is an invalid IP address'
69+
);
70+
}
71+
72+
if (
73+
!transactionReport.tag ||
74+
!Object.values(Tag).includes(transactionReport.tag)
75+
) {
76+
throw new ArgumentError(
77+
`"${transactionReport.tag}" is an invalid value for "transactionReport.tag"`
78+
);
79+
}
80+
81+
this.ipAddress = transactionReport.ipAddress;
82+
this.tag = transactionReport.tag;
83+
Object.assign(this, transactionReport);
84+
}
85+
86+
public toString(): string {
87+
return JSON.stringify(snakecaseKeys(this));
88+
}
89+
}

src/webServiceClient.spec.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import * as nock from 'nock';
33
import * as insights from '../fixtures/insights.json';
44
import * as score from '../fixtures/score.json';
55
import * as subscores from '../fixtures/subscores.json';
6-
import { Client, Device, Transaction } from './index';
6+
import {
7+
Client,
8+
Constants,
9+
Device,
10+
Transaction,
11+
TransactionReport,
12+
} from './index';
713
import * as webRecords from './response/web-records';
814
import { camelizeResponse } from './utils';
915

@@ -130,6 +136,43 @@ describe('WebServiceClient', () => {
130136
});
131137
});
132138

139+
describe('reportTransaction', () => {
140+
it('handles bare minimum request', () => {
141+
const report = new TransactionReport({
142+
ipAddress: '1.1.1.1',
143+
tag: Constants.Tag.CHARGEBACK,
144+
});
145+
146+
expect.assertions(1);
147+
148+
nockInstance
149+
.post(fullPath('transactions/report'), report.toString())
150+
.reply(204);
151+
152+
return expect(client.reportTransaction(report)).resolves.toBeUndefined();
153+
});
154+
155+
it('handles a "full" request', () => {
156+
const report = new TransactionReport({
157+
chargebackCode: 'foobar',
158+
ipAddress: '1.1.1.1',
159+
maxmindId: '1234',
160+
minfraudId: '1234',
161+
notes: 'hello world',
162+
tag: Constants.Tag.CHARGEBACK,
163+
transactionId: 'what the',
164+
});
165+
166+
expect.assertions(1);
167+
168+
nockInstance
169+
.post(fullPath('transactions/report'), report.toString())
170+
.reply(204);
171+
172+
return expect(client.reportTransaction(report)).resolves.toBeUndefined();
173+
});
174+
});
175+
133176
describe('error handling', () => {
134177
const transaction = new Transaction({
135178
device: new Device({

src/webServiceClient.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as http from 'http';
22
import * as https from 'https';
33
import { version } from '../package.json';
44
import Transaction from './request/transaction';
5+
import TransactionReport from './request/transaction-report';
56
import * as models from './response/models';
67
import { WebServiceClientError } from './types';
78

@@ -10,7 +11,7 @@ interface ResponseError {
1011
error?: string;
1112
}
1213

13-
type servicePath = 'factors' | 'insights' | 'score';
14+
type servicePath = 'factors' | 'insights' | 'score' | 'transactions/report';
1415

1516
export default class WebServiceClient {
1617
private accountID: string;
@@ -49,10 +50,14 @@ export default class WebServiceClient {
4950
);
5051
}
5152

53+
public reportTransaction(report: TransactionReport): Promise<void> {
54+
return this.responseFor<void>('transactions/report', report.toString());
55+
}
56+
5257
private responseFor<T>(
5358
path: servicePath,
5459
postData: string,
55-
modelClass: any
60+
modelClass?: any
5661
): Promise<T> {
5762
const parsedPath = `/minfraud/v2.0/${path}`;
5863
const url = `https://${this.host}${parsedPath}`;
@@ -80,6 +85,10 @@ export default class WebServiceClient {
8085
});
8186

8287
response.on('end', () => {
88+
if (response.statusCode && response.statusCode === 204) {
89+
return resolve();
90+
}
91+
8392
try {
8493
data = JSON.parse(data);
8594
} catch {

0 commit comments

Comments
 (0)