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 { 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 { 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 { 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 { 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 { 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 { await this.update({ status: 'CANCELLED' }); } /** * List draft payments */ public static async list( bunqAccount: BunqAccount, monetaryAccountId: number, options?: IBunqPaginationOptions ): Promise { 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 { 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 = {}; 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; } }