Skip to content

Commit 9417715

Browse files
Merge pull request #261 from kontist/feat/verification-of-payee
[KRP-2183] Verification of payee endpoint
2 parents 7525760 + 6e8be5e commit 9417715

File tree

9 files changed

+361
-16
lines changed

9 files changed

+361
-16
lines changed

npm-shrinkwrap.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kontist/mock-solaris",
3-
"version": "1.0.170",
3+
"version": "1.0.171",
44
"description": "Mock Service for Solaris API",
55
"main": "dist/src/index.js",
66
"types": "dist/src/index.d.ts",

src/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import * as termsAPI from "./routes/termsAndConditions";
3232
import * as psd2API from "./routes/psd2";
3333
import * as postboxItemAPI from "./routes/postbox";
3434
import * as topUpsAPI from "./routes/topUps";
35+
import * as verificationOfPayeeAPI from "./routes/verificationOfPayee";
3536
import * as instantCreditTransferAPI from "./routes/instantCreditTransfer";
3637
import * as accountOpeningRequestAPI from "./routes/accountOpeningRequest";
3738
import * as accountClosureRequestAPI from "./routes/accountClosureRequest";
@@ -434,6 +435,12 @@ router.patch(
434435
safeRequestHandler(taxIdentificationsAPI.updateTaxIdentification)
435436
);
436437

