Files
bunq/ts/bunq.classes.draft.ts

374 lines
9.5 KiB
TypeScript

import * as plugins from './bunq.plugins.js';
import { BunqAccount } from './bunq.classes.account.js';
import { BunqMonetaryAccount } from './bunq.classes.monetaryaccount.js';
import type {
IBunqPaymentRequest,
IBunqAmount,
IBunqAlias,
IBunqPaginationOptions
} from './bunq.interfaces.js';
export class BunqDraftPayment {
private bunqAccount: BunqAccount;
private monetaryAccount: BunqMonetaryAccount;
public id?: number;
public created?: string;
public updated?: string;
public status?: string;
public entries?: IDraftPaymentEntry[];
constructor(bunqAccount: BunqAccount, monetaryAccount: BunqMonetaryAccount) {
this.bunqAccount = bunqAccount;
this.monetaryAccount = monetaryAccount;
}
/**
* Create a draft payment
*/
public async create(options: {
description?: string;
status?: 'DRAFT' | 'PENDING' | 'AWAITING_SIGNATURE';
entries: IDraftPaymentEntry[];
previousAttachmentId?: number;
numberOfRequiredAccepts?: number;
}): Promise<number> {
await this.bunqAccount.apiContext.ensureValidSession();
// Convert to snake_case for API
const apiPayload: any = {
entries: options.entries,
};
if (options.description) apiPayload.description = options.description;
if (options.status) apiPayload.status = options.status;
if (options.previousAttachmentId) apiPayload.previous_attachment_id = options.previousAttachmentId;
if (options.numberOfRequiredAccepts !== undefined) apiPayload.number_of_required_accepts = options.numberOfRequiredAccepts;
const response = await this.bunqAccount.getHttpClient().post(
`/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/draft-payment`,
apiPayload
);
if (response.Response && response.Response[0] && response.Response[0].Id) {
this.id = response.Response[0].Id.id;
return this.id;
}
throw new Error('Failed to create draft payment');
}
/**
* Get draft payment details
*/
public async get(): Promise<any> {
if (!this.id) {
throw new Error('Draft payment ID not set');
}
await this.bunqAccount.apiContext.ensureValidSession();
const response = await this.bunqAccount.getHttpClient().get(
`/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/draft-payment/${this.id}`
);
if (response.Response && response.Response[0]) {
const data = response.Response[0].DraftPayment;
this.updateFromApiResponse(data);
return data;
}
throw new Error('Draft payment not found');
}
/**
* Update draft payment
*/
public async update(updates: {
description?: string;
status?: 'CANCELLED';
entries?: IDraftPaymentEntry[];
previousAttachmentId?: number;
}): Promise<void> {
if (!this.id) {
throw new Error('Draft payment ID not set');
}
await this.bunqAccount.apiContext.ensureValidSession();
// Convert to snake_case for API
const apiPayload: any = {};
if (updates.description !== undefined) apiPayload.description = updates.description;
if (updates.status !== undefined) apiPayload.status = updates.status;
if (updates.entries !== undefined) apiPayload.entries = updates.entries;
if (updates.previousAttachmentId !== undefined) apiPayload.previous_attachment_id = updates.previousAttachmentId;
await this.bunqAccount.getHttpClient().put(
`/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/draft-payment/${this.id}`,
apiPayload // Send object directly, not wrapped in array
);
await this.get();
}
/**
* Accept the draft payment (sign it)
*/
public async accept(): Promise<void> {
if (!this.id) {
throw new Error('Draft payment ID not set');
}
await this.bunqAccount.apiContext.ensureValidSession();
await this.bunqAccount.getHttpClient().post(
`/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/draft-payment/${this.id}/accept`,
{}
);
}
/**
* Reject the draft payment
*/
public async reject(reason?: string): Promise<void> {
if (!this.id) {
throw new Error('Draft payment ID not set');
}
await this.bunqAccount.apiContext.ensureValidSession();
await this.bunqAccount.getHttpClient().post(
`/v1/user/${this.bunqAccount.userId}/monetary-account/${this.monetaryAccount.id}/draft-payment/${this.id}/reject`,
{
reason: reason
}
);
}
/**
* Cancel the draft payment
*/
public async cancel(): Promise<void> {
await this.update({ status: 'CANCELLED' });
}
/**
* List draft payments
*/
public static async list(
bunqAccount: BunqAccount,
monetaryAccountId: number,
options?: IBunqPaginationOptions
): Promise<any[]> {
await bunqAccount.apiContext.ensureValidSession();
const response = await bunqAccount.getHttpClient().list(
`/v1/user/${bunqAccount.userId}/monetary-account/${monetaryAccountId}/draft-payment`,
options
);
return response.Response || [];
}
/**
* Update properties from API response
*/
private updateFromApiResponse(data: any): void {
this.created = data.created;
this.updated = data.updated;
this.status = data.status;
this.entries = data.entries;
}
/**
* Create a builder for draft payments
*/
public static builder(
bunqAccount: BunqAccount,
monetaryAccount: BunqMonetaryAccount
): DraftPaymentBuilder {
return new DraftPaymentBuilder(bunqAccount, monetaryAccount);
}
}
/**
* Draft payment entry interface
*/
export interface IDraftPaymentEntry {
amount: IBunqAmount;
counterparty_alias: IBunqAlias;
description: string;
merchant_reference?: string;
attachment?: Array<{ id: number }>;
}
/**
* Builder class for creating draft payments
*/
export class DraftPaymentBuilder {
private bunqAccount: BunqAccount;
private monetaryAccount: BunqMonetaryAccount;
private description?: string;
private entries: IDraftPaymentEntry[] = [];
private numberOfRequiredAccepts?: number;
constructor(bunqAccount: BunqAccount, monetaryAccount: BunqMonetaryAccount) {
this.bunqAccount = bunqAccount;
this.monetaryAccount = monetaryAccount;
}
/**
* Set draft description
*/
public setDescription(description: string): this {
this.description = description;
return this;
}
/**
* Add a payment entry
*/
public addEntry(entry: IDraftPaymentEntry): this {
this.entries.push(entry);
return this;
}
/**
* Add a payment entry with builder pattern
*/
public addPayment(): DraftPaymentEntryBuilder {
return new DraftPaymentEntryBuilder(this);
}
/**
* Set number of required accepts
*/
public requireAccepts(count: number): this {
this.numberOfRequiredAccepts = count;
return this;
}
/**
* Create the draft payment
*/
public async create(): Promise<BunqDraftPayment> {
if (this.entries.length === 0) {
throw new Error('At least one payment entry is required');
}
const draft = new BunqDraftPayment(this.bunqAccount, this.monetaryAccount);
await draft.create({
description: this.description,
entries: this.entries,
numberOfRequiredAccepts: this.numberOfRequiredAccepts,
status: 'DRAFT'
});
return draft;
}
/**
* Internal method to add entry
*/
public _addEntry(entry: IDraftPaymentEntry): void {
this.entries.push(entry);
}
}
/**
* Builder for individual draft payment entries
*/
export class DraftPaymentEntryBuilder {
private builder: DraftPaymentBuilder;
private entry: Partial<IDraftPaymentEntry> = {};
constructor(builder: DraftPaymentBuilder) {
this.builder = builder;
}
/**
* Set the amount
*/
public amount(value: string, currency: string = 'EUR'): this {
this.entry.amount = { value, currency };
return this;
}
/**
* Set the counterparty by IBAN
*/
public toIban(iban: string, name?: string): this {
this.entry.counterparty_alias = {
type: 'IBAN',
value: iban,
name
};
return this;
}
/**
* Set the counterparty by email
*/
public toEmail(email: string, name?: string): this {
this.entry.counterparty_alias = {
type: 'EMAIL',
value: email,
name
};
return this;
}
/**
* Set the counterparty by phone number
*/
public toPhoneNumber(phoneNumber: string, name?: string): this {
this.entry.counterparty_alias = {
type: 'PHONE_NUMBER',
value: phoneNumber,
name
};
return this;
}
/**
* Set the description
*/
public description(description: string): this {
this.entry.description = description;
return this;
}
/**
* Set merchant reference
*/
public merchantReference(reference: string): this {
this.entry.merchant_reference = reference;
return this;
}
/**
* Add attachments
*/
public attachments(attachmentIds: number[]): this {
this.entry.attachment = attachmentIds.map(id => ({ id }));
return this;
}
/**
* Add the entry and return to builder
*/
public add(): DraftPaymentBuilder {
if (!this.entry.amount) {
throw new Error('Amount is required for payment entry');
}
if (!this.entry.counterparty_alias) {
throw new Error('Counterparty is required for payment entry');
}
if (!this.entry.description) {
throw new Error('Description is required for payment entry');
}
this.builder._addEntry(this.entry as IDraftPaymentEntry);
return this.builder;
}
}