374 lines
9.5 KiB
TypeScript
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;
|
|
}
|
|
} |