438+
// VERIFICATION OF PAYEE
439+
router.post(
440+
"/verifications_of_payee",
441+
safeRequestHandler(verificationOfPayeeAPI.verifyPayee)
442+
);
443+
437444
// TRANSACTIONS
438445
router.post(
439446
"/persons/:person_id/accounts/:account_id/transactions/sepa_credit_transfer",

src/helpers/verificationOfPayee.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Date is set to 6th October for internal testing
2+
// The real final start date is 9th October 2025
3+
const vopStartDate = new Date("2025-10-06");
4+
5+
export const isVerificationOfPayeeRequired = (): boolean => {
6+
const currentDate = new Date();
7+
8+
return currentDate >= vopStartDate;
9+
};

src/routes/transactions.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { createSepaDirectDebitReturn } from "../helpers/sepaDirectDebitReturn";
2020
import { triggerBookingsWebhook } from "./backoffice";
2121
import generateID from "../helpers/id";
22+
import { isVerificationOfPayeeRequired } from "../helpers/verificationOfPayee";
2223

2324
export const DIRECT_DEBIT_REFUND_METHOD = "direct_debit_refund";
2425

@@ -146,6 +147,20 @@ export const createSepaCreditTransfer = async (req, res) => {
146147
const { person_id: personId, account_id: accountId } = req.params;
147148
const transfer = req.body;
148149

150+
if (isVerificationOfPayeeRequired() && !transfer.verification_of_payee_id) {
151+
return res.status(400).send({
152+
errors: [
153+
{
154+
id: generateID(),
155+
status: 400,
156+
code: "bad_request",
157+
title: "Bad Request",
158+
detail: `Verification of payee is required.`,
159+
},
160+
],
161+
});
162+
}
163+
149164
log.debug("createSepaCreditTransfer", {
150165
body: req.body,
151166
params: req.params,

src/routes/verificationOfPayee.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import _ from "lodash";
2+
import type { Request, Response } from "express";
3+
import uuid from "node-uuid";
4+
import HttpStatusCodes from "http-status";
5+
6+
const nameValidation: RegExp = /^[\p{L}\p{M}\p{N}\p{P}\p{S}\s]+$/u;
7+
8+
const noMatchRegex: RegExp = /no[_ ]match/i;
9+
const notPossibleRegex: RegExp = /not[_ ]possible/i;
10+
const closeMatchRegex: RegExp = /close[_ ]match/i;
11+
12+
/**
13+
* Creates a verification of Payee
14+
*
15+
* By default will always return a "MATCH" status.
16+
* For other statuses use the appropriate regex patterns in the name field.
17+
* - "NO_MATCH": if name contains "no match" or "no_match" - case insensitive
18+
* - "VERIFICATION_NOT_POSSIBLE": if name contains "not possible" or "not_possible" - case insensitive
19+
* - "CLOSE_MATCH": if name contains "close match" or "close_match" - case insensitive
20+
*
21+
* @see https://docs.solarisgroup.com/api-reference/digital-banking/sepa-transfers/#tag/Verification-of-Payee/paths/~1v1~1verifications_of_payee/post
22+
*/
23+
export const verifyPayee = (req: Request, res: Response) => {
24+
const data = _.pick(req.body, ["iban", "name"]);
25+
const { iban, name } = data;
26+
27+
if (!iban) {
28+
return res.status(HttpStatusCodes.BAD_REQUEST).send({
29+
errors: [
30+
{
31+
id: uuid.v4(),
32+
status: HttpStatusCodes.BAD_REQUEST,
33+
code: "bad_request",
34+
title: "Bad Request",
35+
detail: "IBAN is required.",
36+
},
37+
],
38+
});
39+
}
40+
41+
if (!name) {
42+
return res.status(HttpStatusCodes.BAD_REQUEST).send({
43+
errors: [
44+
{
45+
id: uuid.v4(),
46+
status: HttpStatusCodes.BAD_REQUEST,
47+
code: "bad_request",
48+
title: "Bad Request",
49+
detail: "Name is required.",
50+
},
51+
],
52+
});
53+
}
54+
55+
if (!nameValidation.test(name) || name.length > 140) {
56+
return res.status(HttpStatusCodes.BAD_REQUEST).send({
57+
errors: [
58+
{
59+
id: uuid.v4(),
60+
status: HttpStatusCodes.BAD_REQUEST,
61+
code: "bad_request",
62+
title: "Bad Request",
63+
detail: "Name is not valid.",
64+
},
65+
],
66+
});
67+
}
68+
69+
let result: { status: string; suggested_name?: string } = { status: "MATCH" };
70+
71+
if (noMatchRegex.test(name)) {
72+
result = {
73+
status: "NO_MATCH",
74+
};
75+
}
76+
77+
if (notPossibleRegex.test(name)) {
78+
result = {
79+
status: "VERIFICATION_NOT_POSSIBLE",
80+
};
81+
}
82+
83+
if (closeMatchRegex.test(name)) {
84+
result = {
85+
status: "CLOSE_MATCH",
86+
suggested_name: "Giovanni Kontistini",
87+
};
88+
}
89+
90+
const dateNow = new Date();
91+
const response = {
92+
id: uuid.v4(),
93+
payee: {
94+
iban,
95+
name,
96+
},
97+
result,
98+
created_at: dateNow.toISOString(),
99+
expires_at: new Date(dateNow.getTime() + 5 * 60 * 1000).toISOString(),
100+
};
101+
102+
return res.status(HttpStatusCodes.CREATED).send(response);
103+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import sinon from "sinon";
2+
import { expect } from "chai";
3+
4+
import { isVerificationOfPayeeRequired } from "../../src/helpers/verificationOfPayee";
5+
6+
describe("isVerificationOfPayeeRequired", () => {
7+
let clock;
8+
9+
afterEach(() => {
10+
clock.restore();
11+
});
12+
13+
it("should return true if verification of payee is enabled", () => {
14+
// 1st November 2025
15+
clock = sinon.useFakeTimers(new Date(2025, 10, 1).getTime());
16+
17+
expect(isVerificationOfPayeeRequired()).to.be.true;
18+
});
19+
20+
it("should return false if verification of payee is not enabled", () => {
21+
// 1st October 2025
22+
clock = sinon.useFakeTimers(new Date(2025, 9, 1).getTime());
23+
24+
expect(isVerificationOfPayeeRequired()).to.be.false;
25+
});
26+
});

tests/routes/transactions.spec.ts

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import sinon from "sinon";
22
import { expect } from "chai";
3+
import { mockReq, mockRes } from "sinon-express-mock";
34

45
import * as db from "../../src/db";
56
import * as transactions from "../../src/routes/transactions";
67
import { createPerson } from "../../src/routes/persons";
78

89
describe("Transactions", () => {
9-
let res;
10-
const createPersonReq = {
11-
body: {},
12-
headers: {},
13-
};
14-
15-
beforeEach(async () => {
16-
await db.flushDb();
17-
res = {
18-
status: sinon.stub().callsFake(() => res),
19-
send: sinon.stub(),
10+
describe("directDebitRefund", () => {
11+
let res;
12+
const createPersonReq = {
13+
body: {},
14+
headers: {},
2015
};
21-
});
2216

23-
describe("directDebitRefund", () => {
17+
beforeEach(async () => {
18+
await db.flushDb();
19+
res = {
20+
status: sinon.stub().callsFake(() => res),
21+
send: sinon.stub(),
22+
};
23+
});
24+
2425
const req = (personId) => ({
2526
params: {
2627
person_id: personId,
@@ -53,4 +54,34 @@ describe("Transactions", () => {
5354
});
5455
});
5556
});
57+
58+
describe("createSepaCreditTransfer", () => {
59+
let clock;
60+
61+
after(() => {
62+
clock.restore();
63+
});
64+
65+
it("should throw error if verification of payee is required and not passed", async () => {
66+
// 1st November 2025
67+
clock = sinon.useFakeTimers(new Date(2025, 10, 1).getTime());
68+
69+
const req = mockReq({
70+
params: {
71+
person_id: "person-id",
72+
account_id: "account-id",
73+
},
74+
body: {},
75+
});
76+
const res = mockRes();
77+
78+
await transactions.createSepaCreditTransfer(req, res);
79+
80+
expect(res.status.calledWith(400)).to.be.true;
81+
expect(res.send.calledOnce).to.be.true;
82+
expect(res.send.args[0][0].errors[0].detail).to.deep.equal(
83+
"Verification of payee is required."
84+
);
85+
});
86+
});
5687
});

0 commit comments

Comments
 (0)