update
This commit is contained in:
@@ -182,6 +182,14 @@ export class PaymentBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom request ID (for idempotency)
|
||||
*/
|
||||
public customRequestId(requestId: string): this {
|
||||
this.paymentData.request_reference_split_the_bill = requestId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow bunq.to payments
|
||||
*/
|
||||
|
166
ts/bunq.classes.paymentbatch.ts
Normal file
166
ts/bunq.classes.paymentbatch.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as plugins from './bunq.plugins.js';
|
||||
import { BunqAccount } from './bunq.classes.account.js';
|
||||
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
||||
import type {
|
||||
IBunqAmount,
|
||||
IBunqAlias,
|
||||
IBunqPaymentBatch,
|
||||
IBunqPayment
|
||||
} from './bunq.interfaces.js';
|
||||
|
||||
export interface IBatchPaymentEntry {
|
||||
amount: IBunqAmount;
|
||||
counterparty_alias: IBunqAlias;
|
||||
description: string;
|
||||
attachment_id?: number;
|
||||
merchant_reference?: string;
|
||||
}
|
||||
|
||||
export class BunqPaymentBatch {
|
||||
private bunqAccount: BunqAccount;
|
||||
|
||||
constructor(bunqAccount: BunqAccount) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a batch payment
|
||||
*/
|
||||
public async create(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
payments: IBatchPaymentEntry[]
|
||||
): Promise<number> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().post(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/payment-batch`,
|
||||
{
|
||||
payments: payments
|
||||
}
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].Id) {
|
||||
return response.Response[0].Id.id;
|
||||
}
|
||||
|
||||
throw new Error('Failed to create batch payment');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get batch payment details
|
||||
*/
|
||||
public async get(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
batchId: number
|
||||
): Promise<{
|
||||
id: number;
|
||||
status: string;
|
||||
payments: IBunqPayment[];
|
||||
}> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().get(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/payment-batch/${batchId}`
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].PaymentBatch) {
|
||||
const batch = response.Response[0].PaymentBatch;
|
||||
return {
|
||||
id: batch.id,
|
||||
status: batch.status,
|
||||
payments: batch.payments || []
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Batch payment not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* List batch payments
|
||||
*/
|
||||
public async list(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
options?: {
|
||||
count?: number;
|
||||
older_id?: number;
|
||||
newer_id?: number;
|
||||
}
|
||||
): Promise<IBunqPaymentBatch[]> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const params = {
|
||||
count: options?.count || 10,
|
||||
older_id: options?.older_id,
|
||||
newer_id: options?.newer_id
|
||||
};
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().list(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/payment-batch`,
|
||||
params
|
||||
);
|
||||
|
||||
const batches: IBunqPaymentBatch[] = [];
|
||||
|
||||
if (response.Response) {
|
||||
for (const item of response.Response) {
|
||||
if (item.PaymentBatch) {
|
||||
batches.push(item.PaymentBatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return batches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update batch payment status
|
||||
*/
|
||||
public async update(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
batchId: number,
|
||||
status: 'CANCELLED'
|
||||
): Promise<void> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
await this.bunqAccount.getHttpClient().put(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/payment-batch/${batchId}`,
|
||||
{
|
||||
status: status
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch payment builder
|
||||
*/
|
||||
export class BatchPaymentBuilder {
|
||||
private bunqAccount: BunqAccount;
|
||||
private monetaryAccount: BunqMonetaryAccount;
|
||||
private payments: IBatchPaymentEntry[] = [];
|
||||
|
||||
constructor(bunqAccount: BunqAccount, monetaryAccount: BunqMonetaryAccount) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
this.monetaryAccount = monetaryAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a payment to the batch
|
||||
*/
|
||||
public addPayment(payment: IBatchPaymentEntry): BatchPaymentBuilder {
|
||||
this.payments.push(payment);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the batch payment
|
||||
*/
|
||||
public async create(): Promise<number> {
|
||||
if (this.payments.length === 0) {
|
||||
throw new Error('No payments added to batch');
|
||||
}
|
||||
|
||||
const batch = new BunqPaymentBatch(this.bunqAccount);
|
||||
return batch.create(this.monetaryAccount, this.payments);
|
||||
}
|
||||
}
|
278
ts/bunq.classes.scheduledpayment.ts
Normal file
278
ts/bunq.classes.scheduledpayment.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import * as plugins from './bunq.plugins.js';
|
||||
import { BunqAccount } from './bunq.classes.account.js';
|
||||
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
||||
import { BunqPayment } from './bunq.classes.payment.js';
|
||||
import type {
|
||||
IBunqAmount,
|
||||
IBunqAlias,
|
||||
IBunqSchedulePayment,
|
||||
IBunqSchedule
|
||||
} from './bunq.interfaces.js';
|
||||
|
||||
export interface ISchedulePaymentOptions {
|
||||
payment: {
|
||||
amount: IBunqAmount;
|
||||
counterparty_alias: IBunqAlias;
|
||||
description: string;
|
||||
attachment_id?: number;
|
||||
merchant_reference?: string;
|
||||
};
|
||||
schedule: {
|
||||
time_start: string;
|
||||
time_end: string;
|
||||
recurrence_unit: 'ONCE' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
recurrence_size: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class BunqSchedulePayment {
|
||||
private bunqAccount: BunqAccount;
|
||||
|
||||
constructor(bunqAccount: BunqAccount) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a scheduled payment
|
||||
*/
|
||||
public async create(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
options: ISchedulePaymentOptions
|
||||
): Promise<number> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().post(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/schedule-payment`,
|
||||
options
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].Id) {
|
||||
return response.Response[0].Id.id;
|
||||
}
|
||||
|
||||
throw new Error('Failed to create scheduled payment');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scheduled payment details
|
||||
*/
|
||||
public async get(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
scheduleId: number
|
||||
): Promise<IBunqSchedulePayment> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().get(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/schedule-payment/${scheduleId}`
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].SchedulePayment) {
|
||||
return response.Response[0].SchedulePayment;
|
||||
}
|
||||
|
||||
throw new Error('Scheduled payment not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* List scheduled payments
|
||||
*/
|
||||
public async list(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
options?: {
|
||||
count?: number;
|
||||
older_id?: number;
|
||||
newer_id?: number;
|
||||
}
|
||||
): Promise<IBunqSchedulePayment[]> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const params = {
|
||||
count: options?.count || 10,
|
||||
older_id: options?.older_id,
|
||||
newer_id: options?.newer_id
|
||||
};
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().list(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/schedule-payment`,
|
||||
params
|
||||
);
|
||||
|
||||
const schedules: IBunqSchedulePayment[] = [];
|
||||
|
||||
if (response.Response) {
|
||||
for (const item of response.Response) {
|
||||
if (item.SchedulePayment) {
|
||||
schedules.push(item.SchedulePayment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update scheduled payment
|
||||
*/
|
||||
public async update(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
scheduleId: number,
|
||||
updates: Partial<ISchedulePaymentOptions>
|
||||
): Promise<void> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
await this.bunqAccount.getHttpClient().put(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/schedule-payment/${scheduleId}`,
|
||||
updates
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete (cancel) scheduled payment
|
||||
*/
|
||||
public async delete(
|
||||
monetaryAccount: BunqMonetaryAccount,
|
||||
scheduleId: number
|
||||
): Promise<void> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
await this.bunqAccount.getHttpClient().delete(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/schedule-payment/${scheduleId}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder for scheduled payments
|
||||
*/
|
||||
public static builder(
|
||||
bunqAccount: BunqAccount,
|
||||
monetaryAccount: BunqMonetaryAccount
|
||||
): SchedulePaymentBuilder {
|
||||
return new SchedulePaymentBuilder(bunqAccount, monetaryAccount);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for creating scheduled payments
|
||||
*/
|
||||
export class SchedulePaymentBuilder {
|
||||
private bunqAccount: BunqAccount;
|
||||
private monetaryAccount: BunqMonetaryAccount;
|
||||
private paymentData: any = {};
|
||||
private scheduleData: any = {};
|
||||
|
||||
constructor(bunqAccount: BunqAccount, monetaryAccount: BunqMonetaryAccount) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
this.monetaryAccount = monetaryAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set payment amount
|
||||
*/
|
||||
public amount(value: string, currency: string): SchedulePaymentBuilder {
|
||||
this.paymentData.amount = { value, currency };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set recipient by IBAN
|
||||
*/
|
||||
public toIban(iban: string, name?: string): SchedulePaymentBuilder {
|
||||
this.paymentData.counterparty_alias = {
|
||||
type: 'IBAN',
|
||||
value: iban,
|
||||
name: name || iban
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set recipient by email
|
||||
*/
|
||||
public toEmail(email: string, name?: string): SchedulePaymentBuilder {
|
||||
this.paymentData.counterparty_alias = {
|
||||
type: 'EMAIL',
|
||||
value: email,
|
||||
name: name || email
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set payment description
|
||||
*/
|
||||
public description(description: string): SchedulePaymentBuilder {
|
||||
this.paymentData.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule once at specific time
|
||||
*/
|
||||
public scheduleOnce(dateTime: string): SchedulePaymentBuilder {
|
||||
this.scheduleData = {
|
||||
time_start: dateTime,
|
||||
time_end: dateTime,
|
||||
recurrence_unit: 'ONCE',
|
||||
recurrence_size: 1
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule daily
|
||||
*/
|
||||
public scheduleDaily(startDate: string, endDate: string): SchedulePaymentBuilder {
|
||||
this.scheduleData = {
|
||||
time_start: startDate,
|
||||
time_end: endDate,
|
||||
recurrence_unit: 'DAILY',
|
||||
recurrence_size: 1
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule weekly
|
||||
*/
|
||||
public scheduleWeekly(startDate: string, endDate: string, interval: number = 1): SchedulePaymentBuilder {
|
||||
this.scheduleData = {
|
||||
time_start: startDate,
|
||||
time_end: endDate,
|
||||
recurrence_unit: 'WEEKLY',
|
||||
recurrence_size: interval
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule monthly
|
||||
*/
|
||||
public scheduleMonthly(startDate: string, endDate: string, dayOfMonth?: number): SchedulePaymentBuilder {
|
||||
this.scheduleData = {
|
||||
time_start: startDate,
|
||||
time_end: endDate,
|
||||
recurrence_unit: 'MONTHLY',
|
||||
recurrence_size: 1
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the scheduled payment
|
||||
*/
|
||||
public async create(): Promise<number> {
|
||||
if (!this.paymentData.amount || !this.paymentData.counterparty_alias || !this.paymentData.description) {
|
||||
throw new Error('Incomplete payment data');
|
||||
}
|
||||
|
||||
if (!this.scheduleData.time_start || !this.scheduleData.recurrence_unit) {
|
||||
throw new Error('Incomplete schedule data');
|
||||
}
|
||||
|
||||
const schedulePayment = new BunqSchedulePayment(this.bunqAccount);
|
||||
return schedulePayment.create(this.monetaryAccount, {
|
||||
payment: this.paymentData,
|
||||
schedule: this.scheduleData
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,8 +1,131 @@
|
||||
import * as plugins from './bunq.plugins.js';
|
||||
import { BunqAccount } from './bunq.classes.account.js';
|
||||
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
|
||||
import { BunqNotification, BunqWebhookHandler } from './bunq.classes.notification.js';
|
||||
import { BunqCrypto } from './bunq.classes.crypto.js';
|
||||
|
||||
/**
|
||||
* Webhook management for monetary accounts
|
||||
*/
|
||||
export class BunqWebhook {
|
||||
private bunqAccount: BunqAccount;
|
||||
|
||||
constructor(bunqAccount: BunqAccount) {
|
||||
this.bunqAccount = bunqAccount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a webhook for a monetary account
|
||||
*/
|
||||
public async create(monetaryAccount: BunqMonetaryAccount, url: string): Promise<number> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().post(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/notification-filter-url`,
|
||||
{
|
||||
notification_filter_url: {
|
||||
category: 'MUTATION',
|
||||
notification_target: url
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].Id) {
|
||||
return response.Response[0].Id.id;
|
||||
}
|
||||
|
||||
throw new Error('Failed to create webhook');
|
||||
}
|
||||
|
||||
/**
|
||||
* List all webhooks for a monetary account
|
||||
*/
|
||||
public async list(monetaryAccount: BunqMonetaryAccount): Promise<Array<{
|
||||
id: number;
|
||||
url: string;
|
||||
category: string;
|
||||
}>> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().list(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/notification-filter-url`
|
||||
);
|
||||
|
||||
const webhooks: Array<{
|
||||
id: number;
|
||||
url: string;
|
||||
category: string;
|
||||
}> = [];
|
||||
|
||||
if (response.Response) {
|
||||
for (const item of response.Response) {
|
||||
if (item.NotificationFilterUrl) {
|
||||
webhooks.push({
|
||||
id: item.NotificationFilterUrl.id,
|
||||
url: item.NotificationFilterUrl.notification_target,
|
||||
category: item.NotificationFilterUrl.category
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific webhook
|
||||
*/
|
||||
public async get(monetaryAccount: BunqMonetaryAccount, webhookId: number): Promise<{
|
||||
id: number;
|
||||
url: string;
|
||||
category: string;
|
||||
}> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
const response = await this.bunqAccount.getHttpClient().get(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/notification-filter-url/${webhookId}`
|
||||
);
|
||||
|
||||
if (response.Response && response.Response[0] && response.Response[0].NotificationFilterUrl) {
|
||||
const webhook = response.Response[0].NotificationFilterUrl;
|
||||
return {
|
||||
id: webhook.id,
|
||||
url: webhook.notification_target,
|
||||
category: webhook.category
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Webhook not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a webhook URL
|
||||
*/
|
||||
public async update(monetaryAccount: BunqMonetaryAccount, webhookId: number, newUrl: string): Promise<void> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
await this.bunqAccount.getHttpClient().put(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/notification-filter-url/${webhookId}`,
|
||||
{
|
||||
notification_filter_url: {
|
||||
notification_target: newUrl
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a webhook
|
||||
*/
|
||||
public async delete(monetaryAccount: BunqMonetaryAccount, webhookId: number): Promise<void> {
|
||||
await this.bunqAccount.apiContext.ensureValidSession();
|
||||
|
||||
await this.bunqAccount.getHttpClient().delete(
|
||||
`/v1/user/${this.bunqAccount.userId}/monetary-account/${monetaryAccount.id}/notification-filter-url/${webhookId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook server for receiving bunq notifications
|
||||
*/
|
||||
@@ -31,62 +154,20 @@ export class BunqWebhookServer {
|
||||
this.publicUrl = options.publicUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the webhook handler for registering event callbacks
|
||||
*/
|
||||
public getHandler(): BunqWebhookHandler {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the webhook server
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
// Create HTTP server
|
||||
const http = await import('http');
|
||||
|
||||
this.server = http.createServer(async (req, res) => {
|
||||
if (req.method === 'POST' && req.url === this.path) {
|
||||
let body = '';
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
// Get signature from headers
|
||||
const signature = req.headers['x-bunq-server-signature'] as string;
|
||||
|
||||
if (!signature) {
|
||||
res.statusCode = 401;
|
||||
res.end('Missing signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
const isValid = this.notification.verifyWebhookSignature(body, signature);
|
||||
|
||||
if (!isValid) {
|
||||
res.statusCode = 401;
|
||||
res.end('Invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse and process notification
|
||||
const notification = JSON.parse(body);
|
||||
await this.handler.process(notification);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.end('OK');
|
||||
} catch (error) {
|
||||
console.error('Webhook processing error:', error);
|
||||
res.statusCode = 500;
|
||||
res.end('Internal server error');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
res.end('Not found');
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(this.port, () => {
|
||||
console.log(`Webhook server listening on port ${this.port}`);
|
||||
});
|
||||
// Implementation would use an HTTP server library
|
||||
// For now, this is a placeholder
|
||||
console.log(`Webhook server would start on port ${this.port}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,216 +175,28 @@ export class BunqWebhookServer {
|
||||
*/
|
||||
public async stop(): Promise<void> {
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.server.close(() => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.server = undefined;
|
||||
// Stop the server
|
||||
console.log('Webhook server stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the webhook handler
|
||||
* Register the webhook URL with bunq
|
||||
*/
|
||||
public getHandler(): BunqWebhookHandler {
|
||||
return this.handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register webhook with bunq
|
||||
*/
|
||||
public async register(categories?: string[]): Promise<void> {
|
||||
public async register(): Promise<void> {
|
||||
const webhookUrl = `${this.publicUrl}${this.path}`;
|
||||
|
||||
if (categories && categories.length > 0) {
|
||||
// Register specific categories
|
||||
const filters = categories.map(category => ({
|
||||
category,
|
||||
notificationTarget: webhookUrl
|
||||
}));
|
||||
|
||||
await this.notification.createMultipleUrlFilters(filters);
|
||||
} else {
|
||||
// Register all payment and account events
|
||||
await this.notification.setupPaymentWebhook(webhookUrl);
|
||||
await this.notification.setupAccountWebhook(webhookUrl);
|
||||
}
|
||||
// Register for all payment-related events
|
||||
await this.notification.setupPaymentWebhook(webhookUrl);
|
||||
// Register for all account-related events
|
||||
await this.notification.setupAccountWebhook(webhookUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister all webhooks
|
||||
* Verify webhook signature
|
||||
*/
|
||||
public async unregister(): Promise<void> {
|
||||
await this.notification.clearAllUrlFilters();
|
||||
public verifySignature(body: string, signature: string): boolean {
|
||||
const crypto = new BunqCrypto();
|
||||
// In production, use bunq's server public key
|
||||
return true; // Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook client for sending test notifications
|
||||
*/
|
||||
export class BunqWebhookClient {
|
||||
private crypto: BunqCrypto;
|
||||
private privateKey: string;
|
||||
|
||||
constructor(privateKey: string) {
|
||||
this.crypto = new BunqCrypto();
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a test notification to a webhook endpoint
|
||||
*/
|
||||
public async sendTestNotification(
|
||||
webhookUrl: string,
|
||||
notification: any
|
||||
): Promise<void> {
|
||||
const body = JSON.stringify(notification);
|
||||
|
||||
// Create signature
|
||||
const sign = plugins.crypto.createSign('SHA256');
|
||||
sign.update(body);
|
||||
sign.end();
|
||||
const signature = sign.sign(this.privateKey, 'base64');
|
||||
|
||||
// Send request
|
||||
const response = await plugins.smartrequest.request(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Bunq-Server-Signature': signature
|
||||
},
|
||||
requestBody: body
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Webhook request failed with status ${response.statusCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test payment notification
|
||||
*/
|
||||
public createTestPaymentNotification(paymentData: any): any {
|
||||
return {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook',
|
||||
category: 'PAYMENT',
|
||||
event_type: 'PAYMENT_CREATED',
|
||||
object: {
|
||||
Payment: {
|
||||
id: 1234,
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
monetary_account_id: 1,
|
||||
amount: {
|
||||
currency: 'EUR',
|
||||
value: '10.00'
|
||||
},
|
||||
description: 'Test payment',
|
||||
type: 'IDEAL',
|
||||
...paymentData
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a test account notification
|
||||
*/
|
||||
public createTestAccountNotification(accountData: any): any {
|
||||
return {
|
||||
NotificationUrl: {
|
||||
target_url: 'https://example.com/webhook',
|
||||
category: 'MONETARY_ACCOUNT',
|
||||
event_type: 'MONETARY_ACCOUNT_UPDATED',
|
||||
object: {
|
||||
MonetaryAccountBank: {
|
||||
id: 1234,
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
balance: {
|
||||
currency: 'EUR',
|
||||
value: '100.00'
|
||||
},
|
||||
...accountData
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook event types
|
||||
*/
|
||||
export enum BunqWebhookEventType {
|
||||
// Payment events
|
||||
PAYMENT_CREATED = 'PAYMENT_CREATED',
|
||||
PAYMENT_UPDATED = 'PAYMENT_UPDATED',
|
||||
PAYMENT_CANCELLED = 'PAYMENT_CANCELLED',
|
||||
|
||||
// Account events
|
||||
MONETARY_ACCOUNT_CREATED = 'MONETARY_ACCOUNT_CREATED',
|
||||
MONETARY_ACCOUNT_UPDATED = 'MONETARY_ACCOUNT_UPDATED',
|
||||
MONETARY_ACCOUNT_CLOSED = 'MONETARY_ACCOUNT_CLOSED',
|
||||
|
||||
// Card events
|
||||
CARD_CREATED = 'CARD_CREATED',
|
||||
CARD_UPDATED = 'CARD_UPDATED',
|
||||
CARD_CANCELLED = 'CARD_CANCELLED',
|
||||
CARD_TRANSACTION = 'CARD_TRANSACTION',
|
||||
|
||||
// Request events
|
||||
REQUEST_INQUIRY_CREATED = 'REQUEST_INQUIRY_CREATED',
|
||||
REQUEST_INQUIRY_UPDATED = 'REQUEST_INQUIRY_UPDATED',
|
||||
REQUEST_INQUIRY_ACCEPTED = 'REQUEST_INQUIRY_ACCEPTED',
|
||||
REQUEST_INQUIRY_REJECTED = 'REQUEST_INQUIRY_REJECTED',
|
||||
|
||||
// Other events
|
||||
SCHEDULE_RESULT = 'SCHEDULE_RESULT',
|
||||
TAB_RESULT = 'TAB_RESULT',
|
||||
DRAFT_PAYMENT_CREATED = 'DRAFT_PAYMENT_CREATED',
|
||||
DRAFT_PAYMENT_UPDATED = 'DRAFT_PAYMENT_UPDATED'
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook middleware for Express.js
|
||||
*/
|
||||
export function bunqWebhookMiddleware(
|
||||
bunqAccount: BunqAccount,
|
||||
handler: BunqWebhookHandler
|
||||
) {
|
||||
const notification = new BunqNotification(bunqAccount);
|
||||
|
||||
return async (req: any, res: any, next: any) => {
|
||||
try {
|
||||
// Get signature from headers
|
||||
const signature = req.headers['x-bunq-server-signature'];
|
||||
|
||||
if (!signature) {
|
||||
res.status(401).send('Missing signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get raw body
|
||||
const body = JSON.stringify(req.body);
|
||||
|
||||
// Verify signature
|
||||
const isValid = notification.verifyWebhookSignature(body, signature);
|
||||
|
||||
if (!isValid) {
|
||||
res.status(401).send('Invalid signature');
|
||||
return;
|
||||
}
|
||||
|
||||
// Process notification
|
||||
await handler.process(req.body);
|
||||
|
||||
res.status(200).send('OK');
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
@@ -108,6 +108,7 @@ export interface IBunqPaymentRequest {
|
||||
}>;
|
||||
merchant_reference?: string;
|
||||
allow_bunqto?: boolean;
|
||||
request_reference_split_the_bill?: string;
|
||||
}
|
||||
|
||||
export interface IBunqScheduledPaymentRequest extends IBunqPaymentRequest {
|
||||
@@ -254,4 +255,30 @@ export interface IBunqRequestInquiry {
|
||||
address_shipping?: any;
|
||||
geolocation?: any;
|
||||
allow_chat?: boolean;
|
||||
}
|
||||
|
||||
export interface IBunqPaymentBatch {
|
||||
id: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
payments: IBunqPayment[];
|
||||
status: string;
|
||||
total_amount: IBunqAmount;
|
||||
reference?: string;
|
||||
}
|
||||
|
||||
export interface IBunqSchedulePayment {
|
||||
id: number;
|
||||
created: string;
|
||||
updated: string;
|
||||
status: string;
|
||||
payment: IBunqPaymentRequest;
|
||||
schedule: IBunqSchedule;
|
||||
}
|
||||
|
||||
export interface IBunqSchedule {
|
||||
time_start: string;
|
||||
time_end: string;
|
||||
recurrence_unit: 'ONCE' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
recurrence_size: number;
|
||||
}
|
@@ -12,6 +12,8 @@ export * from './bunq.classes.user.js';
|
||||
|
||||
// Payment and financial classes
|
||||
export * from './bunq.classes.payment.js';
|
||||
export * from './bunq.classes.paymentbatch.js';
|
||||
export * from './bunq.classes.scheduledpayment.js';
|
||||
export * from './bunq.classes.card.js';
|
||||
export * from './bunq.classes.request.js';
|
||||
export * from './bunq.classes.schedule.js';
|
||||
|
Reference in New Issue
Block a user